eslint-plugin-unicorn 68.0.0 → 70.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 (88) hide show
  1. package/package.json +3 -1
  2. package/readme.md +22 -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 +665 -37
  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/expiring-todo-comments.js +23 -0
  10. package/rules/fix/{add-parenthesizes-to-return-or-throw-expression.js → add-parentheses-to-return-or-throw-expression.js} +1 -1
  11. package/rules/fix/index.js +1 -1
  12. package/rules/fix/switch-new-expression-to-call-expression.js +2 -2
  13. package/rules/index.js +22 -0
  14. package/rules/isolated-functions.js +156 -12
  15. package/rules/no-async-promise-finally.js +111 -0
  16. package/rules/no-chained-comparison.js +1 -1
  17. package/rules/no-collection-bracket-access.js +27 -4
  18. package/rules/no-computed-property-existence-check.js +6 -0
  19. package/rules/no-incorrect-template-string-interpolation.js +82 -30
  20. package/rules/no-instanceof-builtins.js +59 -13
  21. package/rules/no-invalid-well-known-symbol-methods.js +205 -0
  22. package/rules/no-late-current-target-access.js +9 -71
  23. package/rules/no-late-event-control.js +127 -0
  24. package/rules/no-manually-wrapped-comments.js +5 -1
  25. package/rules/no-negated-array-predicate.js +2 -2
  26. package/rules/no-negated-comparison.js +24 -61
  27. package/rules/no-negated-condition.js +2 -2
  28. package/rules/no-negation-in-equality-check.js +2 -2
  29. package/rules/no-non-function-verb-prefix.js +25 -2
  30. package/rules/no-nonstandard-builtin-properties.js +2 -0
  31. package/rules/no-object-methods-with-collections.js +4 -289
  32. package/rules/no-return-array-push.js +28 -6
  33. package/rules/no-top-level-side-effects.js +8 -1
  34. package/rules/no-typeof-undefined.js +2 -2
  35. package/rules/no-unnecessary-array-flat-map.js +307 -0
  36. package/rules/no-unnecessary-await.js +2 -2
  37. package/rules/no-unnecessary-fetch-options.js +624 -0
  38. package/rules/no-unnecessary-global-this.js +36 -1
  39. package/rules/no-unreadable-object-destructuring.js +15 -0
  40. package/rules/no-unsafe-promise-all-settled-values.js +865 -0
  41. package/rules/no-unsafe-string-replacement.js +31 -0
  42. package/rules/no-unused-array-method-return.js +11 -2
  43. package/rules/no-useless-boolean-cast.js +58 -1
  44. package/rules/no-useless-concat.js +27 -3
  45. package/rules/no-useless-else.js +48 -3
  46. package/rules/no-useless-iterator-to-array.js +194 -118
  47. package/rules/no-useless-spread.js +2 -2
  48. package/rules/prefer-abort-signal-any.js +1391 -0
  49. package/rules/prefer-abort-signal-timeout.js +395 -0
  50. package/rules/prefer-aggregate-error.js +478 -0
  51. package/rules/prefer-array-flat-map.js +6 -11
  52. package/rules/prefer-array-from-range.js +98 -0
  53. package/rules/prefer-at.js +5 -6
  54. package/rules/prefer-block-statement-over-iife.js +159 -0
  55. package/rules/prefer-code-point.js +58 -8
  56. package/rules/prefer-continue.js +21 -1
  57. package/rules/prefer-dom-node-replace-children.js +353 -0
  58. package/rules/prefer-early-return.js +3 -1
  59. package/rules/prefer-error-is-error.js +181 -0
  60. package/rules/prefer-group-by.js +576 -0
  61. package/rules/prefer-has-check.js +1 -4
  62. package/rules/prefer-hoisting-branch-code.js +9 -7
  63. package/rules/prefer-iterator-concat.js +21 -1
  64. package/rules/prefer-iterator-helpers.js +196 -0
  65. package/rules/prefer-minimal-ternary.js +15 -11
  66. package/rules/prefer-modern-dom-apis.js +1 -45
  67. package/rules/prefer-number-coercion.js +1 -1
  68. package/rules/prefer-object-iterable-methods.js +57 -14
  69. package/rules/prefer-observer-apis.js +498 -0
  70. package/rules/prefer-promise-try.js +238 -0
  71. package/rules/prefer-set-methods.js +337 -0
  72. package/rules/prefer-simplified-conditions.js +584 -0
  73. package/rules/prefer-spread.js +3 -0
  74. package/rules/prefer-string-raw.js +2 -2
  75. package/rules/prefer-toggle-attribute.js +293 -0
  76. package/rules/prefer-url-search-parameters.js +411 -0
  77. package/rules/shared/array-range.js +66 -0
  78. package/rules/shared/iterator-helpers.js +43 -1
  79. package/rules/shared/late-event-handler.js +82 -0
  80. package/rules/shared/name-replacements.js +6 -0
  81. package/rules/utils/builtin-collection-type.js +267 -0
  82. package/rules/utils/comparison.js +65 -0
  83. package/rules/utils/index.js +6 -0
  84. package/rules/utils/is-boolean.js +24 -7
  85. package/rules/utils/is-event.js +127 -0
  86. package/rules/utils/should-report-replace-children-receiver.js +210 -0
  87. package/rules/utils/track-branch-exits.js +8 -7
  88. package/rules/utils/type-helpers.js +166 -8
