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
@@ -0,0 +1,117 @@
1
+ import {getParenthesizedRange, getParenthesizedText} from './utils/index.js';
2
+ import {
3
+ comparisonOperators,
4
+ containsOptionalChain,
5
+ flipOperator,
6
+ isSame,
7
+ unwrapExpression,
8
+ } from './utils/comparison.js';
9
+
10
+ const MESSAGE_ID = 'no-double-comparison';
11
+ const MESSAGE_ID_SUGGESTION = 'no-double-comparison/suggestion';
12
+ const messages = {
13
+ [MESSAGE_ID]: 'These comparisons can be simplified to `{{replacement}}`.',
14
+ [MESSAGE_ID_SUGGESTION]: 'Replace with `{{replacement}}`.',
15
+ };
16
+
17
+ // Map of `<logical-operator> <comparison operators>` to the combined operator. The two comparison operators are registered in both orders so the lookup is order-independent.
18
+ const reductionKey = (logicalOperator, first, second) => `${logicalOperator} ${first} ${second}`;
19
+ const reductions = new Map();
20
+ for (const [logicalOperator, [first, second], result] of [
21
+ ['||', ['===', '<'], {operator: '<='}],
22
+ ['||', ['===', '>'], {operator: '>='}],
23
+ ['||', ['<', '>'], {operator: '!=='}],
24
+ ['&&', ['<=', '>='], {operator: '==='}],
25
+ ['&&', ['<=', '!=='], {operator: '<'}],
26
+ ['&&', ['>=', '!=='], {operator: '>'}],
27
+ ]) {
28
+ reductions.set(reductionKey(logicalOperator, first, second), result);
29
+ reductions.set(reductionKey(logicalOperator, second, first), result);
30
+ }
31
+
32
+ const isComparison = node =>
33
+ node.type === 'BinaryExpression'
34
+ && comparisonOperators.has(node.operator)
35
+ && !containsOptionalChain(node);
36
+
37
+ // Whether a comment falls within an operand, including its surrounding parentheses, since operands are reused with their parentheses.
38
+ const isCommentWithinOperand = (comment, operand, context) => {
39
+ const {sourceCode} = context;
40
+ const [commentStart, commentEnd] = sourceCode.getRange(comment);
41
+ const [operandStart, operandEnd] = getParenthesizedRange(operand, context);
42
+ return commentStart >= operandStart && commentEnd <= operandEnd;
43
+ };
44
+
45
+ /** @param {import('eslint').Rule.RuleContext} context */
46
+ const create = context => {
47
+ const {sourceCode} = context;
48
+
49
+ context.on('LogicalExpression', node => {
50
+ if (node.operator !== '||' && node.operator !== '&&') {
51
+ return;
52
+ }
53
+
54
+ const left = unwrapExpression(node.left);
55
+ const right = unwrapExpression(node.right);
56
+
57
+ if (!isComparison(left) || !isComparison(right)) {
58
+ return;
59
+ }
60
+
61
+ // The two comparisons must constrain the same pair of operands. The operands may appear in the same order or flipped; when flipped, normalize the right operator so the lookup and fix use the left comparison's operand order.
62
+ let rightOperator;
63
+ if (isSame(left.left, right.left) && isSame(left.right, right.right)) {
64
+ rightOperator = right.operator;
65
+ } else if (isSame(left.left, right.right) && isSame(left.right, right.left)) {
66
+ rightOperator = flipOperator[right.operator];
67
+ } else {
68
+ return;
69
+ }
70
+
71
+ const reduction = reductions.get(reductionKey(node.operator, left.operator, rightOperator));
72
+ if (!reduction) {
73
+ return;
74
+ }
75
+
76
+ const leftText = getParenthesizedText(left.left, context);
77
+ const rightText = getParenthesizedText(left.right, context);
78
+ const replacement = `${leftText} ${reduction.operator} ${rightText}`;
79
+
80
+ const problem = {
81
+ node,
82
+ messageId: MESSAGE_ID,
83
+ data: {replacement},
84
+ };
85
+
86
+ // Replacing the whole expression reuses only the left comparison's operands, so a comment outside both of them would be dropped.
87
+ const hasDroppedComment = sourceCode.getCommentsInside(node)
88
+ .some(comment => !isCommentWithinOperand(comment, left.left, context) && !isCommentWithinOperand(comment, left.right, context));
89
+ if (hasDroppedComment) {
90
+ return problem;
91
+ }
92
+
93
+ const fix = fixer => fixer.replaceText(node, replacement);
94
+ problem.suggest = [{messageId: MESSAGE_ID_SUGGESTION, data: {replacement}, fix}];
95
+
96
+ return problem;
97
+ });
98
+ };
99
+
100
+ /** @type {import('eslint').Rule.RuleModule} */
101
+ const config = {
102
+ create,
103
+ meta: {
104
+ type: 'suggestion',
105
+ docs: {
106
+ description: 'Disallow two comparisons of the same operands that can be combined into one.',
107
+ recommended: 'unopinionated',
108
+ },
109
+ hasSuggestions: true,
110
+ messages,
111
+ languages: [
112
+ 'js/js',
113
+ ],
114
+ },
115
+ };
116
+
117
+ export default config;
@@ -0,0 +1,143 @@
1
+ /**
2
+ @import {TSESTree as ESTree} from '@typescript-eslint/types';
3
+ @import * as ESLint from 'eslint';
4
+ */
5
+
6
+ const MESSAGE_ID = 'no-duplicate-if-branches';
7
+ const messages = {
8
+ [MESSAGE_ID]: 'This branch has the same body as the branch on line {{line}}.',
9
+ };
10
+
11
+ const isElseIfStatement = node =>
12
+ node.parent.type === 'IfStatement'
13
+ && node.parent.alternate === node;
14
+
15
+ const getBranchStatements = node => node.type === 'BlockStatement' ? node.body : [node];
16
+
17
+ const getStatementTokens = (node, sourceCode) => {
18
+ const tokens = sourceCode.getTokens(node);
19
+ const lastToken = tokens.at(-1);
20
+
21
+ if (lastToken?.type === 'Punctuator' && lastToken.value === ';') {
22
+ return tokens.slice(0, -1);
23
+ }
24
+
25
+ return tokens;
26
+ };
27
+
28
+ const areSameTokens = (left, right) =>
29
+ left.type === right.type
30
+ && left.value === right.value;
31
+
32
+ const areSameBranchBodies = (leftStatements, rightStatements, sourceCode) => {
33
+ if (leftStatements.length !== rightStatements.length) {
34
+ return false;
35
+ }
36
+
37
+ let hasTokens = false;
38
+
39
+ for (const [statementIndex, leftStatement] of leftStatements.entries()) {
40
+ const leftTokens = getStatementTokens(leftStatement, sourceCode);
41
+ const rightTokens = getStatementTokens(rightStatements[statementIndex], sourceCode);
42
+
43
+ if (leftTokens.length > 0) {
44
+ hasTokens = true;
45
+ }
46
+
47
+ if (leftTokens.length !== rightTokens.length) {
48
+ return false;
49
+ }
50
+
51
+ for (const [tokenIndex, leftToken] of leftTokens.entries()) {
52
+ if (!areSameTokens(leftToken, rightTokens[tokenIndex])) {
53
+ return false;
54
+ }
55
+ }
56
+ }
57
+
58
+ return hasTokens;
59
+ };
60
+
61
+ /**
62
+ @param {ESTree.IfStatement} ifStatement
63
+ @returns {Array<{body: ESTree.Statement, statements: ESTree.Statement[]}>}
64
+ */
65
+ function getBranches(ifStatement) {
66
+ const branches = [];
67
+ let node = ifStatement;
68
+
69
+ while (node) {
70
+ branches.push({
71
+ body: node.consequent,
72
+ statements: getBranchStatements(node.consequent),
73
+ });
74
+
75
+ if (node.alternate?.type !== 'IfStatement') {
76
+ if (node.alternate) {
77
+ branches.push({
78
+ body: node.alternate,
79
+ statements: getBranchStatements(node.alternate),
80
+ });
81
+ }
82
+
83
+ break;
84
+ }
85
+
86
+ node = node.alternate;
87
+ }
88
+
89
+ return branches;
90
+ }
91
+
92
+ /**
93
+ @param {ESTree.IfStatement} ifStatement
94
+ @param {ESLint.Rule.RuleContext} context
95
+ @returns {Generator<ESLint.Rule.ReportDescriptor>}
96
+ */
97
+ function * getProblems(ifStatement, context) {
98
+ if (isElseIfStatement(ifStatement)) {
99
+ return;
100
+ }
101
+
102
+ const {sourceCode} = context;
103
+ const branches = getBranches(ifStatement);
104
+
105
+ for (let index = 1; index < branches.length; index++) {
106
+ const branch = branches[index];
107
+ const previousBranch = branches[index - 1];
108
+
109
+ if (areSameBranchBodies(branch.statements, previousBranch.statements, sourceCode)) {
110
+ yield {
111
+ node: branch.body,
112
+ messageId: MESSAGE_ID,
113
+ data: {
114
+ line: String(sourceCode.getLoc(previousBranch.body).start.line),
115
+ },
116
+ };
117
+ }
118
+ }
119
+ }
120
+
121
+ /** @param {ESLint.Rule.RuleContext} context */
122
+ const create = context => {
123
+ context.on('IfStatement', ifStatement => getProblems(ifStatement, context));
124
+ };
125
+
126
+ /** @type {ESLint.Rule.RuleModule} */
127
+ const config = {
128
+ create,
129
+ meta: {
130
+ type: 'problem',
131
+ docs: {
132
+ description: 'Disallow duplicate adjacent branches in if chains.',
133
+ recommended: true,
134
+ },
135
+ schema: [],
136
+ messages,
137
+ languages: [
138
+ 'js/js',
139
+ ],
140
+ },
141
+ };
142
+
143
+ export default config;
@@ -0,0 +1,151 @@
1
+ import {getParenthesizedRange, getParenthesizedText} from './utils/index.js';
2
+ import {
3
+ containsOptionalChain,
4
+ isSame,
5
+ unwrapExpression,
6
+ } from './utils/comparison.js';
7
+
8
+ /**
9
+ @import * as ESLint from 'eslint';
10
+ */
11
+
12
+ const MESSAGE_ID = 'no-duplicate-logical-operands';
13
+ const MESSAGE_ID_SUGGESTION = 'no-duplicate-logical-operands/suggestion';
14
+ const messages = {
15
+ [MESSAGE_ID]: 'This operand duplicates the left operand.',
16
+ [MESSAGE_ID_SUGGESTION]: 'Remove the duplicate operand.',
17
+ };
18
+
19
+ const simpleComputedPropertyTypes = new Set(['Identifier', 'ThisExpression', 'Literal']);
20
+ const safelyAutofixableReferenceTypes = new Set(['Identifier', 'ThisExpression']);
21
+
22
+ const isSimpleComputedProperty = node => {
23
+ node = unwrapExpression(node);
24
+
25
+ return simpleComputedPropertyTypes.has(node.type);
26
+ };
27
+
28
+ const isSimpleReference = node => {
29
+ node = unwrapExpression(node);
30
+
31
+ if (containsOptionalChain(node)) {
32
+ return false;
33
+ }
34
+
35
+ switch (node.type) {
36
+ case 'Identifier':
37
+ case 'Super':
38
+ case 'ThisExpression': {
39
+ return true;
40
+ }
41
+
42
+ case 'MemberExpression': {
43
+ return isSimpleReference(node.object)
44
+ && (!node.computed || isSimpleComputedProperty(node.property));
45
+ }
46
+
47
+ default: {
48
+ return false;
49
+ }
50
+ }
51
+ };
52
+
53
+ const isSafelyAutofixableReference = node => safelyAutofixableReferenceTypes.has(node.type);
54
+
55
+ const isInsideWithStatement = node => {
56
+ for (let current = node.parent; current; current = current.parent) {
57
+ if (current.type === 'WithStatement') {
58
+ return true;
59
+ }
60
+ }
61
+
62
+ return false;
63
+ };
64
+
65
+ const isCommentInsideOperand = (comment, operand, context) => {
66
+ const [commentStart, commentEnd] = context.sourceCode.getRange(comment);
67
+ const [operandStart, operandEnd] = getParenthesizedRange(operand, context);
68
+ return commentStart >= operandStart && commentEnd <= operandEnd;
69
+ };
70
+
71
+ const canReplaceWithoutDroppingComments = (node, operand, context) =>
72
+ context.sourceCode.getCommentsInside(node).every(comment => isCommentInsideOperand(comment, operand, context));
73
+
74
+ const getAdjacentLeftOperand = node => (
75
+ node.left.type === 'LogicalExpression' && node.left.operator === node.operator
76
+ ? node.left.right
77
+ : node.left
78
+ );
79
+
80
+ /** @param {ESLint.Rule.RuleContext} context */
81
+ const create = context => {
82
+ context.on('LogicalExpression', node => {
83
+ if (node.operator !== '&&' && node.operator !== '||') {
84
+ return;
85
+ }
86
+
87
+ const adjacentLeftOperand = getAdjacentLeftOperand(node);
88
+
89
+ if (
90
+ isInsideWithStatement(node)
91
+ || !isSimpleReference(adjacentLeftOperand)
92
+ || !isSimpleReference(node.right)
93
+ || !isSame(adjacentLeftOperand, node.right)
94
+ ) {
95
+ return;
96
+ }
97
+
98
+ const problem = {
99
+ node: node.right,
100
+ messageId: MESSAGE_ID,
101
+ };
102
+
103
+ if (!canReplaceWithoutDroppingComments(node, node.left, context)) {
104
+ return problem;
105
+ }
106
+
107
+ const replacement = getParenthesizedText(node.left, context);
108
+ const fix = fixer => fixer.replaceText(node, replacement);
109
+
110
+ if (
111
+ isSafelyAutofixableReference(adjacentLeftOperand)
112
+ && isSafelyAutofixableReference(node.right)
113
+ ) {
114
+ return {
115
+ ...problem,
116
+ fix,
117
+ };
118
+ }
119
+
120
+ return {
121
+ ...problem,
122
+ suggest: [
123
+ {
124
+ messageId: MESSAGE_ID_SUGGESTION,
125
+ fix,
126
+ },
127
+ ],
128
+ };
129
+ });
130
+ };
131
+
132
+ /** @type {ESLint.Rule.RuleModule} */
133
+ const config = {
134
+ create,
135
+ meta: {
136
+ type: 'problem',
137
+ docs: {
138
+ description: 'Disallow adjacent duplicate operands in logical expressions.',
139
+ recommended: 'unopinionated',
140
+ },
141
+ fixable: 'code',
142
+ hasSuggestions: true,
143
+ schema: [],
144
+ messages,
145
+ languages: [
146
+ 'js/js',
147
+ ],
148
+ },
149
+ };
150
+
151
+ export default config;
@@ -189,11 +189,7 @@ function getFixFunction(callExpression, functionInfo, context) {
189
189
  return false;
190
190
  }
