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.
- package/index.js +4 -0
- package/package.json +8 -3
- package/readme.md +165 -16
- package/rules/ast/is-empty-node.js +1 -5
- package/rules/ast/is-empty-object-expression.js +2 -2
- package/rules/better-dom-traversing.js +17 -8
- package/rules/class-reference-in-static-methods.js +14 -2
- package/rules/comment-content.js +136 -18
- package/rules/consistent-assert.js +1 -1
- package/rules/consistent-boolean-name.js +44 -5
- package/rules/consistent-compound-words.js +7 -5
- package/rules/consistent-conditional-object-spread.js +360 -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/consistent-tuple-labels.js +49 -0
- package/rules/custom-error-definition.js +26 -7
- 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 +50 -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-computed-property-existence-check.js +6 -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 +106 -43
- package/rules/no-instanceof-builtins.js +73 -14
- 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-invalid-well-known-symbol-methods.js +205 -0
- package/rules/no-keyword-prefix.js +14 -4
- package/rules/no-late-current-target-access.js +13 -107
- package/rules/no-late-event-control.js +127 -0
- 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 +1383 -0
- package/rules/no-object-methods-with-collections.js +4 -289
- package/rules/no-process-exit.js +16 -12
- package/rules/no-redundant-comparison.js +5 -12
- package/rules/no-return-array-push.js +20 -5
- package/rules/no-selector-as-dom-name.js +150 -0
- package/rules/no-thenable.js +27 -11
- package/rules/no-top-level-side-effects.js +8 -1
- 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-global-this.js +36 -1
- package/rules/no-unnecessary-splice.js +15 -20
- package/rules/no-unreadable-for-of-expression.js +2 -1
- package/rules/no-unreadable-object-destructuring.js +15 -0
- package/rules/no-unsafe-property-key.js +10 -15
- package/rules/no-unsafe-string-replacement.js +31 -0
- package/rules/no-unused-array-method-return.js +11 -2
- package/rules/no-unused-properties.js +4 -3
- package/rules/no-useless-boolean-cast.js +58 -1
- package/rules/no-useless-coercion.js +71 -15
- package/rules/no-useless-compound-assignment.js +110 -0
- package/rules/no-useless-concat.js +27 -3
- package/rules/no-useless-delete-check.js +353 -0
- package/rules/no-useless-else.js +27 -62
- 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-iterator-to-array.js +165 -115
- 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-abort-signal-timeout.js +395 -0
- package/rules/prefer-aggregate-error.js +478 -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 +6 -30
- package/rules/prefer-await.js +29 -0
- package/rules/prefer-boolean-return.js +224 -0
- package/rules/prefer-code-point.js +58 -8
- package/rules/prefer-continue.js +377 -0
- package/rules/prefer-default-parameters.js +10 -6
- package/rules/prefer-dispose.js +1 -5
- package/rules/prefer-dom-node-replace-children.js +353 -0
- package/rules/prefer-early-return.js +160 -30
- package/rules/prefer-else-if.js +50 -53
- package/rules/prefer-error-is-error.js +181 -0
- 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-has-check.js +1 -4
- package/rules/prefer-hoisting-branch-code.js +424 -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-iterator-concat.js +21 -1
- 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 +41 -13
- package/rules/prefer-modern-dom-apis.js +1 -45
- 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-observer-apis.js +498 -0
- package/rules/prefer-optional-catch-binding.js +30 -24
- package/rules/prefer-private-class-fields.js +2 -8
- package/rules/prefer-promise-try.js +238 -0
- 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-methods.js +337 -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-toggle-attribute.js +293 -0
- 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-url-search-parameters.js +411 -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/late-event-handler.js +82 -0
- package/rules/shared/{abbreviations.js → name-replacements.js} +63 -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/builtin-collection-type.js +267 -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 +38 -2
- package/rules/utils/is-boolean.js +26 -1
- package/rules/utils/is-event.js +127 -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/should-report-replace-children-receiver.js +210 -0
- package/rules/utils/track-branch-exits.js +79 -0
- package/rules/utils/type-helpers.js +166 -8
- 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;
|
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 {
|