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.
- package/index.js +4 -0
- package/package.json +6 -3
- package/readme.md +153 -16
- package/rules/ast/is-empty-node.js +1 -5
- package/rules/better-dom-traversing.js +17 -8
- package/rules/class-reference-in-static-methods.js +14 -2
- package/rules/consistent-assert.js +1 -1
- package/rules/consistent-boolean-name.js +15 -3
- package/rules/consistent-compound-words.js +7 -5
- package/rules/consistent-conditional-object-spread.js +354 -0
- package/rules/consistent-destructuring.js +5 -0
- package/rules/consistent-existence-index-check.js +1 -1
- package/rules/consistent-json-file-read.js +4 -16
- package/rules/consistent-optional-chaining.js +3 -20
- package/rules/default-export-style.js +569 -0
- package/rules/expiring-todo-comments.js +1 -1
- package/rules/explicit-length-check.js +7 -32
- package/rules/import-style.js +8 -6
- package/rules/index.js +38 -1
- package/rules/logical-assignment-operators.js +17 -4
- package/rules/{prevent-abbreviations.js → name-replacements.js} +11 -9
- package/rules/new-for-builtins.js +39 -5
- package/rules/no-accessor-recursion.js +7 -0
- package/rules/no-accidental-bitwise-operator.js +97 -0
- package/rules/no-array-callback-reference.js +1 -7
- package/rules/no-array-concat-in-loop.js +45 -0
- package/rules/no-array-front-mutation.js +64 -0
- package/rules/no-array-sort-for-min-max.js +225 -0
- package/rules/no-array-splice.js +78 -64
- package/rules/no-boolean-sort-comparator.js +215 -0
- package/rules/no-break-in-nested-loop.js +33 -0
- package/rules/no-chained-comparison.js +116 -0
- package/rules/no-collection-bracket-access.js +165 -0
- package/rules/no-confusing-array-splice.js +2 -21
- package/rules/no-confusing-array-with.js +2 -20
- package/rules/no-constant-zero-expression.js +91 -0
- package/rules/no-declarations-before-early-exit.js +29 -36
- package/rules/no-double-comparison.js +117 -0
- package/rules/no-duplicate-if-branches.js +143 -0
- package/rules/no-duplicate-logical-operands.js +151 -0
- package/rules/no-for-each.js +11 -12
- package/rules/no-for-loop.js +3 -15
- package/rules/no-impossible-length-comparison.js +166 -0
- package/rules/no-incorrect-query-selector.js +8 -4
- package/rules/no-incorrect-template-string-interpolation.js +22 -11
- package/rules/no-instanceof-builtins.js +14 -1
- package/rules/no-invalid-argument-count.js +110 -54
- package/rules/no-invalid-character-comparison.js +121 -0
- package/rules/no-invalid-fetch-options.js +3 -1
- package/rules/no-keyword-prefix.js +14 -4
- package/rules/no-late-current-target-access.js +7 -39
- package/rules/no-loop-iterable-mutation.js +541 -0
- package/rules/no-manually-wrapped-comments.js +10 -2
- package/rules/no-misrefactored-assignment.js +102 -0
- package/rules/no-named-default.js +35 -2
- package/rules/no-nonstandard-builtin-properties.js +1381 -0
- package/rules/no-process-exit.js +16 -12
- package/rules/no-redundant-comparison.js +5 -12
- package/rules/no-selector-as-dom-name.js +150 -0
- package/rules/no-thenable.js +27 -11
- package/rules/no-uncalled-method.js +2 -8
- package/rules/no-undeclared-class-members.js +8 -6
- package/rules/no-unnecessary-boolean-comparison.js +131 -0
- package/rules/no-unnecessary-splice.js +15 -20
- package/rules/no-unreadable-for-of-expression.js +2 -1
- package/rules/no-unsafe-property-key.js +10 -15
- package/rules/no-unused-properties.js +4 -3
- package/rules/no-useless-coercion.js +71 -15
- package/rules/no-useless-compound-assignment.js +110 -0
- package/rules/no-useless-delete-check.js +353 -0
- package/rules/no-useless-else.js +16 -65
- package/rules/no-useless-error-capture-stack-trace.js +1 -1
- package/rules/no-useless-fallback-in-spread.js +7 -118
- package/rules/no-useless-logical-operand.js +248 -0
- package/rules/no-useless-undefined.js +11 -28
- package/rules/no-xor-as-exponentiation.js +59 -0
- package/rules/prefer-array-find.js +95 -5
- package/rules/prefer-array-flat-map.js +9 -21
- package/rules/prefer-array-flat.js +1 -24
- package/rules/prefer-array-from-async.js +273 -0
- package/rules/prefer-array-iterable-methods.js +157 -0
- package/rules/prefer-array-some.js +56 -31
- package/rules/prefer-at.js +1 -24
- package/rules/prefer-await.js +29 -0
- package/rules/prefer-boolean-return.js +224 -0
- package/rules/prefer-continue.js +361 -0
- package/rules/prefer-default-parameters.js +10 -6
- package/rules/prefer-dispose.js +1 -5
- package/rules/prefer-early-return.js +160 -30
- package/rules/prefer-else-if.js +50 -53
- package/rules/prefer-event-target.js +3 -13
- package/rules/prefer-flat-math-min-max.js +103 -0
- package/rules/prefer-global-this.js +1 -1
- package/rules/prefer-hoisting-branch-code.js +422 -0
- package/rules/prefer-identifier-import-export-specifiers.js +6 -11
- package/rules/prefer-includes-over-repeated-comparisons.js +5 -1
- package/rules/prefer-includes.js +6 -0
- package/rules/prefer-logical-operator-over-ternary.js +12 -20
- package/rules/prefer-math-constants.js +103 -0
- package/rules/prefer-math-min-max.js +1 -5
- package/rules/prefer-minimal-ternary.js +26 -2
- package/rules/prefer-node-protocol.js +1 -0
- package/rules/prefer-number-properties.js +2 -5
- package/rules/prefer-object-from-entries.js +6 -1
- package/rules/prefer-object-iterable-methods.js +36 -11
- package/rules/prefer-optional-catch-binding.js +30 -24
- package/rules/prefer-private-class-fields.js +2 -8
- package/rules/prefer-promise-with-resolvers.js +248 -0
- package/rules/prefer-query-selector.js +79 -17
- package/rules/prefer-queue-microtask.js +7 -9
- package/rules/prefer-regexp-escape.js +263 -0
- package/rules/prefer-set-size.js +10 -2
- package/rules/prefer-short-arrow-method.js +3 -3
- package/rules/prefer-single-replace.js +218 -0
- package/rules/prefer-spread.js +14 -21
- package/rules/prefer-string-replace-all.js +5 -0
- package/rules/prefer-structured-clone.js +2 -2
- package/rules/prefer-switch.js +1 -0
- package/rules/prefer-ternary.js +4 -2
- package/rules/prefer-type-literal-last.js +22 -54
- package/rules/prefer-unary-minus.js +103 -0
- package/rules/prefer-url-can-parse.js +308 -0
- package/rules/prefer-while-loop-condition.js +252 -0
- package/rules/require-passive-events.js +11 -4
- package/rules/require-proxy-trap-boolean-return.js +40 -126
- package/rules/rule/to-eslint-listener.js +5 -3
- package/rules/shared/array-concat-in-loop.js +151 -0
- package/rules/shared/identifier-checks.js +4 -16
- package/rules/shared/{abbreviations.js → name-replacements.js} +57 -0
- package/rules/shared/no-array-mutate-rule.js +19 -2
- package/rules/shared/regexp-escape.js +81 -0
- package/rules/shared/splice-replacements.js +1 -18
- package/rules/string-content.js +11 -4
- package/rules/template-indent.js +2 -8
- package/rules/throw-new-error.js +7 -0
- package/rules/utils/block-scope.js +42 -0
- package/rules/utils/builtins.js +50 -1
- package/rules/utils/comments.js +16 -0
- package/rules/utils/comparison.js +17 -0
- package/rules/utils/contains-suspension-point.js +35 -0
- package/rules/utils/get-const-variable-initializer.js +31 -0
- package/rules/utils/global-reference-tracker.js +14 -1
- package/rules/utils/has-optional-chain-element.js +36 -4
- package/rules/utils/index.js +32 -2
- package/rules/utils/is-boolean.js +23 -0
- package/rules/utils/is-function-self-used-inside.js +1 -5
- package/rules/utils/is-weak-map.js +21 -0
- package/rules/utils/is-weak-set.js +21 -0
- package/rules/utils/length-or-size.js +53 -0
- package/rules/utils/member-expression.js +18 -0
- package/rules/utils/needs-semicolon.js +1 -5
- package/rules/utils/numeric.js +20 -0
- package/rules/utils/should-add-parentheses-to-logical-expression-child.js +8 -14
- package/rules/utils/should-add-parentheses-to-member-expression-object.js +1 -5
- package/rules/utils/should-add-parentheses-to-unary-expression.js +8 -2
- package/rules/utils/track-branch-exits.js +79 -0
- 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;
|
package/rules/no-for-each.js
CHANGED
|
@@ -189,11 +189,7 @@ function getFixFunction(callExpression, functionInfo, context) {
|
|
|
189
189
|
return false;
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/rules/no-for-loop.js
CHANGED
|
@@ -594,11 +594,7 @@ const isOnlyArrayOfIndexVariableRead = (arrayReferences, arrayVariable, indexVar
|
|
|
594
594
|
return false;
|
|
595
595
|
}
|
|
596
596
|
|
|
597
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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 =>
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|