eslint-plugin-unicorn 68.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/package.json +3 -1
- package/readme.md +12 -0
- package/rules/ast/is-empty-object-expression.js +2 -2
- package/rules/comment-content.js +136 -18
- package/rules/consistent-boolean-name.js +31 -4
- package/rules/consistent-conditional-object-spread.js +9 -3
- package/rules/consistent-tuple-labels.js +49 -0
- package/rules/custom-error-definition.js +26 -7
- package/rules/index.js +12 -0
- package/rules/no-chained-comparison.js +1 -1
- package/rules/no-computed-property-existence-check.js +6 -0
- package/rules/no-incorrect-template-string-interpolation.js +82 -30
- package/rules/no-instanceof-builtins.js +59 -13
- package/rules/no-invalid-well-known-symbol-methods.js +205 -0
- package/rules/no-late-current-target-access.js +9 -71
- package/rules/no-late-event-control.js +127 -0
- package/rules/no-nonstandard-builtin-properties.js +2 -0
- package/rules/no-object-methods-with-collections.js +4 -289
- package/rules/no-return-array-push.js +20 -5
- package/rules/no-top-level-side-effects.js +8 -1
- package/rules/no-unnecessary-global-this.js +36 -1
- package/rules/no-unreadable-object-destructuring.js +15 -0
- package/rules/no-unsafe-string-replacement.js +31 -0
- package/rules/no-unused-array-method-return.js +11 -2
- package/rules/no-useless-boolean-cast.js +58 -1
- package/rules/no-useless-concat.js +27 -3
- package/rules/no-useless-else.js +16 -2
- package/rules/no-useless-iterator-to-array.js +165 -115
- package/rules/prefer-abort-signal-timeout.js +395 -0
- package/rules/prefer-aggregate-error.js +478 -0
- package/rules/prefer-at.js +5 -6
- package/rules/prefer-code-point.js +58 -8
- package/rules/prefer-continue.js +16 -0
- package/rules/prefer-dom-node-replace-children.js +353 -0
- package/rules/prefer-error-is-error.js +181 -0
- package/rules/prefer-has-check.js +1 -4
- package/rules/prefer-hoisting-branch-code.js +9 -7
- package/rules/prefer-iterator-concat.js +21 -1
- package/rules/prefer-minimal-ternary.js +15 -11
- package/rules/prefer-modern-dom-apis.js +1 -45
- package/rules/prefer-observer-apis.js +498 -0
- package/rules/prefer-promise-try.js +238 -0
- package/rules/prefer-set-methods.js +337 -0
- package/rules/prefer-toggle-attribute.js +293 -0
- package/rules/prefer-url-search-parameters.js +411 -0
- package/rules/shared/late-event-handler.js +82 -0
- package/rules/shared/name-replacements.js +6 -0
- package/rules/utils/builtin-collection-type.js +267 -0
- package/rules/utils/index.js +6 -0
- package/rules/utils/is-boolean.js +3 -1
- package/rules/utils/is-event.js +127 -0
- package/rules/utils/should-report-replace-children-receiver.js +210 -0
- package/rules/utils/type-helpers.js +166 -8
|
@@ -10,7 +10,7 @@ const messages = {
|
|
|
10
10
|
|
|
11
11
|
const orderingOperators = new Set(['<', '>', '<=', '>=']);
|
|
12
12
|
const equalityOperators = new Set(['===', '!==', '==', '!=']);
|
|
13
|
-
const comparisonOperators =
|
|
13
|
+
const comparisonOperators = orderingOperators.union(equalityOperators);
|
|
14
14
|
|
|
15
15
|
const isComparison = node => node.type === 'BinaryExpression' && comparisonOperators.has(node.operator);
|
|
16
16
|
const isBooleanLiteral = node => node.type === 'Literal' && typeof node.value === 'boolean';
|
|
@@ -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,
|
|
@@ -20,6 +20,45 @@ const isBindingDeclaration = (raw, index) => {
|
|
|
20
20
|
return /^\s*(?:import|export|const|let|var)\b[^;={}]*$/.test(raw.slice(lineStart, index));
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
+
// `{type}` and `$expr}` inside an embedded block comment (e.g. JSDoc `@param {string}`) are not template interpolation mistakes (common in code-generation templates). Only closed `/* … */` comments count, so a stray `/*` inside a string doesn't suppress a genuine match.
|
|
24
|
+
const getBlockCommentRanges = raw => raw.matchAll(/\/\*[\s\S]*?\*\//g)
|
|
25
|
+
.map(match => [match.index, match.index + match[0].length])
|
|
26
|
+
.toArray();
|
|
27
|
+
|
|
28
|
+
const isInsideRanges = (ranges, index) => ranges.some(([start, end]) => index >= start && index < end);
|
|
29
|
+
|
|
30
|
+
const getTemplateElementRawRange = (node, sourceCode) => {
|
|
31
|
+
const [start, end] = sourceCode.getRange(node);
|
|
32
|
+
|
|
33
|
+
return [
|
|
34
|
+
start + 1,
|
|
35
|
+
end - (node.tail ? 1 : 2),
|
|
36
|
+
];
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const getTemplateRawWithExpressionsMasked = (node, sourceCode) => {
|
|
40
|
+
const [templateStart, templateEnd] = sourceCode.getRange(node);
|
|
41
|
+
const rawStart = templateStart + 1;
|
|
42
|
+
let raw = sourceCode.text.slice(rawStart, templateEnd - 1);
|
|
43
|
+
|
|
44
|
+
for (const [index, quasi] of node.quasis.entries()) {
|
|
45
|
+
const nextQuasi = node.quasis[index + 1];
|
|
46
|
+
|
|
47
|
+
if (!nextQuasi) {
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const [, quasiRawEnd] = getTemplateElementRawRange(quasi, sourceCode);
|
|
52
|
+
const [nextQuasiRawStart] = getTemplateElementRawRange(nextQuasi, sourceCode);
|
|
53
|
+
const maskStart = quasiRawEnd - rawStart;
|
|
54
|
+
const maskEnd = nextQuasiRawStart - rawStart;
|
|
55
|
+
|
|
56
|
+
raw = raw.slice(0, maskStart) + ' '.repeat(maskEnd - maskStart) + raw.slice(maskEnd);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return raw;
|
|
60
|
+
};
|
|
61
|
+
|
|
23
62
|
const getMissingDollarReplacements = raw => raw.matchAll(missingDollar)
|
|
24
63
|
.filter(match => !isBindingDeclaration(raw, match.index))
|
|
25
64
|
.map(match => ({
|
|
@@ -37,49 +76,62 @@ const getMissingOpeningBraceReplacements = raw => raw.matchAll(missingOpeningBra
|
|
|
37
76
|
}))
|
|
38
77
|
.toArray();
|
|
39
78
|
|
|
40
|
-
const getReplacements = raw => {
|
|
79
|
+
const getReplacements = (raw, blockCommentRanges, rawOffset) => {
|
|
41
80
|
const replacements = [
|
|
42
81
|
...getMissingDollarReplacements(raw),
|
|
43
82
|
...getMissingOpeningBraceReplacements(raw),
|
|
44
83
|
];
|
|
45
84
|
|
|
46
|
-
|
|
85
|
+
if (replacements.length === 0) {
|
|
86
|
+
return replacements;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return replacements
|
|
90
|
+
.filter(replacement => !isInsideRanges(blockCommentRanges, rawOffset + replacement.index))
|
|
91
|
+
.toSorted((replacement, nextReplacement) => replacement.index - nextReplacement.index);
|
|
47
92
|
};
|
|
48
93
|
|
|
49
94
|
/** @param {import('eslint').Rule.RuleContext} context */
|
|
50
95
|
const create = context => {
|
|
51
|
-
context.on('
|
|
52
|
-
if (isTaggedTemplateLiteral(node
|
|
96
|
+
context.on('TemplateLiteral', function * (node) {
|
|
97
|
+
if (isTaggedTemplateLiteral(node)) {
|
|
53
98
|
return;
|
|
54
99
|
}
|
|
55
100
|
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
{
|
|
74
|
-
messageId: MESSAGE_ID_SUGGESTION,
|
|
75
|
-
data: {
|
|
76
|
-
incorrect,
|
|
77
|
-
correct,
|
|
78
|
-
},
|
|
79
|
-
fix: fixer => fixer.replaceTextRange([start, end], correct),
|
|
101
|
+
const templateRawStart = context.sourceCode.getRange(node)[0] + 1;
|
|
102
|
+
const blockCommentRanges = getBlockCommentRanges(getTemplateRawWithExpressionsMasked(node, context.sourceCode));
|
|
103
|
+
|
|
104
|
+
for (const quasi of node.quasis) {
|
|
105
|
+
const [rawStart, rawEnd] = getTemplateElementRawRange(quasi, context.sourceCode);
|
|
106
|
+
const raw = context.sourceCode.text.slice(rawStart, rawEnd);
|
|
107
|
+
const rawOffset = rawStart - templateRawStart;
|
|
108
|
+
|
|
109
|
+
for (const {index, incorrect, correct} of getReplacements(raw, blockCommentRanges, rawOffset)) {
|
|
110
|
+
const start = rawStart + index;
|
|
111
|
+
const end = start + incorrect.length;
|
|
112
|
+
|
|
113
|
+
yield {
|
|
114
|
+
node: quasi,
|
|
115
|
+
loc: {
|
|
116
|
+
start: context.sourceCode.getLocFromIndex(start),
|
|
117
|
+
end: context.sourceCode.getLocFromIndex(end),
|
|
80
118
|
},
|
|
81
|
-
|
|
82
|
-
|
|
119
|
+
messageId: MESSAGE_ID,
|
|
120
|
+
data: {
|
|
121
|
+
correct,
|
|
122
|
+
},
|
|
123
|
+
suggest: [
|
|
124
|
+
{
|
|
125
|
+
messageId: MESSAGE_ID_SUGGESTION,
|
|
126
|
+
data: {
|
|
127
|
+
incorrect,
|
|
128
|
+
correct,
|
|
129
|
+
},
|
|
130
|
+
fix: fixer => fixer.replaceTextRange([start, end], correct),
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
};
|
|
134
|
+
}
|
|
83
135
|
}
|
|
84
136
|
});
|
|
85
137
|
};
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
getParenthesizedRange,
|
|
4
4
|
getTokenStore,
|
|
5
5
|
isParenthesized,
|
|
6
|
+
isGlobalIdentifier,
|
|
6
7
|
shouldAddParenthesesToUnaryExpressionArgument,
|
|
7
8
|
} from './utils/index.js';
|
|
8
9
|
import {replaceNodeOrTokenAndSpacesBefore, fixSpaceAroundKeyword} from './fix/index.js';
|
|
@@ -26,6 +27,13 @@ const primitiveWrappers = new Set([
|
|
|
26
27
|
'Symbol',
|
|
27
28
|
]);
|
|
28
29
|
|
|
30
|
+
const globalObjectNames = new Set([
|
|
31
|
+
'global',
|
|
32
|
+
'globalThis',
|
|
33
|
+
'self',
|
|
34
|
+
'window',
|
|
35
|
+
]);
|
|
36
|
+
|
|
29
37
|
const strictStrategyConstructors = [
|
|
30
38
|
// Error types
|
|
31
39
|
...builtinErrors,
|
|
@@ -73,7 +81,7 @@ const replaceWithFunctionCall = (node, context, functionName) => function * (fix
|
|
|
73
81
|
yield replaceNodeOrTokenAndSpacesBefore(right, '', fixer, context, tokenStore);
|
|
74
82
|
};
|
|
75
83
|
|
|
76
|
-
const replaceWithTypeOfExpression = (node, context) => function * (fixer) {
|
|
84
|
+
const replaceWithTypeOfExpression = (node, context, constructorName) => function * (fixer) {
|
|
77
85
|
const {left, right} = node;
|
|
78
86
|
const tokenStore = getTokenStore(context, node);
|
|
79
87
|
const instanceofToken = tokenStore.getTokenAfter(left, isInstanceofToken);
|
|
@@ -105,9 +113,35 @@ const replaceWithTypeOfExpression = (node, context) => function * (fixer) {
|
|
|
105
113
|
|
|
106
114
|
const rightRange = getParenthesizedRange(right, {sourceCode: tokenStore});
|
|
107
115
|
|
|
108
|
-
yield fixer.replaceTextRange(rightRange, safeQuote +
|
|
116
|
+
yield fixer.replaceTextRange(rightRange, safeQuote + constructorName.toLowerCase() + safeQuote);
|
|
109
117
|
};
|
|
110
118
|
|
|
119
|
+
function getConstructor(node, context) {
|
|
120
|
+
if (node.type === 'Identifier') {
|
|
121
|
+
return {
|
|
122
|
+
name: node.name,
|
|
123
|
+
referenceText: node.name,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (
|
|
128
|
+
node.type === 'MemberExpression'
|
|
129
|
+
&& !node.computed
|
|
130
|
+
&& node.property.type === 'Identifier'
|
|
131
|
+
&& node.object.type === 'Identifier'
|
|
132
|
+
&& globalObjectNames.has(node.object.name)
|
|
133
|
+
&& isGlobalIdentifier(node.object, context)
|
|
134
|
+
) {
|
|
135
|
+
return {
|
|
136
|
+
name: node.property.name,
|
|
137
|
+
referenceText: context.sourceCode.getText(node),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const hasCommentsInside = (node, context) =>
|
|
143
|
+
context.sourceCode.getCommentsInside(node).length > 0;
|
|
144
|
+
|
|
111
145
|
/** @param {import('eslint').Rule.RuleContext} context */
|
|
112
146
|
const create = context => {
|
|
113
147
|
const {
|
|
@@ -124,11 +158,16 @@ const create = context => {
|
|
|
124
158
|
context.on('BinaryExpression', /** @param {import('estree').BinaryExpression} node */ node => {
|
|
125
159
|
const {right, operator} = node;
|
|
126
160
|
|
|
127
|
-
if (
|
|
161
|
+
if (operator !== 'instanceof') {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const constructor = getConstructor(right, context);
|
|
166
|
+
if (!constructor || exclude.includes(constructor.name)) {
|
|
128
167
|
return;
|
|
129
168
|
}
|
|
130
169
|
|
|
131
|
-
const constructorName =
|
|
170
|
+
const {name: constructorName, referenceText} = constructor;
|
|
132
171
|
|
|
133
172
|
/** @type {import('eslint').Rule.ReportDescriptor} */
|
|
134
173
|
const problem = {
|
|
@@ -140,24 +179,31 @@ const create = context => {
|
|
|
140
179
|
constructorName === 'Array'
|
|
141
180
|
|| (constructorName === 'Error' && useErrorIsError)
|
|
142
181
|
) {
|
|
143
|
-
const
|
|
182
|
+
const methodName = constructorName === 'Array' ? 'isArray' : 'isError';
|
|
183
|
+
const functionName = `${referenceText}.${methodName}`;
|
|
144
184
|
problem.fix = replaceWithFunctionCall(node, context, functionName);
|
|
145
185
|
return problem;
|
|
146
186
|
}
|
|
147
187
|
|
|
148
188
|
if (constructorName === 'Function') {
|
|
149
|
-
|
|
189
|
+
if (!hasCommentsInside(right, context)) {
|
|
190
|
+
problem.fix = replaceWithTypeOfExpression(node, context, constructorName);
|
|
191
|
+
}
|
|
192
|
+
|
|
150
193
|
return problem;
|
|
151
194
|
}
|
|
152
195
|
|
|
153
196
|
if (primitiveWrappers.has(constructorName)) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
197
|
+
if (!hasCommentsInside(right, context)) {
|
|
198
|
+
problem.suggest = [
|
|
199
|
+
{
|
|
200
|
+
messageId: MESSAGE_ID_SWITCH_TO_TYPE_OF,
|
|
201
|
+
data: {type: constructorName.toLowerCase()},
|
|
202
|
+
fix: replaceWithTypeOfExpression(node, context, constructorName),
|
|
203
|
+
},
|
|
204
|
+
];
|
|
205
|
+
}
|
|
206
|
+
|
|
161
207
|
return problem;
|
|
162
208
|
}
|
|
163
209
|
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import {isPromiseType} from './utils/index.js';
|
|
2
|
+
|
|
3
|
+
const MESSAGE_ID_ASYNC_DISPOSE = 'no-invalid-well-known-symbol-methods/async-dispose';
|
|
4
|
+
const MESSAGE_ID_DISPOSE_GENERATOR = 'no-invalid-well-known-symbol-methods/dispose-generator';
|
|
5
|
+
const MESSAGE_ID_SYNC_ITERATOR = 'no-invalid-well-known-symbol-methods/sync-iterator';
|
|
6
|
+
const MESSAGE_ID_ASYNC_ITERATOR = 'no-invalid-well-known-symbol-methods/async-iterator';
|
|
7
|
+
const MESSAGE_ID_SUGGESTION = 'no-invalid-well-known-symbol-methods/suggestion';
|
|
8
|
+
|
|
9
|
+
const messages = {
|
|
10
|
+
[MESSAGE_ID_ASYNC_DISPOSE]: '`Symbol.dispose` must not return a Promise. Use `Symbol.asyncDispose` instead.',
|
|
11
|
+
[MESSAGE_ID_DISPOSE_GENERATOR]: '`Symbol.dispose` must not be a generator. Use a normal method for synchronous disposal or `Symbol.asyncDispose` for asynchronous disposal.',
|
|
12
|
+
[MESSAGE_ID_SYNC_ITERATOR]: '`Symbol.iterator` must return a sync iterator. Use `Symbol.asyncIterator` for async iterators.',
|
|
13
|
+
[MESSAGE_ID_ASYNC_ITERATOR]: '`Symbol.asyncIterator` must return an async iterator directly. Use an async generator or return the iterator synchronously.',
|
|
14
|
+
[MESSAGE_ID_SUGGESTION]: 'Use `Symbol.{{replacement}}` instead.',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const promiseTypeNames = new Set(['Promise', 'PromiseLike']);
|
|
18
|
+
const checkedSymbolNames = new Set(['dispose', 'iterator', 'asyncIterator']);
|
|
19
|
+
|
|
20
|
+
function getWellKnownSymbolName(member) {
|
|
21
|
+
if (
|
|
22
|
+
!member.computed
|
|
23
|
+
|| member.key.type !== 'MemberExpression'
|
|
24
|
+
|| member.key.computed
|
|
25
|
+
|| member.key.object.type !== 'Identifier'
|
|
26
|
+
|| member.key.object.name !== 'Symbol'
|
|
27
|
+
|| member.key.property.type !== 'Identifier'
|
|
28
|
+
) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return member.key.property.name;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getFunctionNode(member) {
|
|
36
|
+
if (member.kind === 'get' || member.kind === 'set') {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (
|
|
41
|
+
member.type === 'MethodDefinition'
|
|
42
|
+
|| member.type === 'PropertyDefinition'
|
|
43
|
+
) {
|
|
44
|
+
return member.value;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (member.type === 'Property') {
|
|
48
|
+
return member.value;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isFunctionNode(node) {
|
|
53
|
+
return node?.type === 'FunctionExpression' || node?.type === 'ArrowFunctionExpression';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isPromiseTypeReference(node) {
|
|
57
|
+
return (
|
|
58
|
+
node.type === 'TSTypeReference'
|
|
59
|
+
&& node.typeName.type === 'Identifier'
|
|
60
|
+
&& promiseTypeNames.has(node.typeName.name)
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function hasPromiseReturnTypeAnnotation(functionNode) {
|
|
65
|
+
const typeNode = functionNode.returnType?.typeAnnotation;
|
|
66
|
+
if (!typeNode) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (typeNode.type === 'TSTypeReference') {
|
|
71
|
+
return isPromiseTypeReference(typeNode);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (typeNode.type === 'TSUnionType') {
|
|
75
|
+
return typeNode.types.some(typeNode => isPromiseTypeReference(typeNode));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function hasPromiseReturnTypeInformation(functionNode, context) {
|
|
82
|
+
const {parserServices} = context.sourceCode;
|
|
83
|
+
if (!parserServices?.program) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const checker = parserServices.program.getTypeChecker();
|
|
89
|
+
const typeScriptNode = parserServices.esTreeNodeToTSNodeMap.get(functionNode);
|
|
90
|
+
const signature = checker.getSignatureFromDeclaration(typeScriptNode);
|
|
91
|
+
if (signature) {
|
|
92
|
+
return isPromiseType(checker.getReturnTypeOfSignature(signature), checker) === true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const type = parserServices.getTypeAtLocation(functionNode);
|
|
96
|
+
return type.getCallSignatures().some(signature =>
|
|
97
|
+
isPromiseType(checker.getReturnTypeOfSignature(signature), checker) === true,
|
|
98
|
+
);
|
|
99
|
+
} catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const hasPromiseReturnType = (functionNode, context) =>
|
|
105
|
+
hasPromiseReturnTypeAnnotation(functionNode)
|
|
106
|
+
|| hasPromiseReturnTypeInformation(functionNode, context);
|
|
107
|
+
|
|
108
|
+
function getReplacementSuggestion(member, replacement) {
|
|
109
|
+
return {
|
|
110
|
+
messageId: MESSAGE_ID_SUGGESTION,
|
|
111
|
+
data: {replacement},
|
|
112
|
+
fix: fixer => fixer.replaceText(member.key.property, replacement),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function getProblem(member, context) {
|
|
117
|
+
const symbolName = getWellKnownSymbolName(member);
|
|
118
|
+
if (!checkedSymbolNames.has(symbolName)) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const functionNode = getFunctionNode(member);
|
|
123
|
+
if (!isFunctionNode(functionNode)) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (
|
|
128
|
+
symbolName === 'dispose'
|
|
129
|
+
&& functionNode.generator
|
|
130
|
+
) {
|
|
131
|
+
return {
|
|
132
|
+
node: member.key,
|
|
133
|
+
messageId: MESSAGE_ID_DISPOSE_GENERATOR,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const returnsPromise = !functionNode.async && hasPromiseReturnType(functionNode, context);
|
|
138
|
+
|
|
139
|
+
if (
|
|
140
|
+
symbolName === 'dispose'
|
|
141
|
+
&& (functionNode.async || returnsPromise)
|
|
142
|
+
) {
|
|
143
|
+
return {
|
|
144
|
+
node: member.key,
|
|
145
|
+
messageId: MESSAGE_ID_ASYNC_DISPOSE,
|
|
146
|
+
fix: fixer => fixer.replaceText(member.key.property, 'asyncDispose'),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (
|
|
151
|
+
symbolName === 'iterator'
|
|
152
|
+
&& (functionNode.async || returnsPromise)
|
|
153
|
+
) {
|
|
154
|
+
const problem = {
|
|
155
|
+
node: member.key,
|
|
156
|
+
messageId: MESSAGE_ID_SYNC_ITERATOR,
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
if (functionNode.generator) {
|
|
160
|
+
problem.suggest = [
|
|
161
|
+
getReplacementSuggestion(member, 'asyncIterator'),
|
|
162
|
+
];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return problem;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (
|
|
169
|
+
symbolName === 'asyncIterator'
|
|
170
|
+
&& !functionNode.generator
|
|
171
|
+
&& (functionNode.async || returnsPromise)
|
|
172
|
+
) {
|
|
173
|
+
return {
|
|
174
|
+
node: member.key,
|
|
175
|
+
messageId: MESSAGE_ID_ASYNC_ITERATOR,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
181
|
+
const create = context => {
|
|
182
|
+
context.on('MethodDefinition', node => getProblem(node, context));
|
|
183
|
+
context.on('Property', node => getProblem(node, context));
|
|
184
|
+
context.on('PropertyDefinition', node => getProblem(node, context));
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
188
|
+
const config = {
|
|
189
|
+
create,
|
|
190
|
+
meta: {
|
|
191
|
+
type: 'problem',
|
|
192
|
+
docs: {
|
|
193
|
+
description: 'Disallow invalid implementations of well-known symbol methods.',
|
|
194
|
+
recommended: 'unopinionated',
|
|
195
|
+
},
|
|
196
|
+
fixable: 'code',
|
|
197
|
+
hasSuggestions: true,
|
|
198
|
+
messages,
|
|
199
|
+
languages: [
|
|
200
|
+
'js/js',
|
|
201
|
+
],
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
export default config;
|
|
@@ -1,84 +1,22 @@
|
|
|
1
1
|
import {findVariable} from '@eslint-community/eslint-utils';
|
|
2
|
-
import {isMemberExpression
|
|
3
|
-
import {
|
|
2
|
+
import {isMemberExpression} from './ast/index.js';
|
|
3
|
+
import {
|
|
4
|
+
createLateEventHandlerTracker,
|
|
5
|
+
eventParameterNamePattern,
|
|
6
|
+
getEnclosingFunction,
|
|
7
|
+
} from './shared/late-event-handler.js';
|
|
4
8
|
|
|
5
9
|
const MESSAGE_ID_AFTER_SUSPENSION = 'after-suspension';
|
|
6
10
|
const MESSAGE_ID_IN_NESTED_FUNCTION = 'in-nested-function';
|
|
7
|
-
const eventParameterNamePattern = /^(?:e|event|evt|[a-z][\dA-Za-z]*Event)$/u;
|
|
8
11
|
const messages = {
|
|
9
12
|
[MESSAGE_ID_AFTER_SUSPENSION]: '`{{name}}.currentTarget` is `null` after the handler suspends. It is only set during synchronous event dispatch; save it to a variable beforehand.',
|
|
10
13
|
[MESSAGE_ID_IN_NESTED_FUNCTION]: '`{{name}}.currentTarget` is `null` inside this nested function. It is only set during synchronous event dispatch; save it to a variable in the outer function.',
|
|
11
14
|
};
|
|
12
15
|
|
|
13
|
-
const getEnclosingFunction = node => {
|
|
14
|
-
for (let current = node.parent; current; current = current.parent) {
|
|
15
|
-
if (isFunction(current)) {
|
|
16
|
-
return current;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
};
|
|
20
|
-
|
|
21
16
|
/** @param {import('eslint').Rule.RuleContext} context */
|
|
22
17
|
const create = context => {
|
|
23
18
|
const {sourceCode} = context;
|
|
24
|
-
|
|
25
|
-
const suspendedFunctions = new Set();
|
|
26
|
-
|
|
27
|
-
// ESLint traverses in source order and fires `onExit` only after the whole subtree of the
|
|
28
|
-
// suspension point is visited, so by the time a later access is reached it has “completed”.
|
|
29
|
-
const markSuspended = node => {
|
|
30
|
-
const functionNode = getEnclosingFunction(node);
|
|
31
|
-
if (functionNode) {
|
|
32
|
-
suspendedFunctions.add(functionNode);
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const isRepeatedLoopPart = (loop, child) => {
|
|
37
|
-
switch (loop.type) {
|
|
38
|
-
case 'ForStatement': {
|
|
39
|
-
return [
|
|
40
|
-
loop.test,
|
|
41
|
-
loop.update,
|
|
42
|
-
loop.body,
|
|
43
|
-
].includes(child);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
case 'ForInStatement':
|
|
47
|
-
case 'ForOfStatement': {
|
|
48
|
-
return child !== loop.right;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
case 'DoWhileStatement':
|
|
52
|
-
case 'WhileStatement': {
|
|
53
|
-
return true;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
default: {
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const isInsideSuspendingLoop = (node, functionNode) => {
|
|
63
|
-
for (let child = node, current = node.parent; current && current !== functionNode; child = current, current = current.parent) {
|
|
64
|
-
if (
|
|
65
|
-
isRepeatedLoopPart(current, child)
|
|
66
|
-
&& containsSuspensionPoint(current, sourceCode.visitorKeys)
|
|
67
|
-
) {
|
|
68
|
-
return true;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return false;
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
context.onExit('AwaitExpression', markSuspended);
|
|
76
|
-
context.onExit('YieldExpression', markSuspended);
|
|
77
|
-
context.onExit('ForOfStatement', node => {
|
|
78
|
-
if (node.await) {
|
|
79
|
-
markSuspended(node);
|
|
80
|
-
}
|
|
81
|
-
});
|
|
19
|
+
const lateEventHandlerTracker = createLateEventHandlerTracker(context);
|
|
82
20
|
|
|
83
21
|
context.on('MemberExpression', node => {
|
|
84
22
|
if (
|
|
@@ -104,8 +42,8 @@ const create = context => {
|
|
|
104
42
|
|
|
105
43
|
if (
|
|
106
44
|
!isInNestedFunction
|
|
107
|
-
&& !
|
|
108
|
-
&& !isInsideSuspendingLoop(node, handlerFunction)
|
|
45
|
+
&& !lateEventHandlerTracker.isFunctionSuspended(handlerFunction)
|
|
46
|
+
&& !lateEventHandlerTracker.isInsideSuspendingLoop(node, handlerFunction)
|
|
109
47
|
) {
|
|
110
48
|
return;
|
|
111
49
|
}
|