eslint-plugin-unicorn 67.0.0 → 68.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 (157) hide show
  1. package/index.js +4 -0
  2. package/package.json +6 -3
  3. package/readme.md +153 -16
  4. package/rules/ast/is-empty-node.js +1 -5
  5. package/rules/better-dom-traversing.js +17 -8
  6. package/rules/class-reference-in-static-methods.js +14 -2
  7. package/rules/consistent-assert.js +1 -1
  8. package/rules/consistent-boolean-name.js +15 -3
  9. package/rules/consistent-compound-words.js +7 -5
  10. package/rules/consistent-conditional-object-spread.js +354 -0
  11. package/rules/consistent-destructuring.js +5 -0
  12. package/rules/consistent-existence-index-check.js +1 -1
  13. package/rules/consistent-json-file-read.js +4 -16
  14. package/rules/consistent-optional-chaining.js +3 -20
  15. package/rules/default-export-style.js +569 -0
  16. package/rules/expiring-todo-comments.js +1 -1
  17. package/rules/explicit-length-check.js +7 -32
  18. package/rules/import-style.js +8 -6
  19. package/rules/index.js +38 -1
  20. package/rules/logical-assignment-operators.js +17 -4
  21. package/rules/{prevent-abbreviations.js → name-replacements.js} +11 -9
  22. package/rules/new-for-builtins.js +39 -5
  23. package/rules/no-accessor-recursion.js +7 -0
  24. package/rules/no-accidental-bitwise-operator.js +97 -0
  25. package/rules/no-array-callback-reference.js +1 -7
  26. package/rules/no-array-concat-in-loop.js +45 -0
  27. package/rules/no-array-front-mutation.js +64 -0
  28. package/rules/no-array-sort-for-min-max.js +225 -0
  29. package/rules/no-array-splice.js +78 -64
  30. package/rules/no-boolean-sort-comparator.js +215 -0
  31. package/rules/no-break-in-nested-loop.js +33 -0
  32. package/rules/no-chained-comparison.js +116 -0
  33. package/rules/no-collection-bracket-access.js +165 -0
  34. package/rules/no-confusing-array-splice.js +2 -21
  35. package/rules/no-confusing-array-with.js +2 -20
  36. package/rules/no-constant-zero-expression.js +91 -0
  37. package/rules/no-declarations-before-early-exit.js +29 -36
  38. package/rules/no-double-comparison.js +117 -0
  39. package/rules/no-duplicate-if-branches.js +143 -0
  40. package/rules/no-duplicate-logical-operands.js +151 -0
  41. package/rules/no-for-each.js +11 -12
  42. package/rules/no-for-loop.js +3 -15
  43. package/rules/no-impossible-length-comparison.js +166 -0
  44. package/rules/no-incorrect-query-selector.js +8 -4
  45. package/rules/no-incorrect-template-string-interpolation.js +22 -11
  46. package/rules/no-instanceof-builtins.js +14 -1
  47. package/rules/no-invalid-argument-count.js +110 -54
  48. package/rules/no-invalid-character-comparison.js +121 -0
  49. package/rules/no-invalid-fetch-options.js +3 -1
  50. package/rules/no-keyword-prefix.js +14 -4
  51. package/rules/no-late-current-target-access.js +7 -39
  52. package/rules/no-loop-iterable-mutation.js +541 -0
  53. package/rules/no-manually-wrapped-comments.js +10 -2
  54. package/rules/no-misrefactored-assignment.js +102 -0
  55. package/rules/no-named-default.js +35 -2
  56. package/rules/no-nonstandard-builtin-properties.js +1381 -0
  57. package/rules/no-process-exit.js +16 -12
  58. package/rules/no-redundant-comparison.js +5 -12
  59. package/rules/no-selector-as-dom-name.js +150 -0
  60. package/rules/no-thenable.js +27 -11
  61. package/rules/no-uncalled-method.js +2 -8
  62. package/rules/no-undeclared-class-members.js +8 -6
  63. package/rules/no-unnecessary-boolean-comparison.js +131 -0
  64. package/rules/no-unnecessary-splice.js +15 -20
  65. package/rules/no-unreadable-for-of-expression.js +2 -1
  66. package/rules/no-unsafe-property-key.js +10 -15
  67. package/rules/no-unused-properties.js +4 -3
  68. package/rules/no-useless-coercion.js +71 -15
  69. package/rules/no-useless-compound-assignment.js +110 -0
  70. package/rules/no-useless-delete-check.js +353 -0
  71. package/rules/no-useless-else.js +16 -65
  72. package/rules/no-useless-error-capture-stack-trace.js +1 -1
  73. package/rules/no-useless-fallback-in-spread.js +7 -118
  74. package/rules/no-useless-logical-operand.js +248 -0
  75. package/rules/no-useless-undefined.js +11 -28
  76. package/rules/no-xor-as-exponentiation.js +59 -0
  77. package/rules/prefer-array-find.js +95 -5
  78. package/rules/prefer-array-flat-map.js +9 -21
  79. package/rules/prefer-array-flat.js +1 -24
  80. package/rules/prefer-array-from-async.js +273 -0
  81. package/rules/prefer-array-iterable-methods.js +157 -0
  82. package/rules/prefer-array-some.js +56 -31
  83. package/rules/prefer-at.js +1 -24
  84. package/rules/prefer-await.js +29 -0
  85. package/rules/prefer-boolean-return.js +224 -0
  86. package/rules/prefer-continue.js +361 -0
  87. package/rules/prefer-default-parameters.js +10 -6
  88. package/rules/prefer-dispose.js +1 -5
  89. package/rules/prefer-early-return.js +160 -30
  90. package/rules/prefer-else-if.js +50 -53
  91. package/rules/prefer-event-target.js +3 -13
  92. package/rules/prefer-flat-math-min-max.js +103 -0
  93. package/rules/prefer-global-this.js +1 -1
  94. package/rules/prefer-hoisting-branch-code.js +422 -0
  95. package/rules/prefer-identifier-import-export-specifiers.js +6 -11
  96. package/rules/prefer-includes-over-repeated-comparisons.js +5 -1
  97. package/rules/prefer-includes.js +6 -0
  98. package/rules/prefer-logical-operator-over-ternary.js +12 -20
  99. package/rules/prefer-math-constants.js +103 -0
  100. package/rules/prefer-math-min-max.js +1 -5
  101. package/rules/prefer-minimal-ternary.js +26 -2
  102. package/rules/prefer-node-protocol.js +1 -0
  103. package/rules/prefer-number-properties.js +2 -5
  104. package/rules/prefer-object-from-entries.js +6 -1
  105. package/rules/prefer-object-iterable-methods.js +36 -11
  106. package/rules/prefer-optional-catch-binding.js +30 -24
  107. package/rules/prefer-private-class-fields.js +2 -8
  108. package/rules/prefer-promise-with-resolvers.js +248 -0
  109. package/rules/prefer-query-selector.js +79 -17
  110. package/rules/prefer-queue-microtask.js +7 -9
  111. package/rules/prefer-regexp-escape.js +263 -0
  112. package/rules/prefer-set-size.js +10 -2
  113. package/rules/prefer-short-arrow-method.js +3 -3
  114. package/rules/prefer-single-replace.js +218 -0
  115. package/rules/prefer-spread.js +14 -21
  116. package/rules/prefer-string-replace-all.js +5 -0
  117. package/rules/prefer-structured-clone.js +2 -2
  118. package/rules/prefer-switch.js +1 -0
  119. package/rules/prefer-ternary.js +4 -2
  120. package/rules/prefer-type-literal-last.js +22 -54
  121. package/rules/prefer-unary-minus.js +103 -0
  122. package/rules/prefer-url-can-parse.js +308 -0
  123. package/rules/prefer-while-loop-condition.js +252 -0
  124. package/rules/require-passive-events.js +11 -4
  125. package/rules/require-proxy-trap-boolean-return.js +40 -126
  126. package/rules/rule/to-eslint-listener.js +5 -3
  127. package/rules/shared/array-concat-in-loop.js +151 -0
  128. package/rules/shared/identifier-checks.js +4 -16
  129. package/rules/shared/{abbreviations.js → name-replacements.js} +57 -0
  130. package/rules/shared/no-array-mutate-rule.js +19 -2
  131. package/rules/shared/regexp-escape.js +81 -0
  132. package/rules/shared/splice-replacements.js +1 -18
  133. package/rules/string-content.js +11 -4
  134. package/rules/template-indent.js +2 -8
  135. package/rules/throw-new-error.js +7 -0
  136. package/rules/utils/block-scope.js +42 -0
  137. package/rules/utils/builtins.js +50 -1
  138. package/rules/utils/comments.js +16 -0
  139. package/rules/utils/comparison.js +17 -0
  140. package/rules/utils/contains-suspension-point.js +35 -0
  141. package/rules/utils/get-const-variable-initializer.js +31 -0
  142. package/rules/utils/global-reference-tracker.js +14 -1
  143. package/rules/utils/has-optional-chain-element.js +36 -4
  144. package/rules/utils/index.js +32 -2
  145. package/rules/utils/is-boolean.js +23 -0
  146. package/rules/utils/is-function-self-used-inside.js +1 -5
  147. package/rules/utils/is-weak-map.js +21 -0
  148. package/rules/utils/is-weak-set.js +21 -0
  149. package/rules/utils/length-or-size.js +53 -0
  150. package/rules/utils/member-expression.js +18 -0
  151. package/rules/utils/needs-semicolon.js +1 -5
  152. package/rules/utils/numeric.js +20 -0
  153. package/rules/utils/should-add-parentheses-to-logical-expression-child.js +8 -14
  154. package/rules/utils/should-add-parentheses-to-member-expression-object.js +1 -5
  155. package/rules/utils/should-add-parentheses-to-unary-expression.js +8 -2
  156. package/rules/utils/track-branch-exits.js +79 -0
  157. package/rules/utils/types.js +22 -0