191
191
 
192
- if (callback.body.type !== 'BlockStatement') {
193
- return false;
194
- }
195
-
196
- return true;
192
+ return callback.body.type === 'BlockStatement';
197
193
  };
198
194
 
199
195
  function * removeCallbackParentheses(fixer) {
@@ -375,11 +371,7 @@ function isFixable(callExpression, {scope, functionInfo, allIdentifiers, sourceC
375
371
  return false;
376
372
  }
377
373
 
378
- if (isFunctionSelfUsedInside(callback, callbackScope)) {
379
- return false;
380
- }
381
-
382
- return true;
374
+ return !isFunctionSelfUsedInside(callback, callbackScope);
383
375
  }
384
376
 
385
377
  const ignoredObjects = [
@@ -443,13 +435,20 @@ const create = context => {
443
435
  });
444
436
 
445
437
  context.on('CallExpression', node => {
446
- const scope = sourceCode.getScope(node);
438
+ // Check the cheap method-call shape before resolving scope, so the scope lookup
439
+ // runs only for actual `.forEach()` calls.
447
440
  if (
448
441
  !isMethodCall(node, {
449
442
  method: 'forEach',
450
443
  })
451
444
  || isNodeMatches(node.callee.object, ignoredObjects)
452
- || isStrictCallbagBasicsNamespace(node.callee.object, scope)
445
+ ) {
446
+ return;
447
+ }
448
+
449
+ const scope = sourceCode.getScope(node);
450
+ if (
451
+ isStrictCallbagBasicsNamespace(node.callee.object, scope)
453
452
  || shouldSkipKnownNonArrayReceiver(node.callee.object, context)
454
453
  ) {
455
454
  return;
@@ -594,11 +594,7 @@ const isOnlyArrayOfIndexVariableRead = (arrayReferences, arrayVariable, indexVar
594
594
  return false;
595
595
  }
596
596
 
597
- if (isMemberExpressionChanged(node)) {
598
- return false;
599
- }
600
-
601
- return true;
597
+ return !isMemberExpressionChanged(node);
602
598
  });
603
599
 
604
600
  const getRemovalRange = (node, sourceCode) => {
@@ -728,11 +724,7 @@ const isIndexVariableUsedElsewhereInTheLoopBody = (indexVariable, bodyScope, arr
728
724
  return true;
729
725
  }
730
726
 
731
- if (node.object.name !== arrayIdentifierName) {
732
- return true;
733
- }
734
-
735
- return false;
727
+ return node.object.name !== arrayIdentifierName;
736
728
  });
737
729
 
738
730
  return referencesOtherThanArrayAccess.length > 0;
@@ -854,11 +846,7 @@ const create = context => {
854
846
  const elementReference = arrayReferences.find(reference => {
855
847
  const node = reference.identifier.parent;
856
848
 
857
- if (node.parent.type !== 'VariableDeclarator') {
858
- return false;
859
- }
860
-
861
- return true;
849
+ return node.parent.type === 'VariableDeclarator';
862
850
  });
863
851
  const elementNode = elementReference?.identifier.parent.parent;
864
852
  const elementIdentifierName = elementNode?.id.name;
@@ -0,0 +1,166 @@
1
+ import {getStaticValue} from '@eslint-community/eslint-utils';
2
+ import {
3
+ hasOptionalChainElement,
4
+ hasSameObjectShapePropertyCheck,
5
+ isKnownNonCollectionLengthOrSize,
6
+ isLengthOrSizeMemberExpression,
7
+ unwrapTypeScriptExpression,
8
+ } from './utils/index.js';
9
+
10
+ const MESSAGE_ID = 'no-impossible-length-comparison';
11
+ const messages = {
12
+ [MESSAGE_ID]: 'This comparison is always {{result}} because `.{{property}}` is always a non-negative integer.',
13
+ };
14
+
15
+ const flipOperator = {
16
+ '<': '>',
17
+ '<=': '>=',
18
+ '>': '<',
19
+ '>=': '<=',
20
+ '===': '===',
21
+ '!==': '!==',
22
+ '==': '==',
23
+ '!=': '!=',
24
+ };
25
+
26
+ function getComparisonSubject(node, context) {
27
+ const left = unwrapTypeScriptExpression(node.left);
28
+ const right = unwrapTypeScriptExpression(node.right);
29
+
30
+ if (isLengthOrSizeMemberExpression(left)) {
31
+ return {
32
+ memberExpression: left,
33
+ operator: node.operator,
34
+ value: getStaticValue(right, context.sourceCode.getScope(right))?.value,
35
+ };
36
+ }
37
+
38
+ if (isLengthOrSizeMemberExpression(right)) {
39
+ return {
40
+ memberExpression: right,
41
+ operator: flipOperator[node.operator],
42
+ value: getStaticValue(left, context.sourceCode.getScope(left))?.value,
43
+ };
44
+ }
45
+ }
46
+
47
+ function getConstantResult({operator, value}) {
48
+ if (
49
+ typeof value !== 'number'
50
+ || !Number.isFinite(value)
51
+ ) {
52
+ return;
53
+ }
54
+
55
+ switch (operator) {
56
+ case '<': {
57
+ return value <= 0 ? false : undefined;
58
+ }
59
+
60
+ case '<=': {
61
+ return value < 0 ? false : undefined;
62
+ }
63
+
64
+ case '>': {
65
+ return value < 0 ? true : undefined;
66
+ }
67
+
68
+ case '>=': {
69
+ return value <= 0 ? true : undefined;
70
+ }
71
+
72
+ case '===':
73
+ case '==': {
74
+ return value < 0 ? false : undefined;
75
+ }
76
+
77
+ case '!==':
78
+ case '!=': {
79
+ return value < 0 ? true : undefined;
80
+ }
81
+
82
+ default:
83
+ }
84
+ }
85
+
86
+ function hasOptionalChain(node) {
87
+ node = unwrapTypeScriptExpression(node);
88
+ if (node.type === 'ChainExpression' || hasOptionalChainElement(node)) {
89
+ return true;
90
+ }
91
+
92
+ if (node.type === 'MemberExpression') {
93
+ return hasOptionalChain(node.object);
94
+ }
95
+
96
+ if (node.type === 'CallExpression') {
97
+ return hasOptionalChain(node.callee);
98
+ }
99
+
100
+ return false;
101
+ }
102
+
103
+ function isOptionalChainReceiver(memberExpression) {
104
+ return hasOptionalChain(memberExpression.object);
105
+ }
106
+
107
+ function isCustomClassReceiver(memberExpression) {
108
+ const receiver = unwrapTypeScriptExpression(memberExpression.object);
109
+ return receiver.type === 'ThisExpression' || receiver.type === 'Super';
110
+ }
111
+
112
+ /** @param {import('eslint').Rule.RuleContext} context */
113
+ const create = context => {
114
+ context.on('BinaryExpression', node => {
115
+ if (!Object.hasOwn(flipOperator, node.operator)) {
116
+ return;
117
+ }
118
+
119
+ const comparison = getComparisonSubject(node, context);
120
+ if (!comparison) {
121
+ return;
122
+ }
123
+
124
+ const {memberExpression} = comparison;
125
+ if (
126
+ isCustomClassReceiver(memberExpression)
127
+ || isOptionalChainReceiver(memberExpression)
128
+ || isKnownNonCollectionLengthOrSize(memberExpression, context)
129
+ || hasSameObjectShapePropertyCheck({node, lengthOrSizeNode: memberExpression})
130
+ ) {
131
+ return;
132
+ }
133
+
134
+ const result = getConstantResult(comparison);
135
+ if (result === undefined) {
136
+ return;
137
+ }
138
+
139
+ return {
140
+ node,
141
+ messageId: MESSAGE_ID,
142
+ data: {
143
+ property: memberExpression.property.name,
144
+ result: String(result),
145
+ },
146
+ };
147
+ });
148
+ };
149
+
150
+ /** @type {import('eslint').Rule.RuleModule} */
151
+ const config = {
152
+ create,
153
+ meta: {
154
+ type: 'problem',
155
+ docs: {
156
+ description: 'Disallow impossible comparisons against `.length` or `.size`.',
157
+ recommended: 'unopinionated',
158
+ },
159
+ messages,
160
+ languages: [
161
+ 'js/js',
162
+ ],
163
+ },
164
+ };
165
+
166
+ export default config;
@@ -144,16 +144,20 @@ const getQuerySelectorAllCallForLengthCheck = (node, sourceCode) => {
144
144
 
145
145
  const getLengthCheckProblem = (node, context) => {
146
146
  const {sourceCode} = context;
147
- const querySelectorAllCall = getQuerySelectorAllCallForLengthCheck(node, sourceCode);
148
- if (!querySelectorAllCall) {
149
- return;
150
- }
151
147
 
148
+ // Cheap structural checks first, so the expensive scope resolution in
149
+ // `getQuerySelectorAllCallForLengthCheck` runs only for identifiers that are
150
+ // actually used as a control-flow test.
152
151
  const {node: booleanAncestor, isNegative} = getBooleanAncestor(node, context);
153
152
  if (!isControlFlowTest(booleanAncestor)) {
154
153
  return;
155
154
  }
156
155
 
156
+ const querySelectorAllCall = getQuerySelectorAllCallForLengthCheck(node, sourceCode);
157
+ if (!querySelectorAllCall) {
158
+ return;
159
+ }
160
+
157
161
  const text = sourceCode.getText(node);
158
162
 
159
163
  return {