eslint-plugin-unicorn 67.0.0 → 69.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/index.js +4 -0
  2. package/package.json +8 -3
  3. package/readme.md +165 -16
  4. package/rules/ast/is-empty-node.js +1 -5
  5. package/rules/ast/is-empty-object-expression.js +2 -2
  6. package/rules/better-dom-traversing.js +17 -8
  7. package/rules/class-reference-in-static-methods.js +14 -2
  8. package/rules/comment-content.js +136 -18
  9. package/rules/consistent-assert.js +1 -1
  10. package/rules/consistent-boolean-name.js +44 -5
  11. package/rules/consistent-compound-words.js +7 -5
  12. package/rules/consistent-conditional-object-spread.js +360 -0
  13. package/rules/consistent-destructuring.js +5 -0
  14. package/rules/consistent-existence-index-check.js +1 -1
  15. package/rules/consistent-json-file-read.js +4 -16
  16. package/rules/consistent-optional-chaining.js +3 -20
  17. package/rules/consistent-tuple-labels.js +49 -0
  18. package/rules/custom-error-definition.js +26 -7
  19. package/rules/default-export-style.js +569 -0
  20. package/rules/expiring-todo-comments.js +1 -1
  21. package/rules/explicit-length-check.js +7 -32
  22. package/rules/import-style.js +8 -6
  23. package/rules/index.js +50 -1
  24. package/rules/logical-assignment-operators.js +17 -4
  25. package/rules/{prevent-abbreviations.js → name-replacements.js} +11 -9
  26. package/rules/new-for-builtins.js +39 -5
  27. package/rules/no-accessor-recursion.js +7 -0
  28. package/rules/no-accidental-bitwise-operator.js +97 -0
  29. package/rules/no-array-callback-reference.js +1 -7
  30. package/rules/no-array-concat-in-loop.js +45 -0
  31. package/rules/no-array-front-mutation.js +64 -0
  32. package/rules/no-array-sort-for-min-max.js +225 -0
  33. package/rules/no-array-splice.js +78 -64
  34. package/rules/no-boolean-sort-comparator.js +215 -0
  35. package/rules/no-break-in-nested-loop.js +33 -0
  36. package/rules/no-chained-comparison.js +116 -0
  37. package/rules/no-collection-bracket-access.js +165 -0
  38. package/rules/no-computed-property-existence-check.js +6 -0
  39. package/rules/no-confusing-array-splice.js +2 -21
  40. package/rules/no-confusing-array-with.js +2 -20
  41. package/rules/no-constant-zero-expression.js +91 -0
  42. package/rules/no-declarations-before-early-exit.js +29 -36
  43. package/rules/no-double-comparison.js +117 -0
  44. package/rules/no-duplicate-if-branches.js +143 -0
  45. package/rules/no-duplicate-logical-operands.js +151 -0
  46. package/rules/no-for-each.js +11 -12
  47. package/rules/no-for-loop.js +3 -15
  48. package/rules/no-impossible-length-comparison.js +166 -0
  49. package/rules/no-incorrect-query-selector.js +8 -4
  50. package/rules/no-incorrect-template-string-interpolation.js +106 -43
  51. package/rules/no-instanceof-builtins.js +73 -14
  52. package/rules/no-invalid-argument-count.js +110 -54
  53. package/rules/no-invalid-character-comparison.js +121 -0
  54. package/rules/no-invalid-fetch-options.js +3 -1
  55. package/rules/no-invalid-well-known-symbol-methods.js +205 -0
  56. package/rules/no-keyword-prefix.js +14 -4
  57. package/rules/no-late-current-target-access.js +13 -107
  58. package/rules/no-late-event-control.js +127 -0
  59. package/rules/no-loop-iterable-mutation.js +541 -0
  60. package/rules/no-manually-wrapped-comments.js +10 -2
  61. package/rules/no-misrefactored-assignment.js +102 -0
  62. package/rules/no-named-default.js +35 -2
  63. package/rules/no-nonstandard-builtin-properties.js +1383 -0
  64. package/rules/no-object-methods-with-collections.js +4 -289
  65. package/rules/no-process-exit.js +16 -12
  66. package/rules/no-redundant-comparison.js +5 -12
  67. package/rules/no-return-array-push.js +20 -5
  68. package/rules/no-selector-as-dom-name.js +150 -0
  69. package/rules/no-thenable.js +27 -11
  70. package/rules/no-top-level-side-effects.js +8 -1
  71. package/rules/no-uncalled-method.js +2 -8
  72. package/rules/no-undeclared-class-members.js +8 -6
  73. package/rules/no-unnecessary-boolean-comparison.js +131 -0
  74. package/rules/no-unnecessary-global-this.js +36 -1
  75. package/rules/no-unnecessary-splice.js +15 -20
  76. package/rules/no-unreadable-for-of-expression.js +2 -1
  77. package/rules/no-unreadable-object-destructuring.js +15 -0
  78. package/rules/no-unsafe-property-key.js +10 -15
  79. package/rules/no-unsafe-string-replacement.js +31 -0
  80. package/rules/no-unused-array-method-return.js +11 -2
  81. package/rules/no-unused-properties.js +4 -3
  82. package/rules/no-useless-boolean-cast.js +58 -1
  83. package/rules/no-useless-coercion.js +71 -15
  84. package/rules/no-useless-compound-assignment.js +110 -0
  85. package/rules/no-useless-concat.js +27 -3
  86. package/rules/no-useless-delete-check.js +353 -0
  87. package/rules/no-useless-else.js +27 -62
  88. package/rules/no-useless-error-capture-stack-trace.js +1 -1
  89. package/rules/no-useless-fallback-in-spread.js +7 -118
  90. package/rules/no-useless-iterator-to-array.js +165 -115
  91. package/rules/no-useless-logical-operand.js +248 -0
  92. package/rules/no-useless-undefined.js +11 -28
  93. package/rules/no-xor-as-exponentiation.js +59 -0
  94. package/rules/prefer-abort-signal-timeout.js +395 -0
  95. package/rules/prefer-aggregate-error.js +478 -0
  96. package/rules/prefer-array-find.js +95 -5
  97. package/rules/prefer-array-flat-map.js +9 -21
  98. package/rules/prefer-array-flat.js +1 -24
  99. package/rules/prefer-array-from-async.js +273 -0
  100. package/rules/prefer-array-iterable-methods.js +157 -0
  101. package/rules/prefer-array-some.js +56 -31
  102. package/rules/prefer-at.js +6 -30
  103. package/rules/prefer-await.js +29 -0
  104. package/rules/prefer-boolean-return.js +224 -0
  105. package/rules/prefer-code-point.js +58 -8
  106. package/rules/prefer-continue.js +377 -0
  107. package/rules/prefer-default-parameters.js +10 -6
  108. package/rules/prefer-dispose.js +1 -5
  109. package/rules/prefer-dom-node-replace-children.js +353 -0
  110. package/rules/prefer-early-return.js +160 -30
  111. package/rules/prefer-else-if.js +50 -53
  112. package/rules/prefer-error-is-error.js +181 -0
  113. package/rules/prefer-event-target.js +3 -13
  114. package/rules/prefer-flat-math-min-max.js +103 -0
  115. package/rules/prefer-global-this.js +1 -1
  116. package/rules/prefer-has-check.js +1 -4
  117. package/rules/prefer-hoisting-branch-code.js +424 -0
  118. package/rules/prefer-identifier-import-export-specifiers.js +6 -11
  119. package/rules/prefer-includes-over-repeated-comparisons.js +5 -1
  120. package/rules/prefer-includes.js +6 -0
  121. package/rules/prefer-iterator-concat.js +21 -1
  122. package/rules/prefer-logical-operator-over-ternary.js +12 -20
  123. package/rules/prefer-math-constants.js +103 -0
  124. package/rules/prefer-math-min-max.js +1 -5
  125. package/rules/prefer-minimal-ternary.js +41 -13
  126. package/rules/prefer-modern-dom-apis.js +1 -45
  127. package/rules/prefer-node-protocol.js +1 -0
  128. package/rules/prefer-number-properties.js +2 -5
  129. package/rules/prefer-object-from-entries.js +6 -1
  130. package/rules/prefer-object-iterable-methods.js +36 -11
  131. package/rules/prefer-observer-apis.js +498 -0
  132. package/rules/prefer-optional-catch-binding.js +30 -24
  133. package/rules/prefer-private-class-fields.js +2 -8
  134. package/rules/prefer-promise-try.js +238 -0
  135. package/rules/prefer-promise-with-resolvers.js +248 -0
  136. package/rules/prefer-query-selector.js +79 -17
  137. package/rules/prefer-queue-microtask.js +7 -9
  138. package/rules/prefer-regexp-escape.js +263 -0
  139. package/rules/prefer-set-methods.js +337 -0
  140. package/rules/prefer-set-size.js +10 -2
  141. package/rules/prefer-short-arrow-method.js +3 -3
  142. package/rules/prefer-single-replace.js +218 -0
  143. package/rules/prefer-spread.js +14 -21
  144. package/rules/prefer-string-replace-all.js +5 -0
  145. package/rules/prefer-structured-clone.js +2 -2
  146. package/rules/prefer-switch.js +1 -0
  147. package/rules/prefer-ternary.js +4 -2
  148. package/rules/prefer-toggle-attribute.js +293 -0
  149. package/rules/prefer-type-literal-last.js +22 -54
  150. package/rules/prefer-unary-minus.js +103 -0
  151. package/rules/prefer-url-can-parse.js +308 -0
  152. package/rules/prefer-url-search-parameters.js +411 -0
  153. package/rules/prefer-while-loop-condition.js +252 -0
  154. package/rules/require-passive-events.js +11 -4
  155. package/rules/require-proxy-trap-boolean-return.js +40 -126
  156. package/rules/rule/to-eslint-listener.js +5 -3
  157. package/rules/shared/array-concat-in-loop.js +151 -0
  158. package/rules/shared/identifier-checks.js +4 -16
  159. package/rules/shared/late-event-handler.js +82 -0
  160. package/rules/shared/{abbreviations.js → name-replacements.js} +63 -0
  161. package/rules/shared/no-array-mutate-rule.js +19 -2
  162. package/rules/shared/regexp-escape.js +81 -0
  163. package/rules/shared/splice-replacements.js +1 -18
  164. package/rules/string-content.js +11 -4
  165. package/rules/template-indent.js +2 -8
  166. package/rules/throw-new-error.js +7 -0
  167. package/rules/utils/block-scope.js +42 -0
  168. package/rules/utils/builtin-collection-type.js +267 -0
  169. package/rules/utils/builtins.js +50 -1
  170. package/rules/utils/comments.js +16 -0
  171. package/rules/utils/comparison.js +17 -0
  172. package/rules/utils/contains-suspension-point.js +35 -0
  173. package/rules/utils/get-const-variable-initializer.js +31 -0
  174. package/rules/utils/global-reference-tracker.js +14 -1
  175. package/rules/utils/has-optional-chain-element.js +36 -4
  176. package/rules/utils/index.js +38 -2
  177. package/rules/utils/is-boolean.js +26 -1
  178. package/rules/utils/is-event.js +127 -0
  179. package/rules/utils/is-function-self-used-inside.js +1 -5
  180. package/rules/utils/is-weak-map.js +21 -0
  181. package/rules/utils/is-weak-set.js +21 -0
  182. package/rules/utils/length-or-size.js +53 -0
  183. package/rules/utils/member-expression.js +18 -0
  184. package/rules/utils/needs-semicolon.js +1 -5
  185. package/rules/utils/numeric.js +20 -0
  186. package/rules/utils/should-add-parentheses-to-logical-expression-child.js +8 -14
  187. package/rules/utils/should-add-parentheses-to-member-expression-object.js +1 -5
  188. package/rules/utils/should-add-parentheses-to-unary-expression.js +8 -2
  189. package/rules/utils/should-report-replace-children-receiver.js +210 -0
  190. package/rules/utils/track-branch-exits.js +79 -0
  191. package/rules/utils/type-helpers.js +166 -8
  192. package/rules/utils/types.js +22 -0
