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.
Files changed (53) hide show
  1. package/package.json +3 -1
  2. package/readme.md +12 -0
  3. package/rules/ast/is-empty-object-expression.js +2 -2
  4. package/rules/comment-content.js +136 -18
  5. package/rules/consistent-boolean-name.js +31 -4
  6. package/rules/consistent-conditional-object-spread.js +9 -3
  7. package/rules/consistent-tuple-labels.js +49 -0
  8. package/rules/custom-error-definition.js +26 -7
  9. package/rules/index.js +12 -0
  10. package/rules/no-chained-comparison.js +1 -1
  11. package/rules/no-computed-property-existence-check.js +6 -0
  12. package/rules/no-incorrect-template-string-interpolation.js +82 -30
  13. package/rules/no-instanceof-builtins.js +59 -13
  14. package/rules/no-invalid-well-known-symbol-methods.js +205 -0
  15. package/rules/no-late-current-target-access.js +9 -71
  16. package/rules/no-late-event-control.js +127 -0
  17. package/rules/no-nonstandard-builtin-properties.js +2 -0
  18. package/rules/no-object-methods-with-collections.js +4 -289
  19. package/rules/no-return-array-push.js +20 -5
  20. package/rules/no-top-level-side-effects.js +8 -1
  21. package/rules/no-unnecessary-global-this.js +36 -1
  22. package/rules/no-unreadable-object-destructuring.js +15 -0
  23. package/rules/no-unsafe-string-replacement.js +31 -0
  24. package/rules/no-unused-array-method-return.js +11 -2
  25. package/rules/no-useless-boolean-cast.js +58 -1
  26. package/rules/no-useless-concat.js +27 -3
  27. package/rules/no-useless-else.js +16 -2
  28. package/rules/no-useless-iterator-to-array.js +165 -115
  29. package/rules/prefer-abort-signal-timeout.js +395 -0
  30. package/rules/prefer-aggregate-error.js +478 -0
  31. package/rules/prefer-at.js +5 -6
  32. package/rules/prefer-code-point.js +58 -8
  33. package/rules/prefer-continue.js +16 -0
  34. package/rules/prefer-dom-node-replace-children.js +353 -0
  35. package/rules/prefer-error-is-error.js +181 -0
  36. package/rules/prefer-has-check.js +1 -4
  37. package/rules/prefer-hoisting-branch-code.js +9 -7
  38. package/rules/prefer-iterator-concat.js +21 -1
  39. package/rules/prefer-minimal-ternary.js +15 -11
  40. package/rules/prefer-modern-dom-apis.js +1 -45
  41. package/rules/prefer-observer-apis.js +498 -0
  42. package/rules/prefer-promise-try.js +238 -0
  43. package/rules/prefer-set-methods.js +337 -0
  44. package/rules/prefer-toggle-attribute.js +293 -0
  45. package/rules/prefer-url-search-parameters.js +411 -0
  46. package/rules/shared/late-event-handler.js +82 -0
  47. package/rules/shared/name-replacements.js +6 -0
  48. package/rules/utils/builtin-collection-type.js +267 -0
  49. package/rules/utils/index.js +6 -0
  50. package/rules/utils/is-boolean.js +3 -1
  51. package/rules/utils/is-event.js +127 -0
  52. package/rules/utils/should-report-replace-children-receiver.js +210 -0
  53. 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;