@@ -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 {
@@ -14,17 +14,28 @@ const memberExpression = String.raw`${identifier}(?:\.${identifier})*`;
14
14
  const missingDollar = new RegExp(String.raw`(?<![$\\])(?<!\{)\{(?<expression>${memberExpression})\}(?!\})`, 'gv');
15
15
  const missingOpeningBrace = new RegExp(String.raw`(?<!\\)(?<!\{)\$(?<expression>${memberExpression})\}(?!\})`, 'gv');
16
16
 
17
- const getMissingDollarReplacements = raw => Array.from(raw.matchAll(missingDollar), match => ({
18
- index: match.index,
19
- incorrect: match[0],
20
- correct: `$${match[0]}`,
21
- }));
17
+ // `{foo}` at the start of an `import`/`export`/`const`/`let`/`var` line is a named specifier or destructuring pattern, not a missing-dollar mistake (common in code-generation templates). Requiring no `=` (or `;`/`{`/`}`) before it keeps object literals like `const x = {foo}` reported.
18
+ const isBindingDeclaration = (raw, index) => {
19
+ const lineStart = raw.lastIndexOf('\n', index - 1) + 1;
20
+ return /^\s*(?:import|export|const|let|var)\b[^;={}]*$/.test(raw.slice(lineStart, index));
21
+ };
22
+
23
+ const getMissingDollarReplacements = raw => raw.matchAll(missingDollar)
24
+ .filter(match => !isBindingDeclaration(raw, match.index))
25
+ .map(match => ({
26
+ index: match.index,
27
+ incorrect: match[0],
28
+ correct: `$${match[0]}`,
29
+ }))
30
+ .toArray();
22
31
 
23
- const getMissingOpeningBraceReplacements = raw => Array.from(raw.matchAll(missingOpeningBrace), match => ({
24
- index: match.index,
25
- incorrect: match[0],
26
- correct: `\${${match.groups.expression}}`,
27
- }));
32
+ const getMissingOpeningBraceReplacements = raw => raw.matchAll(missingOpeningBrace)
33
+ .map(match => ({
34
+ index: match.index,
35
+ incorrect: match[0],
36
+ correct: `\${${match.groups.expression}}`,
37
+ }))
38
+ .toArray();
28
39
 
29
40
  const getReplacements = raw => {
30
41
  const replacements = [
@@ -80,7 +91,7 @@ const config = {
80
91
  type: 'problem',
81
92
  docs: {
82
93
  description: 'Disallow incorrect template literal interpolation syntax.',
83
- recommended: 'unopinionated',
94
+ recommended: true,
84
95
  },
85
96
  hasSuggestions: true,
86
97
  messages,
@@ -2,6 +2,8 @@ import {
2
2
  checkVueTemplate,
3
3
  getParenthesizedRange,
4
4
  getTokenStore,
5
+ isParenthesized,
6
+ shouldAddParenthesesToUnaryExpressionArgument,
5
7
  } from './utils/index.js';
6
8
  import {replaceNodeOrTokenAndSpacesBefore, fixSpaceAroundKeyword} from './fix/index.js';
7
9
  import builtinErrors from './shared/builtin-errors.js';
@@ -86,7 +88,18 @@ const replaceWithTypeOfExpression = (node, context) => function * (fixer) {
86
88
  yield fixSpaceAroundKeyword(fixer, node, context);
87
89
 
88
90
  const leftRange = getParenthesizedRange(left, {sourceCode: tokenStore});
89
- yield fixer.insertTextBeforeRange(leftRange, 'typeof ');
91
+
92
+ // Wrap a low-precedence left operand so `typeof` applies to the whole expression,
93
+ // e.g. `a + b instanceof Function` -> `typeof (a + b) === 'function'`.
94
+ if (
95
+ !isParenthesized(left, context)
96
+ && shouldAddParenthesesToUnaryExpressionArgument(left, 'typeof')
97
+ ) {
98
+ yield fixer.insertTextBeforeRange(leftRange, 'typeof (');
99
+ yield fixer.insertTextAfterRange(leftRange, ')');
100
+ } else {
101
+ yield fixer.insertTextBeforeRange(leftRange, 'typeof ');
102
+ }
90
103
 
91
104
  yield fixer.replaceText(instanceofToken, '===');
92
105