@@ -1,8 +1,10 @@
1
1
  import {isFunction, isLoop, loopTypes} from './ast/index.js';
2
2
 
3
3
  const MESSAGE_ID = 'no-break-in-nested-loop';
4
+ const SWITCH_CONTINUE_MESSAGE_ID = 'switch-continue';
4
5
  const messages = {
5
6
  [MESSAGE_ID]: 'Move this nested loop or switch into a function instead of using `{{keyword}}` here.',
7
+ [SWITCH_CONTINUE_MESSAGE_ID]: 'An unlabeled `continue` inside a `switch` continues the surrounding loop, not the next `case`. Use a labeled `continue` if that is intentional.',
6
8
  };
7
9
 
8
10
  const controlFlowNodeTypes = new Set([
@@ -56,6 +58,30 @@ function isNestedControlFlowStatement(node, sourceCode) {
56
58
  return false;
57
59
  }
58
60
 
61
+ function isContinueInSwitchInsideLoop(node, sourceCode) {
62
+ if (node.type !== 'ContinueStatement' || node.label) {
63
+ return false;
64
+ }
65
+
66
+ let hasSwitch = false;
67
+
68
+ for (const ancestor of sourceCode.getAncestors(node).toReversed()) {
69
+ if (isFunction(ancestor)) {
70
+ return false;
71
+ }
72
+
73
+ if (isLoop(ancestor)) {
74
+ return hasSwitch;
75
+ }
76
+
77
+ if (ancestor.type === 'SwitchStatement') {
78
+ hasSwitch = true;
79
+ }
80
+ }
81
+
82
+ return false;
83
+ }
84
+
59
85
  /** @param {import('eslint').Rule.RuleContext} context */
60
86
  const create = context => {
61
87
  const {sourceCode} = context;
@@ -68,6 +94,13 @@ const create = context => {
68
94
  return;
69
95
  }
70
96
 
97
+ if (isContinueInSwitchInsideLoop(node, sourceCode)) {
98
+ return {
99
+ node,
100
+ messageId: SWITCH_CONTINUE_MESSAGE_ID,
101
+ };
102
+ }
103
+
71
104
  return {
72
105
  node,
73
106
  messageId: MESSAGE_ID,
@@ -0,0 +1,116 @@
1
+ import {hasSideEffect} from '@eslint-community/eslint-utils';
2
+ import {getParenthesizedText, isParenthesized, isBoolean} from './utils/index.js';
3
+
4
+ const MESSAGE_ID_ERROR = 'no-chained-comparison/error';
5
+ const MESSAGE_ID_SUGGESTION = 'no-chained-comparison/suggestion';
6
+ const messages = {
7
+ [MESSAGE_ID_ERROR]: 'Comparison operators cannot be chained. The inner comparison evaluates to a boolean, which is then compared instead of the operands.',
8
+ [MESSAGE_ID_SUGGESTION]: 'Replace with `{{replacement}}`.',
9
+ };
10
+
11
+ const orderingOperators = new Set(['<', '>', '<=', '>=']);
12
+ const equalityOperators = new Set(['===', '!==', '==', '!=']);
13
+ const comparisonOperators = orderingOperators.union(equalityOperators);
14
+
15
+ const isComparison = node => node.type === 'BinaryExpression' && comparisonOperators.has(node.operator);
16
+ const isBooleanLiteral = node => node.type === 'Literal' && typeof node.value === 'boolean';
17
+
18
+ // The `&&` rewrite is a `LogicalExpression`, which binds looser than any comparison. It can only
19
+ // replace `node` where a surrounding operator won't recapture it: at the top of an expression or
20
+ // when already parenthesized, but not as an operand of another binary expression (`foo === a < b < c`)
21
+ // or mixed with `??` (which is a syntax error).
22
+ const isSafeForLogicalReplacement = (node, context) => {
23
+ if (isParenthesized(node, context)) {
24
+ return true;
25
+ }
26
+
27
+ const {parent} = node;
28
+ return parent.type !== 'BinaryExpression'
29
+ && !(parent.type === 'LogicalExpression' && parent.operator === '??');
30
+ };
31
+
32
+ /** @param {import('eslint').Rule.RuleContext} context */
33
+ const create = context => {
34
+ const {sourceCode} = context;
35
+
36
+ context.on('BinaryExpression', node => {
37
+ if (!comparisonOperators.has(node.operator)) {
38
+ return;
39
+ }
40
+
41
+ const nested = isComparison(node.left) ? node.left : node.right;
42
+ if (!isComparison(nested)) {
43
+ return;
44
+ }
45
+
46
+ const isNestedOnLeft = nested === node.left;
47
+ const sibling = isNestedOnLeft ? node.right : node.left;
48
+
49
+ // For equality operators, comparing a comparison result against another boolean value
50
+ // (`(a > 0) === (b > 0)`, `(a < b) === true`, `(a > 0) !== Boolean(b)`) is an intentional
51
+ // boolean comparison, not a chained comparison.
52
+ if (
53
+ equalityOperators.has(node.operator)
54
+ && isBoolean(sibling, context)
55
+ ) {
56
+ return;
57
+ }
58
+
59
+ const problem = {
60
+ node,
61
+ messageId: MESSAGE_ID_ERROR,
62
+ };
63
+
64
+ // Only the canonical left-associative chain `(a OP1 b) OP2 c` has an unambiguous
65
+ // `a OP1 b && b OP2 c` rewrite. The `&&` rewrite only reflects a plausible intent for a
66
+ // real ordering or equality chain, so the inner and outer operators must be the same kind
67
+ // and the outer operand must not be a boolean literal (`(a < b) < true` is not a range
68
+ // check). The middle operand `b` is duplicated, so the rewrite must also be free of side
69
+ // effects and comments.
70
+ const isSameOperatorKind = orderingOperators.has(nested.operator) === orderingOperators.has(node.operator);
71
+ const isPlausibleChain = isSameOperatorKind && !isBooleanLiteral(node.right);
72
+ if (
73
+ isNestedOnLeft
74
+ && isPlausibleChain
75
+ // Both outer operands must be plain. A comparison at either end (`(a < b) < (c < d)`
76
+ // or a deeper chain `a < b < c < d`) has no single unambiguous `&&` rewrite.
77
+ && !isComparison(node.right)
78
+ && !isComparison(nested.left)
79
+ && isSafeForLogicalReplacement(node, context)
80
+ && !hasSideEffect(nested.right, sourceCode)
81
+ && sourceCode.getCommentsInside(node).length === 0
82
+ ) {
83
+ const replacement = `${sourceCode.getText(node.left)} && ${getParenthesizedText(nested.right, context)} ${node.operator} ${getParenthesizedText(node.right, context)}`;
84
+
85
+ problem.suggest = [
86
+ {
87
+ messageId: MESSAGE_ID_SUGGESTION,
88
+ data: {replacement},
89
+ /** @param {import('eslint').Rule.RuleFixer} fixer */
90
+ fix: fixer => fixer.replaceText(node, replacement),
91
+ },
92
+ ];
93
+ }
94
+
95
+ return problem;
96
+ });
97
+ };
98
+
99
+ /** @type {import('eslint').Rule.RuleModule} */
100
+ const config = {
101
+ create,
102
+ meta: {
103
+ type: 'problem',
104
+ docs: {
105
+ description: 'Disallow chained comparisons such as `a < b < c`.',
106
+ recommended: 'unopinionated',
107
+ },
108
+ hasSuggestions: true,
109
+ messages,
110
+ languages: [
111
+ 'js/js',
112
+ ],
113
+ },
114
+ };
115
+
116
+ export default config;
@@ -0,0 +1,165 @@
1
+ import {getStaticValue} from '@eslint-community/eslint-utils';
2
+ import {
3
+ isMap,
4
+ isSet,
5
+ isWeakMap,
6
+ isWeakSet,
7
+ isLeftHandSide,
8
+ getParenthesizedText,
9
+ } from './utils/index.js';
10
+
11
+ /**
12
+ @import * as ESLint from 'eslint';
13
+ */
14
+
15
+ const MESSAGE_ID_READ = 'no-collection-bracket-access/read';
16
+ const MESSAGE_ID_WRITE = 'no-collection-bracket-access/write';
17
+ const MESSAGE_ID_DELETE = 'no-collection-bracket-access/delete';
18
+ const MESSAGE_ID_SUGGESTION_SET = 'no-collection-bracket-access/suggestion-set';
19
+ const MESSAGE_ID_SUGGESTION_DELETE = 'no-collection-bracket-access/suggestion-delete';
20
+ const messages = {
21
+ [MESSAGE_ID_READ]: 'Do not read from a `{{type}}` using bracket notation.',
22
+ [MESSAGE_ID_WRITE]: 'Do not write to a `{{type}}` using bracket notation.',
23
+ [MESSAGE_ID_DELETE]: 'Do not delete from a `{{type}}` using bracket notation.',
24
+ [MESSAGE_ID_SUGGESTION_SET]: 'Switch to `{{type}}#set()`.',
25
+ [MESSAGE_ID_SUGGESTION_DELETE]: 'Switch to `{{type}}#delete()`.',
26
+ };
27
+
28
+ const collectionTypes = [
29
+ {
30
+ name: 'Map',
31
+ is: isMap,
32
+ prototype: Map.prototype,
33
+ hasSetMethod: true,
34
+ },
35
+ {
36
+ name: 'WeakMap',
37
+ is: isWeakMap,
38
+ prototype: WeakMap.prototype,
39
+ hasSetMethod: true,
40
+ },
41
+ {
42
+ name: 'Set',
43
+ is: isSet,
44
+ prototype: Set.prototype,
45
+ hasSetMethod: false,
46
+ },
47
+ {
48
+ name: 'WeakSet',
49
+ is: isWeakSet,
50
+ prototype: WeakSet.prototype,
51
+ hasSetMethod: false,
52
+ },
53
+ ];
54
+
55
+ // Only `Identifier`/`ThisExpression`/`MemberExpression` receivers can be placed before
56
+ // `.method(…)` without parentheses, so suggestions are limited to them.
57
+ const simpleObjectTypes = new Set(['Identifier', 'ThisExpression', 'MemberExpression']);
58
+
59
+ function getProblem(node, collection, context) {
60
+ const {sourceCode} = context;
61
+ const {name: type} = collection;
62
+
63
+ // An optional access (`map?.['foo']`) wraps the member in a `ChainExpression`.
64
+ const outerNode = node.parent.type === 'ChainExpression' ? node.parent : node;
65
+ const {parent} = outerNode;
66
+ const object = getParenthesizedText(node.object, context);
67
+ const key = getParenthesizedText(node.property, context);
68
+
69
+ let messageId = MESSAGE_ID_READ;
70
+ let suggestionMessageId;
71
+ let replacement;
72
+ if (
73
+ parent.type === 'UnaryExpression'
74
+ && parent.operator === 'delete'
75
+ && parent.argument === outerNode
76
+ ) {
77
+ messageId = MESSAGE_ID_DELETE;
78
+ suggestionMessageId = MESSAGE_ID_SUGGESTION_DELETE;
79
+ replacement = `${object}.delete(${key})`;
80
+ } else if (
81
+ isLeftHandSide(node)
82
+ || (
83
+ (parent.type === 'ForOfStatement' || parent.type === 'ForInStatement')
84
+ && parent.left === outerNode
85
+ )
86
+ ) {
87
+ messageId = MESSAGE_ID_WRITE;
88
+ if (
89
+ collection.hasSetMethod
90
+ && parent.type === 'AssignmentExpression'
91
+ && parent.operator === '='
92
+ && parent.left === node
93
+ ) {
94
+ suggestionMessageId = MESSAGE_ID_SUGGESTION_SET;
95
+ replacement = `${object}.set(${key}, ${getParenthesizedText(parent.right, context)})`;
96
+ }
97
+ }
98
+
99
+ const problem = {node, messageId, data: {type}};
100
+
101
+ // Only suggest for the simple, statement-level case, to avoid changing the value of the
102
+ // surrounding expression, dropping comments, or needing to parenthesize the receiver.
103
+ if (
104
+ suggestionMessageId
105
+ && !node.optional
106
+ && simpleObjectTypes.has(node.object.type)
107
+ && parent.parent.type === 'ExpressionStatement'
108
+ && sourceCode.getCommentsInside(parent).length === 0
109
+ ) {
110
+ problem.suggest = [
111
+ {
112
+ messageId: suggestionMessageId,
113
+ data: {type},
114
+ fix: fixer => fixer.replaceText(parent, replacement),
115
+ },
116
+ ];
117
+ }
118
+
119
+ return problem;
120
+ }
121
+
122
+ /** @param {ESLint.Rule.RuleContext} context */
123
+ const create = context => {
124
+ context.on('MemberExpression', node => {
125
+ if (!node.computed) {
126
+ return;
127
+ }
128
+
129
+ const collection = collectionTypes.find(({is}) => is(node.object, context));
130
+ if (!collection) {
131
+ return;
132
+ }
133
+
134
+ // Allow accessing a real member, including `Symbol` ones like `map[Symbol.iterator]`.
135
+ const staticResult = getStaticValue(node.property, context.sourceCode.getScope(node.property));
136
+ if (
137
+ staticResult
138
+ && (typeof staticResult.value === 'string' || typeof staticResult.value === 'symbol')
139
+ && Reflect.has(collection.prototype, staticResult.value)
140
+ ) {
141
+ return;
142
+ }
143
+
144
+ return getProblem(node, collection, context);
145
+ });
146
+ };
147
+
148
+ /** @type {ESLint.Rule.RuleModule} */
149
+ const config = {
150
+ create,
151
+ meta: {
152
+ type: 'problem',
153
+ docs: {
154
+ description: 'Disallow accessing `Map`, `Set`, `WeakMap`, and `WeakSet` entries with bracket notation.',
155
+ recommended: 'unopinionated',
156
+ },
157
+ hasSuggestions: true,
158
+ messages,
159
+ languages: [
160
+ 'js/js',
161
+ ],
162
+ },
163
+ };
164
+
165
+ export default config;
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  getParenthesizedText,
3
3
  hasOptionalChainElement,
4
+ isBoolean,
4
5
  isBooleanExpression,
5
6
  isControlFlowTest,
6
7
  wouldRemoveComments,
@@ -120,6 +121,11 @@ const create = context => {
120
121
  return;
121
122
  }
122
123
 
124
+ // A property whose value type is a known boolean is a value read, not an existence check (and `no-unnecessary-boolean-comparison` may rewrite a comparison into this form).
125
+ if (isBoolean(node, context)) {
126
+ return;
127
+ }
128
+
123
129
  return {
124
130
  node,
125
131
  messageId: MESSAGE_ID,
@@ -1,5 +1,5 @@
1
- import {isMethodCall, isNumericLiteral} from './ast/index.js';
2
- import {getParenthesizedText} from './utils/index.js';
1
+ import {isMethodCall} from './ast/index.js';
2
+ import {getParenthesizedText, getStaticNumberValue} from './utils/index.js';
3
3
  import {isSame, unwrapExpression} from './utils/comparison.js';
4
4
 
5
5
  const REPLACE_ONE_ELEMENT = 'replace-one-element';
@@ -13,25 +13,6 @@ const messages = {
13
13
  [SUGGESTION_INSERT_AT_NEGATIVE_ONE]: 'Resolve the insertion index explicitly.',
14
14
  };
15
15
 
16
- function getStaticNumberValue(node) {
17
- node = unwrapExpression(node);
18
-
19
- if (isNumericLiteral(node)) {
20
- return node.value;
21
- }
22
-
23
- if (
24
- node.type === 'UnaryExpression'
25
- && (node.operator === '+' || node.operator === '-')
26
- ) {
27
- const value = getStaticNumberValue(node.argument);
28
-
29
- if (typeof value === 'number') {
30
- return node.operator === '-' ? -value : value;
31
- }
32
- }
33
- }
34
-
35
16
  const isNegativeStaticIndex = node => Math.trunc(getStaticNumberValue(node)) < 0;
36
17
 
37
18
  function getNormalizedDeleteCountValue(node) {
@@ -1,4 +1,5 @@
1
- import {isMethodCall, isNumericLiteral} from './ast/index.js';
1
+ import {isMethodCall} from './ast/index.js';
2
+ import {getStaticNumberValue} from './utils/index.js';
2
3
  import {isSame, unwrapExpression} from './utils/comparison.js';
3
4
 
4
5
  const MESSAGE_ID_NEGATIVE_INDEX = 'negative-index';
@@ -13,25 +14,6 @@ const messages = {
13
14
  @import * as ESLint from 'eslint';
14
15
  */
15
16
 
16
- function getStaticNumberValue(node) {
17
- node = unwrapExpression(node);
18
-
19
- if (isNumericLiteral(node)) {
20
- return node.value;
21
- }
22
-
23
- if (
24
- node.type === 'UnaryExpression'
25
- && (node.operator === '+' || node.operator === '-')
26
- ) {
27
- const value = getStaticNumberValue(node.argument);
28
-
29
- if (typeof value === 'number') {
30
- return node.operator === '-' ? -value : value;
31
- }
32
- }
33
- }
34
-
35
17
  const isNegativeStaticIndex = node => Math.trunc(getStaticNumberValue(node)) < 0;
36
18
 
37
19
  function isLengthMemberExpressionFor(node, object) {
@@ -0,0 +1,91 @@
1
+ import {getStaticValue} from '@eslint-community/eslint-utils';
2
+ import {isLiteral} from './ast/index.js';
3
+
4
+ const MESSAGE_ID_ERROR = 'no-constant-zero-expression/error';
5
+ const MESSAGE_ID_SUGGESTION = 'no-constant-zero-expression/suggestion';
6
+ const messages = {
7
+ [MESSAGE_ID_ERROR]: 'This operation always evaluates to `0` (or `-0`/`NaN`), which is likely a mistake.',
8
+ [MESSAGE_ID_SUGGESTION]: 'Replace this expression with `0`.',
9
+ };
10
+
11
+ const isZero = node => isLiteral(node, 0);
12
+
13
+ /**
14
+ Check if a binary expression has a `0` operand that dominates the result.
15
+
16
+ `x * 0` / `0 * x` and `x & 0` / `0 & x` are dominated by a `0` on either side. `0 / x` is dominated only when the `0` is on the left, since `x / 0` is `Infinity`/`NaN`.
17
+
18
+ @param {import('estree').BinaryExpression} node
19
+ @returns {boolean}
20
+ */
21
+ const isErasedToZero = node => {
22
+ switch (node.operator) {
23
+ case '*':
24
+ case '&': {
25
+ return isZero(node.left) || isZero(node.right);
26
+ }
27
+
28
+ case '/': {
29
+ return isZero(node.left);
30
+ }
31
+
32
+ default: {
33
+ return false;
34
+ }
35
+ }
36
+ };
37
+
38
+ /** @param {import('eslint').Rule.RuleContext} context */
39
+ const create = context => {
40
+ const {sourceCode} = context;
41
+
42
+ context.on('BinaryExpression', node => {
43
+ if (!isErasedToZero(node)) {
44
+ return;
45
+ }
46
+
47
+ const problem = {
48
+ node,
49
+ messageId: MESSAGE_ID_ERROR,
50
+ };
51
+
52
+ // Only suggest replacing with `0` when the whole expression provably folds to exactly `+0`.
53
+ // `getStaticValue` resolves only side-effect-free constants, and `Object.is(…, 0)` excludes
54
+ // `-0` (e.g. `-5 * 0`, `0 / -5`) and `NaN` (e.g. `0 / 0`), so those are reported without a suggestion.
55
+ const staticValue = getStaticValue(node, sourceCode.getScope(node));
56
+ if (
57
+ staticValue
58
+ && Object.is(staticValue.value, 0)
59
+ && sourceCode.getCommentsInside(node).length === 0
60
+ ) {
61
+ problem.suggest = [
62
+ {
63
+ messageId: MESSAGE_ID_SUGGESTION,
64
+ /** @param {import('eslint').Rule.RuleFixer} fixer */
65
+ fix: fixer => fixer.replaceText(node, '0'),
66
+ },
67
+ ];
68
+ }
69
+
70
+ return problem;
71
+ });
72
+ };
73
+
74
+ /** @type {import('eslint').Rule.RuleModule} */
75
+ const config = {
76
+ create,
77
+ meta: {
78
+ type: 'problem',
79
+ docs: {
80
+ description: 'Disallow arithmetic and bitwise operations that always evaluate to `0`.',
81
+ recommended: 'unopinionated',
82
+ },
83
+ hasSuggestions: true,
84
+ messages,
85
+ languages: [
86
+ 'js/js',
87
+ ],
88
+ },
89
+ };
90
+
91
+ export default config;
@@ -1,5 +1,5 @@
1
1
  import {isCommentToken, hasSideEffect} from '@eslint-community/eslint-utils';
2
- import {getReferences} from './utils/index.js';
2
+ import {containsSuspensionPoint, getReferences, trackBranchExits} from './utils/index.js';
3
3
 
4
4
  const MESSAGE_ID = 'no-declarations-before-early-exit';
5
5
  const messages = {
@@ -10,40 +10,17 @@ const isLetOrConstDeclaration = node =>
10
10
  node.type === 'VariableDeclaration'
11
11
  && (node.kind === 'let' || node.kind === 'const');
12
12
 
13
- const getLastStatement = node => {
14
- if (node.type !== 'BlockStatement') {
15
- return node;
16
- }
17
-
18
- return node.body.at(-1);
19
- };
20
-
21
- const isEarlyExitStatement = node =>
22
- node?.type === 'ReturnStatement'
23
- || node?.type === 'ThrowStatement'
24
- || node?.type === 'BreakStatement'
25
- || node?.type === 'ContinueStatement';
26
-
27
- function branchDefinitelyExits(node) {
28
- const statement = getLastStatement(node);
29
- if (isEarlyExitStatement(statement)) {
30
- return true;
31
- }
32
-
33
- return statement?.type === 'IfStatement'
34
- && statement.alternate
35
- && branchDefinitelyExits(statement.consequent)
36
- && branchDefinitelyExits(statement.alternate);
37
- }
38
-
39
- function isGuardStatement(node) {
13
+ // A guard is an `if` statement where exactly one branch always exits, so control after it is
14
+ // conditional. `branchAlwaysExits` uses code path analysis, so guards whose exiting branch ends
15
+ // in an exhaustive `switch`, `try`/`finally`, or other complex control flow are detected too.
16
+ function isGuardStatement(node, branchAlwaysExits) {
40
17
  if (node.type !== 'IfStatement') {
41
18
  return false;
42
19
  }
43
20
 
44
- const consequentExits = branchDefinitelyExits(node.consequent);
45
- const alternateExits = node.alternate && branchDefinitelyExits(node.alternate);
46
- return consequentExits !== Boolean(alternateExits);
21
+ const consequentExits = branchAlwaysExits(node.consequent);
22
+ const alternateExits = Boolean(node.alternate) && branchAlwaysExits(node.alternate);
23
+ return consequentExits !== alternateExits;
47
24
  }
48
25
 
49
26
  // https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts
@@ -161,6 +138,11 @@ function getProblem({
161
138
  return;
162
139
  }
163
140
 
141
+ // Moving a declaration whose initializer suspends (`await`/`yield`) would reorder the guard's condition across the suspension point, changing observable timing.
142
+ if (declarator.init && containsSuspensionPoint(declarator.init, sourceCode.visitorKeys)) {
143
+ return;
144
+ }
145
+
164
146
  if (
165
147
  guardIndex > declarationIndex + 1
166
148
  && !isSimpleInitializer(declarator.init)
@@ -213,7 +195,12 @@ function getProblem({
213
195
  return problem;
214
196
  }
215
197
 
216
- function * getStatementListProblems(sourceCode, statements) {
198
+ function * getStatementListProblems(sourceCode, statements, branchAlwaysExits) {
199
+ // Resolve each statement to itself when it is a guard (or `undefined` otherwise) at most
200
+ // once per list, instead of re-running the code-path-analysis check for every
201
+ // declaration/statement pair.
202
+ let guards;
203
+
217
204
  for (const [declarationIndex, declaration] of statements.entries()) {
218
205
  if (
219
206
  !isLetOrConstDeclaration(declaration)
@@ -222,8 +209,11 @@ function * getStatementListProblems(sourceCode, statements) {
222
209
  continue;
223
210
  }
224
211
 
225
- for (const [guardIndex, guardStatement] of statements.entries()) {
226
- if (guardIndex <= declarationIndex || !isGuardStatement(guardStatement)) {
212
+ guards ??= statements.map(statement => isGuardStatement(statement, branchAlwaysExits) ? statement : undefined);
213
+
214
+ for (let guardIndex = declarationIndex + 1; guardIndex < statements.length; guardIndex++) {
215
+ const guardStatement = guards[guardIndex];
216
+ if (!guardStatement) {
227
217
  continue;
228
218
  }
229
219
 
@@ -250,9 +240,12 @@ function * getStatementListProblems(sourceCode, statements) {
250
240
  /** @param {import('eslint').Rule.RuleContext} context */
251
241
  const create = context => {
252
242
  const {sourceCode} = context;
243
+ const branchAlwaysExits = trackBranchExits(context);
253
244
 
254
- context.on('Program', node => getStatementListProblems(sourceCode, node.body));
255
- context.on('BlockStatement', node => getStatementListProblems(sourceCode, node.body));
245
+ // Run on exit so that all nested `if` statements have been visited and their branch exit
246
+ // information is available before scanning each statement list.
247
+ context.onExit('Program', node => getStatementListProblems(sourceCode, node.body, branchAlwaysExits));
248
+ context.onExit('BlockStatement', node => getStatementListProblems(sourceCode, node.body, branchAlwaysExits));
256
249
  };
257
250
 
258
251
  /** @type {import('eslint').Rule.RuleModule} */