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
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import {
|
|
2
|
+
findVariable,
|
|
3
|
+
getPropertyName,
|
|
4
|
+
getStaticValue,
|
|
5
|
+
} from '@eslint-community/eslint-utils';
|
|
6
|
+
import {
|
|
7
|
+
isCallExpression,
|
|
8
|
+
isMemberExpression,
|
|
9
|
+
isNewExpression,
|
|
10
|
+
} from './ast/index.js';
|
|
11
|
+
import {removeStatement} from './fix/index.js';
|
|
12
|
+
import {
|
|
13
|
+
getLastTrailingCommentOnSameLine,
|
|
14
|
+
hasCommentInRange,
|
|
15
|
+
isGlobalIdentifier,
|
|
16
|
+
isLeftHandSide,
|
|
17
|
+
isTypeScriptExpressionWrapper,
|
|
18
|
+
} from './utils/index.js';
|
|
19
|
+
|
|
20
|
+
const MESSAGE_ID = 'prefer-abort-signal-timeout';
|
|
21
|
+
const SUGGESTION_ID = 'prefer-abort-signal-timeout/suggestion';
|
|
22
|
+
const MAX_SET_TIMEOUT_DELAY = (2 ** 31) - 1;
|
|
23
|
+
const reasonSensitiveProperties = new Set([
|
|
24
|
+
'reason',
|
|
25
|
+
'throwIfAborted',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
const messages = {
|
|
29
|
+
[MESSAGE_ID]: 'Prefer `AbortSignal.timeout()` over manually aborting an `AbortController` with `setTimeout()`.',
|
|
30
|
+
[SUGGESTION_ID]: 'Replace with `AbortSignal.timeout()`.',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const getStatementList = statement => {
|
|
34
|
+
if (Array.isArray(statement.parent.body)) {
|
|
35
|
+
return statement.parent.body;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (Array.isArray(statement.parent.consequent)) {
|
|
39
|
+
return statement.parent.consequent;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const getNextStatement = statement => {
|
|
44
|
+
const statements = getStatementList(statement);
|
|
45
|
+
if (!statements) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return statements[statements.indexOf(statement) + 1];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const isGlobalNameAvailable = (name, node, context) => {
|
|
53
|
+
const variable = findVariable(context.sourceCode.getScope(node), name);
|
|
54
|
+
return !variable || variable.defs.length === 0;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const isGlobalAbortControllerConstructor = (node, context) =>
|
|
58
|
+
isNewExpression(node, {
|
|
59
|
+
name: 'AbortController',
|
|
60
|
+
argumentsLength: 0,
|
|
61
|
+
})
|
|
62
|
+
&& isGlobalIdentifier(node.callee, context);
|
|
63
|
+
|
|
64
|
+
const getTimeoutCall = (statement, context) => {
|
|
65
|
+
if (
|
|
66
|
+
statement?.type !== 'ExpressionStatement'
|
|
67
|
+
|| !isCallExpression(statement.expression, {
|
|
68
|
+
name: 'setTimeout',
|
|
69
|
+
argumentsLength: 2,
|
|
70
|
+
optional: false,
|
|
71
|
+
})
|
|
72
|
+
|| !isGlobalIdentifier(statement.expression.callee, context)
|
|
73
|
+
) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return statement.expression;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const getCallbackExpression = callback => {
|
|
81
|
+
if (
|
|
82
|
+
!callback
|
|
83
|
+
|| (
|
|
84
|
+
callback.type !== 'ArrowFunctionExpression'
|
|
85
|
+
&& callback.type !== 'FunctionExpression'
|
|
86
|
+
)
|
|
87
|
+
|| callback.async
|
|
88
|
+
|| callback.generator
|
|
89
|
+
|| callback.params.length > 0
|
|
90
|
+
) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (callback.body.type !== 'BlockStatement') {
|
|
95
|
+
return callback.body;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (
|
|
99
|
+
callback.body.body.length !== 1
|
|
100
|
+
|| callback.body.body[0].type !== 'ExpressionStatement'
|
|
101
|
+
) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return callback.body.body[0].expression;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const getAbortReference = (timeoutCall, controllerName) => {
|
|
109
|
+
const [callback] = timeoutCall.arguments;
|
|
110
|
+
const expression = getCallbackExpression(callback);
|
|
111
|
+
|
|
112
|
+
if (
|
|
113
|
+
!isCallExpression(expression, {
|
|
114
|
+
argumentsLength: 0,
|
|
115
|
+
optional: false,
|
|
116
|
+
})
|
|
117
|
+
|| !isMemberExpression(expression.callee, {
|
|
118
|
+
property: 'abort',
|
|
119
|
+
computed: false,
|
|
120
|
+
optional: false,
|
|
121
|
+
})
|
|
122
|
+
|| expression.callee.object.type !== 'Identifier'
|
|
123
|
+
|| expression.callee.object.name !== controllerName
|
|
124
|
+
) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return expression.callee.object;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const isForLoopLeftSide = node =>
|
|
132
|
+
(
|
|
133
|
+
node.parent.type === 'ForInStatement'
|
|
134
|
+
|| node.parent.type === 'ForOfStatement'
|
|
135
|
+
)
|
|
136
|
+
&& node.parent.left === node;
|
|
137
|
+
|
|
138
|
+
const isReasonSensitiveProperty = (node, context) =>
|
|
139
|
+
reasonSensitiveProperties.has(getPropertyName(node, context.sourceCode.getScope(node)));
|
|
140
|
+
|
|
141
|
+
const isReasonSensitiveRead = (node, context) =>
|
|
142
|
+
isMemberExpression(node.parent)
|
|
143
|
+
&& node.parent.object === node
|
|
144
|
+
&& isReasonSensitiveProperty(node.parent, context);
|
|
145
|
+
|
|
146
|
+
const getExpressionParentIgnoringTypeScriptWrappers = node => {
|
|
147
|
+
let expression = node;
|
|
148
|
+
let {parent} = node;
|
|
149
|
+
|
|
150
|
+
while (isTypeScriptExpressionWrapper(parent)) {
|
|
151
|
+
expression = parent;
|
|
152
|
+
parent = parent.parent;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {expression, parent};
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const isSignalAlias = node => {
|
|
159
|
+
const {expression, parent} = getExpressionParentIgnoringTypeScriptWrappers(node);
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
parent.type === 'VariableDeclarator'
|
|
163
|
+
&& parent.init === expression
|
|
164
|
+
) || (
|
|
165
|
+
parent.type === 'AssignmentExpression'
|
|
166
|
+
&& parent.right === expression
|
|
167
|
+
);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const getSignalMember = (identifier, context) => {
|
|
171
|
+
const {parent} = identifier;
|
|
172
|
+
|
|
173
|
+
if (
|
|
174
|
+
!isMemberExpression(parent, {
|
|
175
|
+
property: 'signal',
|
|
176
|
+
computed: false,
|
|
177
|
+
optional: false,
|
|
178
|
+
})
|
|
179
|
+
|| parent.object !== identifier
|
|
180
|
+
|| isLeftHandSide(parent)
|
|
181
|
+
|| isForLoopLeftSide(parent)
|
|
182
|
+
|| isReasonSensitiveRead(parent, context)
|
|
183
|
+
|| isSignalAlias(parent)
|
|
184
|
+
|| hasCommentInRange(context, context.sourceCode.getRange(parent))
|
|
185
|
+
) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return parent;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const getSignalMembers = (variable, abortReference, context) => {
|
|
193
|
+
const signalMembers = [];
|
|
194
|
+
|
|
195
|
+
for (const reference of variable.references) {
|
|
196
|
+
const {identifier} = reference;
|
|
197
|
+
|
|
198
|
+
if (
|
|
199
|
+
identifier === abortReference
|
|
200
|
+
|| reference.init
|
|
201
|
+
) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (reference.isWrite()) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const signalMember = getSignalMember(identifier, context);
|
|
210
|
+
if (!signalMember) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
signalMembers.push(signalMember);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return signalMembers.length > 0 ? signalMembers : undefined;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const isInsideRange = (node, range, sourceCode) => {
|
|
221
|
+
const nodeRange = sourceCode.getRange(node);
|
|
222
|
+
return nodeRange[0] >= range[0] && nodeRange[1] <= range[1];
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const hasCommentBetween = (context, leftNode, rightNode) => {
|
|
226
|
+
const {sourceCode} = context;
|
|
227
|
+
const [, leftEnd] = sourceCode.getRange(leftNode);
|
|
228
|
+
const [rightStart] = sourceCode.getRange(rightNode);
|
|
229
|
+
|
|
230
|
+
return sourceCode.getAllComments().some(comment => {
|
|
231
|
+
const [commentStart, commentEnd] = sourceCode.getRange(comment);
|
|
232
|
+
return commentStart >= leftEnd && commentEnd <= rightStart;
|
|
233
|
+
});
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const isValidAbortSignalTimeoutDelay = (node, context) => {
|
|
237
|
+
const staticValue = getStaticValue(node, context.sourceCode.getScope(node));
|
|
238
|
+
|
|
239
|
+
if (!staticValue) {
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const {value} = staticValue;
|
|
244
|
+
return typeof value === 'number'
|
|
245
|
+
&& Number.isSafeInteger(value)
|
|
246
|
+
&& value >= 0
|
|
247
|
+
&& value <= MAX_SET_TIMEOUT_DELAY;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const shouldSkipDelay = (delay, signalMembers, timeoutStatementRange, context) =>
|
|
251
|
+
delay.type === 'SequenceExpression'
|
|
252
|
+
|| !isValidAbortSignalTimeoutDelay(delay, context)
|
|
253
|
+
|| signalMembers.some(signalMember => isInsideRange(signalMember, timeoutStatementRange, context.sourceCode));
|
|
254
|
+
|
|
255
|
+
const hasNameConflict = (name, variable, node, context) => {
|
|
256
|
+
const existingVariable = findVariable(context.sourceCode.getScope(node), name);
|
|
257
|
+
return existingVariable && existingVariable !== variable;
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const getReplacementName = (name, variable, signalMembers, context) => {
|
|
261
|
+
let replacementName = name;
|
|
262
|
+
|
|
263
|
+
if (name === 'abortController') {
|
|
264
|
+
replacementName = 'abortSignal';
|
|
265
|
+
} else if (name === 'controller') {
|
|
266
|
+
replacementName = 'signal';
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (replacementName === name) {
|
|
270
|
+
return name;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (
|
|
274
|
+
hasNameConflict(replacementName, variable, variable.identifiers[0], context)
|
|
275
|
+
|| signalMembers.some(signalMember => hasNameConflict(replacementName, variable, signalMember, context))
|
|
276
|
+
) {
|
|
277
|
+
return name;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return replacementName;
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const createProblem = (declarator, context) => {
|
|
284
|
+
const {sourceCode} = context;
|
|
285
|
+
const declaration = declarator.parent;
|
|
286
|
+
const {id, init} = declarator;
|
|
287
|
+
|
|
288
|
+
if (
|
|
289
|
+
declaration.type !== 'VariableDeclaration'
|
|
290
|
+
|| declaration.kind !== 'const'
|
|
291
|
+
|| declaration.declarations.length !== 1
|
|
292
|
+
|| id.type !== 'Identifier'
|
|
293
|
+
|| !isGlobalAbortControllerConstructor(init, context)
|
|
294
|
+
|| !isGlobalNameAvailable('AbortSignal', id, context)
|
|
295
|
+
|| hasCommentInRange(context, sourceCode.getRange(init))
|
|
296
|
+
|| (
|
|
297
|
+
id.typeAnnotation
|
|
298
|
+
&& hasCommentInRange(context, sourceCode.getRange(id.typeAnnotation))
|
|
299
|
+
)
|
|
300
|
+
) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const timeoutStatement = getNextStatement(declaration);
|
|
305
|
+
const timeoutCall = getTimeoutCall(timeoutStatement, context);
|
|
306
|
+
if (
|
|
307
|
+
!timeoutCall
|
|
308
|
+
|| hasCommentBetween(context, declaration, timeoutStatement)
|
|
309
|
+
|| hasCommentInRange(context, sourceCode.getRange(timeoutStatement))
|
|
310
|
+
|| getLastTrailingCommentOnSameLine(context, timeoutStatement)
|
|
311
|
+
) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const abortReference = getAbortReference(timeoutCall, id.name);
|
|
316
|
+
if (!abortReference) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const variable = findVariable(sourceCode.getScope(id), id);
|
|
321
|
+
if (!variable) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (findVariable(sourceCode.getScope(abortReference), abortReference) !== variable) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const signalMembers = getSignalMembers(variable, abortReference, context);
|
|
330
|
+
if (!signalMembers) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const timeoutStatementRange = sourceCode.getRange(timeoutStatement);
|
|
335
|
+
const [, delay] = timeoutCall.arguments;
|
|
336
|
+
if (shouldSkipDelay(delay, signalMembers, timeoutStatementRange, context)) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const replacementName = getReplacementName(id.name, variable, signalMembers, context);
|
|
341
|
+
const timeoutText = `AbortSignal.timeout(${sourceCode.getText(delay)})`;
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
node: id,
|
|
345
|
+
messageId: MESSAGE_ID,
|
|
346
|
+
suggest: [
|
|
347
|
+
{
|
|
348
|
+
messageId: SUGGESTION_ID,
|
|
349
|
+
* fix(fixer) {
|
|
350
|
+
yield fixer.replaceText(init, timeoutText);
|
|
351
|
+
|
|
352
|
+
if (id.typeAnnotation) {
|
|
353
|
+
yield fixer.replaceText(id.typeAnnotation, ': AbortSignal');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (replacementName !== id.name) {
|
|
357
|
+
const [idStart] = sourceCode.getRange(id);
|
|
358
|
+
yield fixer.replaceTextRange([idStart, idStart + id.name.length], replacementName);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
for (const signalMember of signalMembers) {
|
|
362
|
+
yield fixer.replaceText(signalMember, replacementName);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
yield removeStatement(timeoutStatement, context, fixer);
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
],
|
|
369
|
+
};
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
373
|
+
const create = context => {
|
|
374
|
+
context.on('VariableDeclarator', node => createProblem(node, context));
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
378
|
+
const config = {
|
|
379
|
+
create,
|
|
380
|
+
meta: {
|
|
381
|
+
type: 'suggestion',
|
|
382
|
+
docs: {
|
|
383
|
+
description: 'Prefer `AbortSignal.timeout()` over manually aborting an `AbortController` with `setTimeout()`.',
|
|
384
|
+
recommended: true,
|
|
385
|
+
},
|
|
386
|
+
hasSuggestions: true,
|
|
387
|
+
schema: [],
|
|
388
|
+
messages,
|
|
389
|
+
languages: [
|
|
390
|
+
'js/js',
|
|
391
|
+
],
|
|
392
|
+
},
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
export default config;
|