Add prefer-simple-condition-first rule#2902
Conversation
|
The auto-fix currently runs whenever the left side is "complex" and has no call/new expression. That still includes expressions like assignment/update/member chains, where swapping sides can change behvaior (or whether an exception is thrown because of short-circuiting order). Examples:
Because of that, these should be suggestion-only, not auto-fixed. Could we also add tests for these non-call side-effect/throwing cases? The current tests do not cover them, so this would be easy to miss. |
| 'if (foo);', | ||
|
|
||
| // Simple on left, complex on right — correct order | ||
| 'if (bar || foo());', |
There was a problem hiding this comment.
| 'if (bar || foo());', | |
| 'if (bar || foo());', | |
| // Potentially unsafe to reorder (side effects / throws) | |
| 'if ((state.ready = true) && ok);', | |
| 'if (++counter && ok);', | |
| 'if (object.deep.value && ok);', | |
| 'const x = object.deep.value || ok;', | |
| 'if (tag`x` && ok);', |
1ba694e to
75b5e71
Compare
3dc9a16 to
e17e0c2
Compare
|
Updated to follow your inline suggestion — side-effect patterns (assignments, updates, deep member chains, tagged templates) are now skipped entirely instead of flagged as suggestions. Calls/new still get suggestion-only treatment. |
| } | ||
|
|
||
| if (!isStatic || isPrivate || key.type === 'PrivateIdentifier') { | ||
| if (isPrivate || !isStatic || key.type === 'PrivateIdentifier') { |
There was a problem hiding this comment.
Let's consider !foo as "simple"?
There was a problem hiding this comment.
Done, !identifier is now treated as simple. Reverted the swaps in no-static-only-class and text-encoding-identifier-case since both sides are simple with this change.
|
A few things to address still: Negative numeric literals aren't treated as simple In the AST, This directly causes the unnecessary swap in the prefer-at rule: // Both sides are conceptually simple comparisons, but this gets flagged:
(startIndex === -1 && firstElementGetMethod === 'pop')Fix: in the
Should add Missing test cases Would be good to add tests for:
Minor: the Since the dogfooding config disables the rule, this swap shouldn't have been needed. Looks like it was auto-fixed before the dogfooding disable was added? I'd revert it. |
…on-first - Use getParenthesizedText/getParenthesizedRange to preserve parentheses when swapping conditions, preventing precedence changes - Add shouldAddParenthesesToLogicalExpressionChild for readability parens - Detect TaggedTemplateExpression as call-like (suggestion, not auto-fix) - Add test cases for parenthesized expressions, unary, tagged templates, and nullish coalescing operator
- Skip expressions with side effects (assignments, updates, deep member chains, tagged templates) entirely - Prevent auto-fix oscillation in chained logical expressions by treating all-simple chains as simple - Use suggestion instead of auto-fix for mixed chains and calls - Auto-fix simple condition order across codebase - Disable rule in dogfooding config (suggestion-only violations remain in fallback || patterns)
- Revert two || operand swaps in remove-argument.js and prefer-object-from-entries.js that broke fallback patterns (parentheses[0] must be tried first, node/initObject is the fallback) - Merge hasCall and hasSideEffectsOrThrows into single isUnsafeToAutoFix function, eliminating duplicated traversal - Make side-effect patterns report as suggestion-only instead of silently skipping (assignment, update, deep member, tagged template) - Add test cases for nested side effects to verify recursive detection
…ggestion
Follow the inline code suggestion: expressions with assignments, updates,
deep member chains, or tagged templates are now not flagged at all, matching
the documented behavior ("not flagged, since reordering would change program
behavior"). Calls/new remain as suggestion-only.
- Split isUnsafeToAutoFix into hasSideEffectsOrThrows (skip) and
hasCallOrNew (suggestion only)
- Move 9 test cases from invalid to valid
- Update dogfooding config comment
Per review feedback, negated identifiers like `!foo` are now considered simple. Reverts dogfooding swaps in no-static-only-class and text-encoding-identifier-case where both sides were already simple.
Now that `!identifier` is simple, these dogfooding reorderings are no longer flagged by the rule. Restores original condition order.
5195329 to
70806e6
Compare
- Handle negative/positive numeric literals as simple operands in isSimple() - Add AwaitExpression, YieldExpression, ImportExpression to hasSideEffectsOrThrows() - Add test cases for await, yield, import(), negative literals, optional chaining - Revert unnecessary test/package.js swap
|
|
Some more tests I would add:
|
- Treat any non-optional MemberExpression, `in`, and `instanceof` as potentially throwing (not just deep member chains) - Only flag LogicalExpressions in boolean contexts (if/while/for/ ternary/!) — value-producing contexts are excluded since reordering changes the result - Move affected test cases from invalid to valid - Add test cases for shallow member access, computed access, in/instanceof, value-producing contexts, and non-if boolean contexts (while, for, ternary)
Fixes #2853.
Adds a new rule that flags logical expressions (
&&,||) where a simple condition appears on the right but a complex one appears on the left, and offers to swap them.What's "simple"
Per fisker's scoping in #2853 (comment):
foo)identifier === literaloridentifier !== literal(strict equality/inequality between identifiers and/or literals)Everything else is considered "complex."
Fix safety
CallExpressionorNewExpression— reordering changes short-circuit evaluation timing, as noted by @axetroyExamples