@@ -1,9 +1,14 @@
1
- import {getPropertyName} from '@eslint-community/eslint-utils';
1
+ import {isRegExp} from 'node:util/types';
2
+ import {findVariable, getPropertyName, getStaticValue} from '@eslint-community/eslint-utils';
2
3
  import {renameVariable} from './fix/index.js';
4
+ import resolveVariableName from './utils/resolve-variable-name.js';
3
5
  import {
4
6
  getAvailableVariableName,
5
7
  getScopes,
6
8
  getVariableIdentifiers,
9
+ isNullishType,
10
+ isTypeParameterType,
11
+ isUnknownType,
7
12
  upperFirst,
8
13
  } from './utils/index.js';
9
14
  import {
@@ -15,20 +20,27 @@ import {
15
20
  } from './utils/is-boolean.js';
16
21
 
17
22
  const MESSAGE_ID = 'consistent-boolean-name';
23
+ const MESSAGE_ID_NON_BOOLEAN_PREFIX = 'non-boolean-prefix';
18
24
  const MESSAGE_ID_SUGGESTION = 'rename';
19
25
  const messages = {
20
26
  [MESSAGE_ID]: 'Boolean name `{{name}}` should start with {{prefixes}}.',
27
+ [MESSAGE_ID_NON_BOOLEAN_PREFIX]: '`{{name}}` starts with `{{prefix}}`, so it should be boolean.',
21
28
  [MESSAGE_ID_SUGGESTION]: 'Rename to `{{replacement}}`.',
22
29
  };
23
30
 
24
31
  const defaultPrefixes = {
25
32
  is: true,
33
+ are: true,
26
34
  has: true,
35
+ have: true,
27
36
  can: true,
28
37
  should: true,
29
38
  was: true,
39
+ were: true,
30
40
  did: true,
31
41
  will: true,
42
+ // `requireLogin()` reads like an action or assertion, not a boolean.
43
+ requires: true,
32
44
  };
33
45
 
34
46
  const getEnabledPrefixes = ({prefixes = {}} = {}) =>
@@ -42,6 +54,48 @@ const getEnabledPrefixes = ({prefixes = {}} = {}) =>
42
54
  const formatPrefixes = prefixes =>
43
55
  prefixes.map(prefix => `\`${prefix}\``).join(', ');
44
56
 
57
+ const booleanBinaryOperators = new Set([
58
+ '>',
59
+ '>=',
60
+ '<',
61
+ '<=',
62
+ '==',
63
+ '===',
64
+ '!=',
65
+ '!==',
66
+ 'in',
67
+ 'instanceof',
68
+ ]);
69
+ const boolean = 'boolean';
70
+ const nonBoolean = 'non-boolean';
71
+ const unknown = 'unknown';
72
+ const nullishTypeAnnotationTypes = new Set([
73
+ 'TSNullKeyword',
74
+ 'TSUndefinedKeyword',
75
+ ]);
76
+ const unknownTypeAnnotationTypes = new Set([
77
+ 'TSAnyKeyword',
78
+ 'TSNeverKeyword',
79
+ 'TSUnknownKeyword',
80
+ ]);
81
+ const nonBooleanExpressionTypes = new Set([
82
+ 'ArrayExpression',
83
+ 'ObjectExpression',
84
+ 'ClassExpression',
85
+ 'NewExpression',
86
+ 'TemplateLiteral',
87
+ 'UpdateExpression',
88
+ ]);
89
+ const expressionWrapperTypes = new Set([
90
+ 'AwaitExpression',
91
+ 'TSNonNullExpression',
92
+ 'ParenthesizedExpression',
93
+ ]);
94
+ const typeScriptExpressionWrapperTypes = new Set([
95
+ 'TSAsExpression',
96
+ 'TSTypeAssertion',
97
+ 'TSSatisfiesExpression',
98
+ ]);
45
99
  const isUpperCase = string => string === string.toUpperCase();
46
100
  const stripLeadingUnderscores = name => name.replace(/^_+/, '');
47
101
 
@@ -83,17 +137,28 @@ function findParameter(parameters, identifier) {
83
137
  const prepareOptions = ({
84
138
  checkProperties = false,
85
139
  prefixes = {},
140
+ ignore = [],
86
141
  } = {}) => ({
87
142
  checkProperties,
88
143
  prefixes: getEnabledPrefixes({prefixes}),
144
+ ignore: ignore.map(pattern => isRegExp(pattern) ? pattern : new RegExp(pattern, 'u')),
89
145
  });
90
146
 
91
- function hasBooleanPrefix(name, prefixes) {
147
+ function isIgnoredName(name, ignore) {
148
+ return ignore.some(regexp => {
149
+ regexp.lastIndex = 0;
150
+ const isIgnored = regexp.test(name);
151
+ regexp.lastIndex = 0;
152
+ return isIgnored;
153
+ });
154
+ }
155
+
156
+ function getBooleanPrefix(name, prefixes) {
92
157
  name = stripLeadingUnderscores(name);
93
158
 
94
159
  for (const prefix of prefixes) {
95
160
  if (name.startsWith(`${prefix.toUpperCase()}_`)) {
96
- return true;
161
+ return prefix;
97
162
  }
98
163
 
99
164
  if (
@@ -101,11 +166,9 @@ function hasBooleanPrefix(name, prefixes) {
101
166
  && name.length > prefix.length
102
167
  && /[\dA-Z_]/.test(name[prefix.length])
103
168
  ) {
104
- return true;
169
+ return prefix;
105
170
  }
106
171
  }
107
-
108
- return false;
109
172
  }
110
173
 
111
174
  function getReplacementName(name, prefix) {
@@ -169,45 +232,435 @@ const isBooleanValue = (node, context) => isFunction(node)
169
232
  ? isBooleanFunction(node, context)
170
233
  : isBooleanFunctionReference(node, context) || isBooleanExpression(node, context);
171
234
 
172
- const isBooleanVariable = (variable, context) => {
173
- const {sourceCode} = context;
235
+ function getSupportedVariableDefinition(variable) {
236
+ if (variable.defs.length !== 1) {
237
+ return;
238
+ }
239
+
240
+ const [definition] = variable.defs;
241
+ const {name} = definition;
174
242
 
175
243
  if (
176
- variable.defs.length > 1
177
- && variable.defs.every(definition => definition.type === 'FunctionName')
244
+ name?.type !== 'Identifier'
245
+ || !['Variable', 'Parameter', 'FunctionName'].includes(definition.type)
246
+ || (definition.type === 'Variable' && definition.node.id.type !== 'Identifier')
247
+ || (definition.type === 'Parameter' && definition.node.parent?.kind === 'set')
178
248
  ) {
179
- const overloadDefinitions = variable.defs.filter(definition => definition.node.type === 'TSDeclareFunction');
249
+ return;
250
+ }
180
251
 
181
- return overloadDefinitions.length > 0
182
- ? overloadDefinitions.every(definition => isBooleanFunctionDefinition(definition, context))
183
- : variable.defs.every(definition => isBooleanFunctionDefinition(definition, context));
252
+ return definition;
253
+ }
254
+
255
+ function getFunctionDefinitions(variable) {
256
+ if (
257
+ variable.defs.length <= 1
258
+ || variable.defs.some(definition => definition.type !== 'FunctionName')
259
+ ) {
260
+ return;
184
261
  }
185
262
 
186
- if (variable.defs.length !== 1) {
187
- return false;
263
+ const overloadDefinitions = variable.defs.filter(definition => definition.node.type === 'TSDeclareFunction');
264
+ return overloadDefinitions.length > 0 ? overloadDefinitions : variable.defs;
265
+ }
266
+
267
+ function combineBooleanStates(states) {
268
+ if (
269
+ states.length === 0
270
+ || states.includes(unknown)
271
+ ) {
272
+ return unknown;
188
273
  }
189
274
 
190
- const [definition] = variable.defs;
191
- const {name} = definition;
275
+ return states.every(state => state === boolean) ? boolean : nonBoolean;
276
+ }
192
277
 
193
- if (name?.type !== 'Identifier') {
194
- return false;
278
+ function combineVariableBooleanStates(states) {
279
+ if (states.includes(nonBoolean)) {
280
+ return nonBoolean;
195
281
  }
196
282
 
197
- if (!['Variable', 'Parameter', 'FunctionName'].includes(definition.type)) {
198
- return false;
283
+ return combineBooleanStates(states);
284
+ }
285
+
286
+ function getTypeBooleanState(type, checker, visitedTypes = new Set(), functionTypesAreBoolean = true) {
287
+ if (!type) {
288
+ return unknown;
199
289
  }
200
290
 
201
- // Destructuring patterns (`const {completed} = task`, `const [enabled] = list`) are intentionally not checked, since renaming a destructured binding is more involved than renaming a plain identifier.
202
- if (definition.type === 'Variable' && definition.node.id.type !== 'Identifier') {
203
- return false;
291
+ if (
292
+ isUnknownType(type)
293
+ || type.intrinsicName === 'never'
294
+ ) {
295
+ return unknown;
296
+ }
297
+
298
+ if (visitedTypes.has(type)) {
299
+ return unknown;
300
+ }
301
+
302
+ visitedTypes.add(type);
303
+
304
+ if (isTypeParameterType(type)) {
305
+ const constraint = type.getConstraint();
306
+ const result = constraint ? getTypeBooleanState(constraint, checker, visitedTypes, functionTypesAreBoolean) : unknown;
307
+ visitedTypes.delete(type);
308
+ return result;
309
+ }
310
+
311
+ const nonNullableType = checker.getNonNullableType(type);
312
+ if (nonNullableType !== type) {
313
+ const result = getTypeBooleanState(nonNullableType, checker, visitedTypes, functionTypesAreBoolean);
314
+ visitedTypes.delete(type);
315
+ return result;
316
+ }
317
+
318
+ if (type.isUnion()) {
319
+ const result = combineBooleanStates(
320
+ type.types
321
+ .filter(type => !isNullishType(type))
322
+ .map(type => getTypeBooleanState(type, checker, visitedTypes, functionTypesAreBoolean)),
323
+ );
324
+ visitedTypes.delete(type);
325
+ return result;
326
+ }
327
+
328
+ const signatures = type.getCallSignatures();
329
+ if (signatures.length > 0) {
330
+ const result = functionTypesAreBoolean
331
+ ? combineBooleanStates(signatures.map(signature => getTypeBooleanState(signature.getReturnType(), checker, visitedTypes, false)))
332
+ : nonBoolean;
333
+ visitedTypes.delete(type);
334
+ return result;
335
+ }
336
+
337
+ const constraint = checker.getBaseConstraintOfType(type);
338
+ if (constraint && constraint !== type) {
339
+ const result = getTypeBooleanState(constraint, checker, visitedTypes, functionTypesAreBoolean);
340
+ visitedTypes.delete(type);
341
+ return result;
342
+ }
343
+
344
+ if (type.getProperties().length === 0) {
345
+ visitedTypes.delete(type);
346
+ return unknown;
347
+ }
348
+
349
+ const typeString = checker.typeToString(checker.getWidenedType(checker.getBaseTypeOfLiteralType(type)));
350
+ visitedTypes.delete(type);
351
+
352
+ return typeString === 'boolean' ? boolean : nonBoolean;
353
+ }
354
+
355
+ function getTypeInformationBooleanState(node, context, functionTypesAreBoolean = true) {
356
+ const {parserServices} = context.sourceCode;
357
+ if (!parserServices?.program) {
358
+ return unknown;
359
+ }
360
+
361
+ try {
362
+ return getTypeBooleanState(
363
+ parserServices.getTypeAtLocation(node),
364
+ parserServices.program.getTypeChecker(),
365
+ new Set(),
366
+ functionTypesAreBoolean,
367
+ );
368
+ } catch {
369
+ return unknown;
370
+ }
371
+ }
372
+
373
+ function getTypeReferenceName(typeName) {
374
+ if (typeName?.type === 'Identifier') {
375
+ return typeName.name;
376
+ }
377
+ }
378
+
379
+ const getTypeState = typeState => ({
380
+ visitedTypeReferenceNames: new Set(),
381
+ functionTypesAreBoolean: true,
382
+ ...typeState,
383
+ });
384
+
385
+ function getTypeMembersBooleanState(members, context, scope, typeState) {
386
+ const normalizedTypeState = getTypeState(typeState);
387
+ const callSignatures = members.filter(member => member.type === 'TSCallSignatureDeclaration');
388
+
389
+ if (callSignatures.length > 0) {
390
+ return normalizedTypeState.functionTypesAreBoolean
391
+ ? combineBooleanStates(callSignatures.map(member => getTypeAnnotationBooleanState(member.returnType, context, scope, {...normalizedTypeState, functionTypesAreBoolean: false})))
392
+ : nonBoolean;
393
+ }
394
+
395
+ return members.length > 0 ? nonBoolean : unknown;
396
+ }
397
+
398
+ function getTypeReferenceBooleanState(node, context, scope, typeState) {
399
+ const normalizedTypeState = getTypeState(typeState);
400
+ const {visitedTypeReferenceNames} = normalizedTypeState;
401
+ const name = getTypeReferenceName(node.typeName);
402
+ if (!name || visitedTypeReferenceNames.has(name)) {
403
+ return unknown;
404
+ }
405
+
406
+ visitedTypeReferenceNames.add(name);
407
+
408
+ const [definition] = resolveVariableName(name, scope)?.defs ?? [];
409
+ let result = unknown;
410
+
411
+ if (definition?.type === 'Type') {
412
+ const definitionScope = context.sourceCode.getScope(definition.node);
413
+
414
+ if (definition.node.type === 'TSTypeAliasDeclaration') {
415
+ result = getTypeAnnotationBooleanState(definition.node.typeAnnotation, context, definitionScope, normalizedTypeState);
416
+ } else if (definition.node.type === 'TSInterfaceDeclaration') {
417
+ result = getTypeMembersBooleanState(definition.node.body.body, context, definitionScope, normalizedTypeState);
418
+ }
419
+ }
420
+
421
+ visitedTypeReferenceNames.delete(name);
422
+ return result;
423
+ }
424
+
425
+ function getUnionTypeAnnotationBooleanState(node, context, scope, typeState) {
426
+ return combineBooleanStates(
427
+ node.types
428
+ .filter(type => !nullishTypeAnnotationTypes.has(type.type))
429
+ .map(type => getTypeAnnotationBooleanState(type, context, scope, typeState)),
430
+ );
431
+ }
432
+
433
+ function getSimpleTypeAnnotationBooleanState(node) {
434
+ if (!node) {
435
+ return unknown;
436
+ }
437
+
438
+ if (
439
+ nullishTypeAnnotationTypes.has(node.type)
440
+ || unknownTypeAnnotationTypes.has(node.type)
441
+ ) {
442
+ return unknown;
443
+ }
444
+
445
+ if (node.type === 'TSBooleanKeyword') {
446
+ return boolean;
447
+ }
448
+
449
+ if (node.type === 'TSLiteralType') {
450
+ return typeof node.literal.value === 'boolean' ? boolean : nonBoolean;
451
+ }
452
+
453
+ if (node.type === 'TSTypePredicate') {
454
+ return node.asserts ? nonBoolean : boolean;
455
+ }
456
+
457
+ if (node.type === 'TypeAnnotation') {
458
+ return node.typeAnnotation?.type === 'BooleanTypeAnnotation' ? boolean : nonBoolean;
459
+ }
460
+
461
+ return nonBoolean;
462
+ }
463
+
464
+ function getTypeAnnotationBooleanState(node, context, scope, typeState) {
465
+ const normalizedTypeState = getTypeState(typeState);
466
+
467
+ if (
468
+ node?.type === 'TSTypeAnnotation'
469
+ || node?.type === 'TSParenthesizedType'
470
+ ) {
471
+ return getTypeAnnotationBooleanState(node.typeAnnotation, context, scope, normalizedTypeState);
472
+ }
473
+
474
+ if (node?.type === 'TSFunctionType') {
475
+ return normalizedTypeState.functionTypesAreBoolean
476
+ ? getTypeAnnotationBooleanState(node.returnType, context, scope, {...normalizedTypeState, functionTypesAreBoolean: false})
477
+ : nonBoolean;
478
+ }
479
+
480
+ if (node?.type === 'TSUnionType') {
481
+ return getUnionTypeAnnotationBooleanState(node, context, scope, normalizedTypeState);
482
+ }
483
+
484
+ if (node?.type === 'TSTypeReference') {
485
+ return getTypeReferenceBooleanState(node, context, scope, normalizedTypeState);
486
+ }
487
+
488
+ if (node?.type === 'TSTypeLiteral') {
489
+ return getTypeMembersBooleanState(node.members, context, scope, normalizedTypeState);
490
+ }
491
+
492
+ return getSimpleTypeAnnotationBooleanState(node);
493
+ }
494
+
495
+ function getFunctionBooleanState(node, context, visitedVariables = new Set()) {
496
+ if (node.async || node.generator) {
497
+ return nonBoolean;
498
+ }
499
+
500
+ const stateFromTypeInformation = getTypeInformationBooleanState(node, context);
501
+ if (stateFromTypeInformation !== unknown) {
502
+ return stateFromTypeInformation;
503
+ }
504
+
505
+ const scope = context.sourceCode.getScope(node);
506
+ const stateFromReturnType = getTypeAnnotationBooleanState(node.returnType, context, scope, {functionTypesAreBoolean: false});
507
+ if (stateFromReturnType !== unknown) {
508
+ return stateFromReturnType;
509
+ }
510
+
511
+ if (!node.body) {
512
+ return unknown;
513
+ }
514
+
515
+ if (node.body.type === 'BlockStatement') {
516
+ if (node.body.body.length === 0) {
517
+ return nonBoolean;
518
+ }
519
+
520
+ if (
521
+ node.body.body.length === 1
522
+ && node.body.body[0].type === 'ReturnStatement'
523
+ ) {
524
+ return node.body.body[0].argument
525
+ ? getExpressionBooleanState(node.body.body[0].argument, context, visitedVariables, false)
526
+ : nonBoolean;
527
+ }
528
+ }
529
+
530
+ return node.type === 'ArrowFunctionExpression' && node.body.type !== 'BlockStatement'
531
+ ? getExpressionBooleanState(node.body, context, visitedVariables, false)
532
+ : unknown;
533
+ }
534
+
535
+ function getKnownIdentifierBooleanState(node, context, visitedVariables, functionValuesAreBoolean) {
536
+ const variable = findVariable(context.sourceCode.getScope(node), node);
537
+ return variable ? getVariableBooleanState(variable, context, visitedVariables, functionValuesAreBoolean) : unknown;
538
+ }
539
+
540
+ function getStaticExpressionBooleanState(node, scope) {
541
+ if (node.type === 'Identifier') {
542
+ return unknown;
204
543
  }
205
544
 
206
- // A setter parameter's name is positional and dictated by the accessor, not chosen freely.
207
- if (definition.type === 'Parameter' && definition.node.parent?.kind === 'set') {
545
+ const staticValue = getStaticValue(node, scope)?.value;
546
+
547
+ return staticValue === undefined
548
+ ? unknown
549
+ : (typeof staticValue === 'boolean' ? boolean : nonBoolean);
550
+ }
551
+
552
+ function getSimpleExpressionBooleanState(node) {
553
+ if (nonBooleanExpressionTypes.has(node.type)) {
554
+ return nonBoolean;
555
+ }
556
+
557
+ if (node.type === 'Literal') {
558
+ return node.value === null ? unknown : nonBoolean;
559
+ }
560
+
561
+ if (node.type === 'UnaryExpression') {
562
+ return ['!', 'delete'].includes(node.operator) ? boolean : nonBoolean;
563
+ }
564
+
565
+ if (node.type === 'BinaryExpression') {
566
+ return booleanBinaryOperators.has(node.operator) ? boolean : nonBoolean;
567
+ }
568
+
569
+ return unknown;
570
+ }
571
+
572
+ function getWrappedExpression(node) {
573
+ if (expressionWrapperTypes.has(node.type)) {
574
+ return node.argument ?? node.expression;
575
+ }
576
+
577
+ if (typeScriptExpressionWrapperTypes.has(node.type)) {
578
+ return node.expression;
579
+ }
580
+ }
581
+
582
+ function getDerivedExpressionBooleanState(node, context, visitedVariables, functionValuesAreBoolean) {
583
+ if (node.type === 'Identifier') {
584
+ return getKnownIdentifierBooleanState(node, context, visitedVariables, functionValuesAreBoolean);
585
+ }
586
+
587
+ const wrappedExpression = getWrappedExpression(node);
588
+ if (wrappedExpression) {
589
+ return getExpressionBooleanState(wrappedExpression, context, visitedVariables, functionValuesAreBoolean);
590
+ }
591
+
592
+ if (node.type === 'AssignmentExpression') {
593
+ return node.operator === '=' ? getExpressionBooleanState(node.right, context, visitedVariables, functionValuesAreBoolean) : unknown;
594
+ }
595
+
596
+ if (node.type === 'SequenceExpression') {
597
+ return getExpressionBooleanState(node.expressions.at(-1), context, visitedVariables, functionValuesAreBoolean);
598
+ }
599
+
600
+ if (node.type === 'ConditionalExpression') {
601
+ return combineBooleanStates([
602
+ getExpressionBooleanState(node.consequent, context, visitedVariables, functionValuesAreBoolean),
603
+ getExpressionBooleanState(node.alternate, context, visitedVariables, functionValuesAreBoolean),
604
+ ]);
605
+ }
606
+
607
+ return unknown;
608
+ }
609
+
610
+ function getExpressionBooleanState(node, context, visitedVariables = new Set(), functionValuesAreBoolean = true) {
611
+ if (!node) {
612
+ return unknown;
613
+ }
614
+
615
+ if (isFunction(node)) {
616
+ return functionValuesAreBoolean
617
+ ? getFunctionBooleanState(node, context, visitedVariables)
618
+ : nonBoolean;
619
+ }
620
+
621
+ const stateFromTypeInformation = getTypeInformationBooleanState(node, context, functionValuesAreBoolean);
622
+ if (stateFromTypeInformation !== unknown) {
623
+ return stateFromTypeInformation;
624
+ }
625
+
626
+ const scope = context.sourceCode.getScope(node);
627
+ const stateFromTypeAnnotation = getTypeAnnotationBooleanState(node.typeAnnotation, context, scope, {functionTypesAreBoolean: functionValuesAreBoolean});
628
+ if (stateFromTypeAnnotation !== unknown) {
629
+ return stateFromTypeAnnotation;
630
+ }
631
+
632
+ if (isBooleanExpression(node, context, visitedVariables)) {
633
+ return boolean;
634
+ }
635
+
636
+ const stateFromStaticValue = getStaticExpressionBooleanState(node, scope);
637
+ if (stateFromStaticValue !== unknown) {
638
+ return stateFromStaticValue;
639
+ }
640
+
641
+ const stateFromSimpleExpression = getSimpleExpressionBooleanState(node);
642
+ if (stateFromSimpleExpression !== unknown) {
643
+ return stateFromSimpleExpression;
644
+ }
645
+
646
+ return getDerivedExpressionBooleanState(node, context, visitedVariables, functionValuesAreBoolean);
647
+ }
648
+
649
+ const isBooleanVariable = (variable, context) => {
650
+ const {sourceCode} = context;
651
+
652
+ const functionDefinitions = getFunctionDefinitions(variable);
653
+ if (functionDefinitions) {
654
+ return functionDefinitions.every(definition => isBooleanFunctionDefinition(definition, context));
655
+ }
656
+
657
+ const definition = getSupportedVariableDefinition(variable);
658
+ if (!definition) {
208
659
  return false;
209
660
  }
210
661
 
662
+ const {name} = definition;
663
+
211
664
  const scope = sourceCode.getScope(name);
212
665
 
213
666
  if (name.typeAnnotation) {
@@ -216,6 +669,11 @@ const isBooleanVariable = (variable, context) => {
216
669
  }
217
670
 
218
671
  if (definition.type === 'Parameter') {
672
+ // Some parsers (such as Svelte's `{#each}` bindings) create `Parameter` definitions whose owner is not a function and has no `params` list.
673
+ if (!isFunction(definition.node)) {
674
+ return false;
675
+ }
676
+
219
677
  const parameter = findParameter(definition.node.params, name);
220
678
 
221
679
  return parameter?.type === 'AssignmentPattern'
@@ -230,12 +688,91 @@ const isBooleanVariable = (variable, context) => {
230
688
  return isBooleanValue(definition.node.init, context);
231
689
  }
232
690
 
691
+ return definition.type === 'FunctionName' && isBooleanFunctionDefinition(definition, context);
692
+ };
693
+
694
+ function getParameterBooleanState(definition, context, visitedVariables, functionValuesAreBoolean) {
695
+ if (!isFunction(definition.node)) {
696
+ return unknown;
697
+ }
698
+
699
+ const parameter = findParameter(definition.node.params, definition.name);
700
+
701
+ return parameter?.type === 'AssignmentPattern'
702
+ ? getExpressionBooleanState(parameter.right, context, visitedVariables, functionValuesAreBoolean)
703
+ : unknown;
704
+ }
705
+
706
+ function getDefinitionBooleanState(definition, context, visitedVariables, functionValuesAreBoolean) {
707
+ const scope = context.sourceCode.getScope(definition.name);
708
+ const stateFromTypeAnnotation = getTypeAnnotationBooleanState(definition.name.typeAnnotation, context, scope, {functionTypesAreBoolean: functionValuesAreBoolean});
709
+ if (stateFromTypeAnnotation !== unknown) {
710
+ return stateFromTypeAnnotation;
711
+ }
712
+
713
+ if (definition.type === 'Parameter') {
714
+ return getParameterBooleanState(definition, context, visitedVariables, functionValuesAreBoolean);
715
+ }
716
+
717
+ if (definition.type === 'Variable') {
718
+ return getExpressionBooleanState(definition.node.init, context, visitedVariables, functionValuesAreBoolean);
719
+ }
720
+
233
721
  if (definition.type === 'FunctionName') {
234
- return isBooleanFunctionDefinition(definition, context);
722
+ return functionValuesAreBoolean
723
+ ? getFunctionBooleanState(definition.node, context, visitedVariables)
724
+ : nonBoolean;
235
725
  }
236
726
 
237
- return false;
238
- };
727
+ return unknown;
728
+ }
729
+
730
+ function getVariableBooleanState(variable, context, visitedVariables = new Set(), functionValuesAreBoolean = true) {
731
+ if (!variable || visitedVariables.has(variable)) {
732
+ return unknown;
733
+ }
734
+
735
+ visitedVariables.add(variable);
736
+
737
+ const functionDefinitions = getFunctionDefinitions(variable);
738
+ const definition = getSupportedVariableDefinition(variable);
739
+ if (
740
+ !functionDefinitions
741
+ && !definition
742
+ ) {
743
+ visitedVariables.delete(variable);
744
+ return unknown;
745
+ }
746
+
747
+ let result;
748
+ if (functionDefinitions && !functionValuesAreBoolean) {
749
+ result = nonBoolean;
750
+ } else if (functionDefinitions) {
751
+ result = combineBooleanStates(functionDefinitions.map(definition => getFunctionBooleanState(definition.node, context, visitedVariables)));
752
+ } else {
753
+ result = getDefinitionBooleanState(definition, context, visitedVariables, functionValuesAreBoolean);
754
+ }
755
+
756
+ if (variable.references.some(reference => reference.writeExpr)) {
757
+ result = combineVariableBooleanStates([
758
+ result,
759
+ ...variable.references
760
+ .filter(reference => reference.writeExpr)
761
+ .map(reference => getExpressionBooleanState(reference.writeExpr, context, visitedVariables, functionValuesAreBoolean)),
762
+ ]);
763
+ }
764
+
765
+ if (
766
+ result === unknown
767
+ && functionValuesAreBoolean
768
+ && isBooleanVariable(variable, context)
769
+ ) {
770
+ result = boolean;
771
+ }
772
+
773
+ visitedVariables.delete(variable);
774
+ return result;
775
+ }
239
776
 
240
777
  function getBooleanPropertyName(node, sourceCode) {
241
778
  if (
@@ -304,6 +841,53 @@ function isBooleanProperty(node, context) {
304
841
  return false;
305
842
  }
306
843
 
844
+ function getExplicitPropertyBooleanState(node, context) {
845
+ const {sourceCode} = context;
846
+
847
+ if (node.type === 'Property') {
848
+ if (
849
+ node.parent.type !== 'ObjectExpression'
850
+ || node.shorthand
851
+ || node.kind === 'set'
852
+ ) {
853
+ return unknown;
854
+ }
855
+
856
+ return getExpressionBooleanState(node.value, context);
857
+ }
858
+
859
+ if (
860
+ node.type === 'MethodDefinition'
861
+ || node.type === 'TSAbstractMethodDefinition'
862
+ ) {
863
+ return ['constructor', 'set'].includes(node.kind) ? unknown : getFunctionBooleanState(node.value, context);
864
+ }
865
+
866
+ if (propertyDefinitionTypes.has(node.type)) {
867
+ const scope = sourceCode.getScope(node);
868
+ const stateFromTypeAnnotation = getTypeAnnotationBooleanState(node.typeAnnotation, context, scope);
869
+
870
+ return stateFromTypeAnnotation === unknown
871
+ ? getExpressionBooleanState(node.value, context)
872
+ : stateFromTypeAnnotation;
873
+ }
874
+
875
+ if (node.type === 'TSPropertySignature') {
876
+ return getTypeAnnotationBooleanState(node.typeAnnotation, context, sourceCode.getScope(node));
877
+ }
878
+
879
+ if (node.type === 'TSMethodSignature') {
880
+ return getTypeAnnotationBooleanState(node.returnType, context, sourceCode.getScope(node), {functionTypesAreBoolean: false});
881
+ }
882
+
883
+ return unknown;
884
+ }
885
+
886
+ function getPropertyBooleanState(node, context) {
887
+ const state = getExplicitPropertyBooleanState(node, context);
888
+ return state === unknown && isBooleanProperty(node, context) ? boolean : state;
889
+ }
890
+
307
891
  function getSuggestions(variable, prefixes, context) {
308
892
  if (
309
893
  !shouldSuggestRename(variable)
@@ -387,17 +971,37 @@ function getAutofix(variable, prefixes, context, suggestions) {
387
971
 
388
972
  /** @param {import('eslint').Rule.RuleContext} context */
389
973
  const create = context => {
390
- const {checkProperties, prefixes} = prepareOptions(context.options[0]);
974
+ const {checkProperties, prefixes, ignore} = prepareOptions(context.options[0]);
391
975
 
392
976
  if (prefixes.length === 0) {
393
977
  return;
394
978
  }
395
979
 
396
980
  const checkVariable = variable => {
397
- // `hasBooleanPrefix` is a cheap string check, so run it before the expensive
398
- // `isBooleanVariable` analysis.
981
+ if (isIgnoredName(variable.name, ignore)) {
982
+ return;
983
+ }
984
+
985
+ const booleanPrefix = getBooleanPrefix(variable.name, prefixes);
986
+ if (booleanPrefix) {
987
+ if (getVariableBooleanState(variable, context) === nonBoolean) {
988
+ const [definition] = variable.defs;
989
+
990
+ context.report({
991
+ node: definition.name,
992
+ messageId: MESSAGE_ID_NON_BOOLEAN_PREFIX,
993
+ data: {
994
+ name: variable.name,
995
+ prefix: booleanPrefix,
996
+ },
997
+ });
998
+ }
999
+
1000
+ return;
1001
+ }
1002
+
399
1003
  if (
400
- hasBooleanPrefix(variable.name, prefixes)
1004
+ getVariableBooleanState(variable, context) === nonBoolean
401
1005
  || !isBooleanVariable(variable, context)
402
1006
  ) {
403
1007
  return;
@@ -435,12 +1039,31 @@ const create = context => {
435
1039
 
436
1040
  if (
437
1041
  !name
438
- || hasBooleanPrefix(name, prefixes)
439
- || !isBooleanProperty(node, context)
1042
+ || isIgnoredName(name, ignore)
440
1043
  ) {
441
1044
  return;
442
1045
  }
443
1046
 
1047
+ const booleanPrefix = getBooleanPrefix(name, prefixes);
1048
+ if (booleanPrefix) {
1049
+ if (getPropertyBooleanState(node, context) === nonBoolean) {
1050
+ context.report({
1051
+ node: node.key,
1052
+ messageId: MESSAGE_ID_NON_BOOLEAN_PREFIX,
1053
+ data: {
1054
+ name,
1055
+ prefix: booleanPrefix,
1056
+ },
1057
+ });
1058
+ }
1059
+
1060
+ return;
1061
+ }
1062
+
1063
+ if (!isBooleanProperty(node, context)) {
1064
+ return;
1065
+ }
1066
+
444
1067
  context.report({
445
1068
  node: node.key,
446
1069
  messageId: MESSAGE_ID,
@@ -494,10 +1117,15 @@ const config = {
494
1117
  pattern: '^[a-z][a-zA-Z0-9]*$',
495
1118
  },
496
1119
  },
1120
+ ignore: {
1121
+ type: 'array',
1122
+ uniqueItems: true,
1123
+ description: 'Patterns to ignore.',
1124
+ },
497
1125
  },
498
1126
  },
499
1127
  ],
500
- defaultOptions: [{}],
1128
+ defaultOptions: [{ignore: []}],
501
1129
  messages,
502
1130
  },
503
1131
  };