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
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import {isFunction, isLoop, loopTypes} from './ast/index.js';
|
|
2
2
|
|
|
3
3
|
const MESSAGE_ID = 'no-break-in-nested-loop';
|
|
4
|
+
const SWITCH_CONTINUE_MESSAGE_ID = 'switch-continue';
|
|
4
5
|
const messages = {
|
|
5
6
|
[MESSAGE_ID]: 'Move this nested loop or switch into a function instead of using `{{keyword}}` here.',
|
|
7
|
+
[SWITCH_CONTINUE_MESSAGE_ID]: 'An unlabeled `continue` inside a `switch` continues the surrounding loop, not the next `case`. Use a labeled `continue` if that is intentional.',
|
|
6
8
|
};
|
|
7
9
|
|
|
8
10
|
const controlFlowNodeTypes = new Set([
|
|
@@ -56,6 +58,30 @@ function isNestedControlFlowStatement(node, sourceCode) {
|
|
|
56
58
|
return false;
|
|
57
59
|
}
|
|
58
60
|
|
|
61
|
+
function isContinueInSwitchInsideLoop(node, sourceCode) {
|
|
62
|
+
if (node.type !== 'ContinueStatement' || node.label) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let hasSwitch = false;
|
|
67
|
+
|
|
68
|
+
for (const ancestor of sourceCode.getAncestors(node).toReversed()) {
|
|
69
|
+
if (isFunction(ancestor)) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (isLoop(ancestor)) {
|
|
74
|
+
return hasSwitch;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (ancestor.type === 'SwitchStatement') {
|
|
78
|
+
hasSwitch = true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
59
85
|
/** @param {import('eslint').Rule.RuleContext} context */
|
|
60
86
|
const create = context => {
|
|
61
87
|
const {sourceCode} = context;
|
|
@@ -68,6 +94,13 @@ const create = context => {
|
|
|
68
94
|
return;
|
|
69
95
|
}
|
|
70
96
|
|
|
97
|
+
if (isContinueInSwitchInsideLoop(node, sourceCode)) {
|
|
98
|
+
return {
|
|
99
|
+
node,
|
|
100
|
+
messageId: SWITCH_CONTINUE_MESSAGE_ID,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
71
104
|
return {
|
|
72
105
|
node,
|
|
73
106
|
messageId: MESSAGE_ID,
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import {hasSideEffect} from '@eslint-community/eslint-utils';
|
|
2
|
+
import {getParenthesizedText, isParenthesized, isBoolean} from './utils/index.js';
|
|
3
|
+
|
|
4
|
+
const MESSAGE_ID_ERROR = 'no-chained-comparison/error';
|
|
5
|
+
const MESSAGE_ID_SUGGESTION = 'no-chained-comparison/suggestion';
|
|
6
|
+
const messages = {
|
|
7
|
+
[MESSAGE_ID_ERROR]: 'Comparison operators cannot be chained. The inner comparison evaluates to a boolean, which is then compared instead of the operands.',
|
|
8
|
+
[MESSAGE_ID_SUGGESTION]: 'Replace with `{{replacement}}`.',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const orderingOperators = new Set(['<', '>', '<=', '>=']);
|
|
12
|
+
const equalityOperators = new Set(['===', '!==', '==', '!=']);
|
|
13
|
+
const comparisonOperators = orderingOperators.union(equalityOperators);
|
|
14
|
+
|
|
15
|
+
const isComparison = node => node.type === 'BinaryExpression' && comparisonOperators.has(node.operator);
|
|
16
|
+
const isBooleanLiteral = node => node.type === 'Literal' && typeof node.value === 'boolean';
|
|
17
|
+
|
|
18
|
+
// The `&&` rewrite is a `LogicalExpression`, which binds looser than any comparison. It can only
|
|
19
|
+
// replace `node` where a surrounding operator won't recapture it: at the top of an expression or
|
|
20
|
+
// when already parenthesized, but not as an operand of another binary expression (`foo === a < b < c`)
|
|
21
|
+
// or mixed with `??` (which is a syntax error).
|
|
22
|
+
const isSafeForLogicalReplacement = (node, context) => {
|
|
23
|
+
if (isParenthesized(node, context)) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const {parent} = node;
|
|
28
|
+
return parent.type !== 'BinaryExpression'
|
|
29
|
+
&& !(parent.type === 'LogicalExpression' && parent.operator === '??');
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
33
|
+
const create = context => {
|
|
34
|
+
const {sourceCode} = context;
|
|
35
|
+
|
|
36
|
+
context.on('BinaryExpression', node => {
|
|
37
|
+
if (!comparisonOperators.has(node.operator)) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const nested = isComparison(node.left) ? node.left : node.right;
|
|
42
|
+
if (!isComparison(nested)) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const isNestedOnLeft = nested === node.left;
|
|
47
|
+
const sibling = isNestedOnLeft ? node.right : node.left;
|
|
48
|
+
|
|
49
|
+
// For equality operators, comparing a comparison result against another boolean value
|
|
50
|
+
// (`(a > 0) === (b > 0)`, `(a < b) === true`, `(a > 0) !== Boolean(b)`) is an intentional
|
|
51
|
+
// boolean comparison, not a chained comparison.
|
|
52
|
+
if (
|
|
53
|
+
equalityOperators.has(node.operator)
|
|
54
|
+
&& isBoolean(sibling, context)
|
|
55
|
+
) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const problem = {
|
|
60
|
+
node,
|
|
61
|
+
messageId: MESSAGE_ID_ERROR,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Only the canonical left-associative chain `(a OP1 b) OP2 c` has an unambiguous
|
|
65
|
+
// `a OP1 b && b OP2 c` rewrite. The `&&` rewrite only reflects a plausible intent for a
|
|
66
|
+
// real ordering or equality chain, so the inner and outer operators must be the same kind
|
|
67
|
+
// and the outer operand must not be a boolean literal (`(a < b) < true` is not a range
|
|
68
|
+
// check). The middle operand `b` is duplicated, so the rewrite must also be free of side
|
|
69
|
+
// effects and comments.
|
|
70
|
+
const isSameOperatorKind = orderingOperators.has(nested.operator) === orderingOperators.has(node.operator);
|
|
71
|
+
const isPlausibleChain = isSameOperatorKind && !isBooleanLiteral(node.right);
|
|
72
|
+
if (
|
|
73
|
+
isNestedOnLeft
|
|
74
|
+
&& isPlausibleChain
|
|
75
|
+
// Both outer operands must be plain. A comparison at either end (`(a < b) < (c < d)`
|
|
76
|
+
// or a deeper chain `a < b < c < d`) has no single unambiguous `&&` rewrite.
|
|
77
|
+
&& !isComparison(node.right)
|
|
78
|
+
&& !isComparison(nested.left)
|
|
79
|
+
&& isSafeForLogicalReplacement(node, context)
|
|
80
|
+
&& !hasSideEffect(nested.right, sourceCode)
|
|
81
|
+
&& sourceCode.getCommentsInside(node).length === 0
|
|
82
|
+
) {
|
|
83
|
+
const replacement = `${sourceCode.getText(node.left)} && ${getParenthesizedText(nested.right, context)} ${node.operator} ${getParenthesizedText(node.right, context)}`;
|
|
84
|
+
|
|
85
|
+
problem.suggest = [
|
|
86
|
+
{
|
|
87
|
+
messageId: MESSAGE_ID_SUGGESTION,
|
|
88
|
+
data: {replacement},
|
|
89
|
+
/** @param {import('eslint').Rule.RuleFixer} fixer */
|
|
90
|
+
fix: fixer => fixer.replaceText(node, replacement),
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return problem;
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
100
|
+
const config = {
|
|
101
|
+
create,
|
|
102
|
+
meta: {
|
|
103
|
+
type: 'problem',
|
|
104
|
+
docs: {
|
|
105
|
+
description: 'Disallow chained comparisons such as `a < b < c`.',
|
|
106
|
+
recommended: 'unopinionated',
|
|
107
|
+
},
|
|
108
|
+
hasSuggestions: true,
|
|
109
|
+
messages,
|
|
110
|
+
languages: [
|
|
111
|
+
'js/js',
|
|
112
|
+
],
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export default config;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import {getStaticValue} from '@eslint-community/eslint-utils';
|
|
2
|
+
import {
|
|
3
|
+
isMap,
|
|
4
|
+
isSet,
|
|
5
|
+
isWeakMap,
|
|
6
|
+
isWeakSet,
|
|
7
|
+
isLeftHandSide,
|
|
8
|
+
getParenthesizedText,
|
|
9
|
+
} from './utils/index.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
@import * as ESLint from 'eslint';
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const MESSAGE_ID_READ = 'no-collection-bracket-access/read';
|
|
16
|
+
const MESSAGE_ID_WRITE = 'no-collection-bracket-access/write';
|
|
17
|
+
const MESSAGE_ID_DELETE = 'no-collection-bracket-access/delete';
|
|
18
|
+
const MESSAGE_ID_SUGGESTION_SET = 'no-collection-bracket-access/suggestion-set';
|
|
19
|
+
const MESSAGE_ID_SUGGESTION_DELETE = 'no-collection-bracket-access/suggestion-delete';
|
|
20
|
+
const messages = {
|
|
21
|
+
[MESSAGE_ID_READ]: 'Do not read from a `{{type}}` using bracket notation.',
|
|
22
|
+
[MESSAGE_ID_WRITE]: 'Do not write to a `{{type}}` using bracket notation.',
|
|
23
|
+
[MESSAGE_ID_DELETE]: 'Do not delete from a `{{type}}` using bracket notation.',
|
|
24
|
+
[MESSAGE_ID_SUGGESTION_SET]: 'Switch to `{{type}}#set()`.',
|
|
25
|
+
[MESSAGE_ID_SUGGESTION_DELETE]: 'Switch to `{{type}}#delete()`.',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const collectionTypes = [
|
|
29
|
+
{
|
|
30
|
+
name: 'Map',
|
|
31
|
+
is: isMap,
|
|
32
|
+
prototype: Map.prototype,
|
|
33
|
+
hasSetMethod: true,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'WeakMap',
|
|
37
|
+
is: isWeakMap,
|
|
38
|
+
prototype: WeakMap.prototype,
|
|
39
|
+
hasSetMethod: true,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'Set',
|
|
43
|
+
is: isSet,
|
|
44
|
+
prototype: Set.prototype,
|
|
45
|
+
hasSetMethod: false,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'WeakSet',
|
|
49
|
+
is: isWeakSet,
|
|
50
|
+
prototype: WeakSet.prototype,
|
|
51
|
+
hasSetMethod: false,
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
// Only `Identifier`/`ThisExpression`/`MemberExpression` receivers can be placed before
|
|
56
|
+
// `.method(…)` without parentheses, so suggestions are limited to them.
|
|
57
|
+
const simpleObjectTypes = new Set(['Identifier', 'ThisExpression', 'MemberExpression']);
|
|
58
|
+
|
|
59
|
+
function getProblem(node, collection, context) {
|
|
60
|
+
const {sourceCode} = context;
|
|
61
|
+
const {name: type} = collection;
|
|
62
|
+
|
|
63
|
+
// An optional access (`map?.['foo']`) wraps the member in a `ChainExpression`.
|
|
64
|
+
const outerNode = node.parent.type === 'ChainExpression' ? node.parent : node;
|
|
65
|
+
const {parent} = outerNode;
|
|
66
|
+
const object = getParenthesizedText(node.object, context);
|
|
67
|
+
const key = getParenthesizedText(node.property, context);
|
|
68
|
+
|
|
69
|
+
let messageId = MESSAGE_ID_READ;
|
|
70
|
+
let suggestionMessageId;
|
|
71
|
+
let replacement;
|
|
72
|
+
if (
|
|
73
|
+
parent.type === 'UnaryExpression'
|
|
74
|
+
&& parent.operator === 'delete'
|
|
75
|
+
&& parent.argument === outerNode
|
|
76
|
+
) {
|
|
77
|
+
messageId = MESSAGE_ID_DELETE;
|
|
78
|
+
suggestionMessageId = MESSAGE_ID_SUGGESTION_DELETE;
|
|
79
|
+
replacement = `${object}.delete(${key})`;
|
|
80
|
+
} else if (
|
|
81
|
+
isLeftHandSide(node)
|
|
82
|
+
|| (
|
|
83
|
+
(parent.type === 'ForOfStatement' || parent.type === 'ForInStatement')
|
|
84
|
+
&& parent.left === outerNode
|
|
85
|
+
)
|
|
86
|
+
) {
|
|
87
|
+
messageId = MESSAGE_ID_WRITE;
|
|
88
|
+
if (
|
|
89
|
+
collection.hasSetMethod
|
|
90
|
+
&& parent.type === 'AssignmentExpression'
|
|
91
|
+
&& parent.operator === '='
|
|
92
|
+
&& parent.left === node
|
|
93
|
+
) {
|
|
94
|
+
suggestionMessageId = MESSAGE_ID_SUGGESTION_SET;
|
|
95
|
+
replacement = `${object}.set(${key}, ${getParenthesizedText(parent.right, context)})`;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const problem = {node, messageId, data: {type}};
|
|
100
|
+
|
|
101
|
+
// Only suggest for the simple, statement-level case, to avoid changing the value of the
|
|
102
|
+
// surrounding expression, dropping comments, or needing to parenthesize the receiver.
|
|
103
|
+
if (
|
|
104
|
+
suggestionMessageId
|
|
105
|
+
&& !node.optional
|
|
106
|
+
&& simpleObjectTypes.has(node.object.type)
|
|
107
|
+
&& parent.parent.type === 'ExpressionStatement'
|
|
108
|
+
&& sourceCode.getCommentsInside(parent).length === 0
|
|
109
|
+
) {
|
|
110
|
+
problem.suggest = [
|
|
111
|
+
{
|
|
112
|
+
messageId: suggestionMessageId,
|
|
113
|
+
data: {type},
|
|
114
|
+
fix: fixer => fixer.replaceText(parent, replacement),
|
|
115
|
+
},
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return problem;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** @param {ESLint.Rule.RuleContext} context */
|
|
123
|
+
const create = context => {
|
|
124
|
+
context.on('MemberExpression', node => {
|
|
125
|
+
if (!node.computed) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const collection = collectionTypes.find(({is}) => is(node.object, context));
|
|
130
|
+
if (!collection) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Allow accessing a real member, including `Symbol` ones like `map[Symbol.iterator]`.
|
|
135
|
+
const staticResult = getStaticValue(node.property, context.sourceCode.getScope(node.property));
|
|
136
|
+
if (
|
|
137
|
+
staticResult
|
|
138
|
+
&& (typeof staticResult.value === 'string' || typeof staticResult.value === 'symbol')
|
|
139
|
+
&& Reflect.has(collection.prototype, staticResult.value)
|
|
140
|
+
) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return getProblem(node, collection, context);
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/** @type {ESLint.Rule.RuleModule} */
|
|
149
|
+
const config = {
|
|
150
|
+
create,
|
|
151
|
+
meta: {
|
|
152
|
+
type: 'problem',
|
|
153
|
+
docs: {
|
|
154
|
+
description: 'Disallow accessing `Map`, `Set`, `WeakMap`, and `WeakSet` entries with bracket notation.',
|
|
155
|
+
recommended: 'unopinionated',
|
|
156
|
+
},
|
|
157
|
+
hasSuggestions: true,
|
|
158
|
+
messages,
|
|
159
|
+
languages: [
|
|
160
|
+
'js/js',
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
export default config;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getParenthesizedText,
|
|
3
3
|
hasOptionalChainElement,
|
|
4
|
+
isBoolean,
|
|
4
5
|
isBooleanExpression,
|
|
5
6
|
isControlFlowTest,
|
|
6
7
|
wouldRemoveComments,
|
|
@@ -120,6 +121,11 @@ const create = context => {
|
|
|
120
121
|
return;
|
|
121
122
|
}
|
|
122
123
|
|
|
124
|
+
// A property whose value type is a known boolean is a value read, not an existence check (and `no-unnecessary-boolean-comparison` may rewrite a comparison into this form).
|
|
125
|
+
if (isBoolean(node, context)) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
123
129
|
return {
|
|
124
130
|
node,
|
|
125
131
|
messageId: MESSAGE_ID,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {isMethodCall
|
|
2
|
-
import {getParenthesizedText} from './utils/index.js';
|
|
1
|
+
import {isMethodCall} from './ast/index.js';
|
|
2
|
+
import {getParenthesizedText, getStaticNumberValue} from './utils/index.js';
|
|
3
3
|
import {isSame, unwrapExpression} from './utils/comparison.js';
|
|
4
4
|
|
|
5
5
|
const REPLACE_ONE_ELEMENT = 'replace-one-element';
|
|
@@ -13,25 +13,6 @@ const messages = {
|
|
|
13
13
|
[SUGGESTION_INSERT_AT_NEGATIVE_ONE]: 'Resolve the insertion index explicitly.',
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
function getStaticNumberValue(node) {
|
|
17
|
-
node = unwrapExpression(node);
|
|
18
|
-
|
|
19
|
-
if (isNumericLiteral(node)) {
|
|
20
|
-
return node.value;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (
|
|
24
|
-
node.type === 'UnaryExpression'
|
|
25
|
-
&& (node.operator === '+' || node.operator === '-')
|
|
26
|
-
) {
|
|
27
|
-
const value = getStaticNumberValue(node.argument);
|
|
28
|
-
|
|
29
|
-
if (typeof value === 'number') {
|
|
30
|
-
return node.operator === '-' ? -value : value;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
16
|
const isNegativeStaticIndex = node => Math.trunc(getStaticNumberValue(node)) < 0;
|
|
36
17
|
|
|
37
18
|
function getNormalizedDeleteCountValue(node) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {isMethodCall
|
|
1
|
+
import {isMethodCall} from './ast/index.js';
|
|
2
|
+
import {getStaticNumberValue} from './utils/index.js';
|
|
2
3
|
import {isSame, unwrapExpression} from './utils/comparison.js';
|
|
3
4
|
|
|
4
5
|
const MESSAGE_ID_NEGATIVE_INDEX = 'negative-index';
|
|
@@ -13,25 +14,6 @@ const messages = {
|
|
|
13
14
|
@import * as ESLint from 'eslint';
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
|
-
function getStaticNumberValue(node) {
|
|
17
|
-
node = unwrapExpression(node);
|
|
18
|
-
|
|
19
|
-
if (isNumericLiteral(node)) {
|
|
20
|
-
return node.value;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (
|
|
24
|
-
node.type === 'UnaryExpression'
|
|
25
|
-
&& (node.operator === '+' || node.operator === '-')
|
|
26
|
-
) {
|
|
27
|
-
const value = getStaticNumberValue(node.argument);
|
|
28
|
-
|
|
29
|
-
if (typeof value === 'number') {
|
|
30
|
-
return node.operator === '-' ? -value : value;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
17
|
const isNegativeStaticIndex = node => Math.trunc(getStaticNumberValue(node)) < 0;
|
|
36
18
|
|
|
37
19
|
function isLengthMemberExpressionFor(node, object) {
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import {getStaticValue} from '@eslint-community/eslint-utils';
|
|
2
|
+
import {isLiteral} from './ast/index.js';
|
|
3
|
+
|
|
4
|
+
const MESSAGE_ID_ERROR = 'no-constant-zero-expression/error';
|
|
5
|
+
const MESSAGE_ID_SUGGESTION = 'no-constant-zero-expression/suggestion';
|
|
6
|
+
const messages = {
|
|
7
|
+
[MESSAGE_ID_ERROR]: 'This operation always evaluates to `0` (or `-0`/`NaN`), which is likely a mistake.',
|
|
8
|
+
[MESSAGE_ID_SUGGESTION]: 'Replace this expression with `0`.',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const isZero = node => isLiteral(node, 0);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
Check if a binary expression has a `0` operand that dominates the result.
|
|
15
|
+
|
|
16
|
+
`x * 0` / `0 * x` and `x & 0` / `0 & x` are dominated by a `0` on either side. `0 / x` is dominated only when the `0` is on the left, since `x / 0` is `Infinity`/`NaN`.
|
|
17
|
+
|
|
18
|
+
@param {import('estree').BinaryExpression} node
|
|
19
|
+
@returns {boolean}
|
|
20
|
+
*/
|
|
21
|
+
const isErasedToZero = node => {
|
|
22
|
+
switch (node.operator) {
|
|
23
|
+
case '*':
|
|
24
|
+
case '&': {
|
|
25
|
+
return isZero(node.left) || isZero(node.right);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
case '/': {
|
|
29
|
+
return isZero(node.left);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
default: {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
39
|
+
const create = context => {
|
|
40
|
+
const {sourceCode} = context;
|
|
41
|
+
|
|
42
|
+
context.on('BinaryExpression', node => {
|
|
43
|
+
if (!isErasedToZero(node)) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const problem = {
|
|
48
|
+
node,
|
|
49
|
+
messageId: MESSAGE_ID_ERROR,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Only suggest replacing with `0` when the whole expression provably folds to exactly `+0`.
|
|
53
|
+
// `getStaticValue` resolves only side-effect-free constants, and `Object.is(…, 0)` excludes
|
|
54
|
+
// `-0` (e.g. `-5 * 0`, `0 / -5`) and `NaN` (e.g. `0 / 0`), so those are reported without a suggestion.
|
|
55
|
+
const staticValue = getStaticValue(node, sourceCode.getScope(node));
|
|
56
|
+
if (
|
|
57
|
+
staticValue
|
|
58
|
+
&& Object.is(staticValue.value, 0)
|
|
59
|
+
&& sourceCode.getCommentsInside(node).length === 0
|
|
60
|
+
) {
|
|
61
|
+
problem.suggest = [
|
|
62
|
+
{
|
|
63
|
+
messageId: MESSAGE_ID_SUGGESTION,
|
|
64
|
+
/** @param {import('eslint').Rule.RuleFixer} fixer */
|
|
65
|
+
fix: fixer => fixer.replaceText(node, '0'),
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return problem;
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
75
|
+
const config = {
|
|
76
|
+
create,
|
|
77
|
+
meta: {
|
|
78
|
+
type: 'problem',
|
|
79
|
+
docs: {
|
|
80
|
+
description: 'Disallow arithmetic and bitwise operations that always evaluate to `0`.',
|
|
81
|
+
recommended: 'unopinionated',
|
|
82
|
+
},
|
|
83
|
+
hasSuggestions: true,
|
|
84
|
+
messages,
|
|
85
|
+
languages: [
|
|
86
|
+
'js/js',
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export default config;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {isCommentToken, hasSideEffect} from '@eslint-community/eslint-utils';
|
|
2
|
-
import {getReferences} from './utils/index.js';
|
|
2
|
+
import {containsSuspensionPoint, getReferences, trackBranchExits} from './utils/index.js';
|
|
3
3
|
|
|
4
4
|
const MESSAGE_ID = 'no-declarations-before-early-exit';
|
|
5
5
|
const messages = {
|
|
@@ -10,40 +10,17 @@ const isLetOrConstDeclaration = node =>
|
|
|
10
10
|
node.type === 'VariableDeclaration'
|
|
11
11
|
&& (node.kind === 'let' || node.kind === 'const');
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return node.body.at(-1);
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const isEarlyExitStatement = node =>
|
|
22
|
-
node?.type === 'ReturnStatement'
|
|
23
|
-
|| node?.type === 'ThrowStatement'
|
|
24
|
-
|| node?.type === 'BreakStatement'
|
|
25
|
-
|| node?.type === 'ContinueStatement';
|
|
26
|
-
|
|
27
|
-
function branchDefinitelyExits(node) {
|
|
28
|
-
const statement = getLastStatement(node);
|
|
29
|
-
if (isEarlyExitStatement(statement)) {
|
|
30
|
-
return true;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return statement?.type === 'IfStatement'
|
|
34
|
-
&& statement.alternate
|
|
35
|
-
&& branchDefinitelyExits(statement.consequent)
|
|
36
|
-
&& branchDefinitelyExits(statement.alternate);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function isGuardStatement(node) {
|
|
13
|
+
// A guard is an `if` statement where exactly one branch always exits, so control after it is
|
|
14
|
+
// conditional. `branchAlwaysExits` uses code path analysis, so guards whose exiting branch ends
|
|
15
|
+
// in an exhaustive `switch`, `try`/`finally`, or other complex control flow are detected too.
|
|
16
|
+
function isGuardStatement(node, branchAlwaysExits) {
|
|
40
17
|
if (node.type !== 'IfStatement') {
|
|
41
18
|
return false;
|
|
42
19
|
}
|
|
43
20
|
|
|
44
|
-
const consequentExits =
|
|
45
|
-
const alternateExits = node.alternate &&
|
|
46
|
-
return consequentExits !==
|
|
21
|
+
const consequentExits = branchAlwaysExits(node.consequent);
|
|
22
|
+
const alternateExits = Boolean(node.alternate) && branchAlwaysExits(node.alternate);
|
|
23
|
+
return consequentExits !== alternateExits;
|
|
47
24
|
}
|
|
48
25
|
|
|
49
26
|
// https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts
|
|
@@ -161,6 +138,11 @@ function getProblem({
|
|
|
161
138
|
return;
|
|
162
139
|
}
|
|
163
140
|
|
|
141
|
+
// Moving a declaration whose initializer suspends (`await`/`yield`) would reorder the guard's condition across the suspension point, changing observable timing.
|
|
142
|
+
if (declarator.init && containsSuspensionPoint(declarator.init, sourceCode.visitorKeys)) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
164
146
|
if (
|
|
165
147
|
guardIndex > declarationIndex + 1
|
|
166
148
|
&& !isSimpleInitializer(declarator.init)
|
|
@@ -213,7 +195,12 @@ function getProblem({
|
|
|
213
195
|
return problem;
|
|
214
196
|
}
|
|
215
197
|
|
|
216
|
-
function * getStatementListProblems(sourceCode, statements) {
|
|
198
|
+
function * getStatementListProblems(sourceCode, statements, branchAlwaysExits) {
|
|
199
|
+
// Resolve each statement to itself when it is a guard (or `undefined` otherwise) at most
|
|
200
|
+
// once per list, instead of re-running the code-path-analysis check for every
|
|
201
|
+
// declaration/statement pair.
|
|
202
|
+
let guards;
|
|
203
|
+
|
|
217
204
|
for (const [declarationIndex, declaration] of statements.entries()) {
|
|
218
205
|
if (
|
|
219
206
|
!isLetOrConstDeclaration(declaration)
|
|
@@ -222,8 +209,11 @@ function * getStatementListProblems(sourceCode, statements) {
|
|
|
222
209
|
continue;
|
|
223
210
|
}
|
|
224
211
|
|
|
225
|
-
|
|
226
|
-
|
|
212
|
+
guards ??= statements.map(statement => isGuardStatement(statement, branchAlwaysExits) ? statement : undefined);
|
|
213
|
+
|
|
214
|
+
for (let guardIndex = declarationIndex + 1; guardIndex < statements.length; guardIndex++) {
|
|
215
|
+
const guardStatement = guards[guardIndex];
|
|
216
|
+
if (!guardStatement) {
|
|
227
217
|
continue;
|
|
228
218
|
}
|
|
229
219
|
|
|
@@ -250,9 +240,12 @@ function * getStatementListProblems(sourceCode, statements) {
|
|
|
250
240
|
/** @param {import('eslint').Rule.RuleContext} context */
|
|
251
241
|
const create = context => {
|
|
252
242
|
const {sourceCode} = context;
|
|
243
|
+
const branchAlwaysExits = trackBranchExits(context);
|
|
253
244
|
|
|
254
|
-
|
|
255
|
-
|
|
245
|
+
// Run on exit so that all nested `if` statements have been visited and their branch exit
|
|
246
|
+
// information is available before scanning each statement list.
|
|
247
|
+
context.onExit('Program', node => getStatementListProblems(sourceCode, node.body, branchAlwaysExits));
|
|
248
|
+
context.onExit('BlockStatement', node => getStatementListProblems(sourceCode, node.body, branchAlwaysExits));
|
|
256
249
|
};
|
|
257
250
|
|
|
258
251
|
/** @type {import('eslint').Rule.RuleModule} */
|