eslint-plugin-unicorn 69.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 (44) hide show
  1. package/package.json +1 -1
  2. package/readme.md +10 -0
  3. package/rules/consistent-boolean-name.js +637 -36
  4. package/rules/expiring-todo-comments.js +23 -0
  5. package/rules/fix/{add-parenthesizes-to-return-or-throw-expression.js → add-parentheses-to-return-or-throw-expression.js} +1 -1
  6. package/rules/fix/index.js +1 -1
  7. package/rules/fix/switch-new-expression-to-call-expression.js +2 -2
  8. package/rules/index.js +10 -0
  9. package/rules/isolated-functions.js +156 -12
  10. package/rules/no-async-promise-finally.js +111 -0
  11. package/rules/no-collection-bracket-access.js +27 -4
  12. package/rules/no-manually-wrapped-comments.js +5 -1
  13. package/rules/no-negated-array-predicate.js +2 -2
  14. package/rules/no-negated-comparison.js +24 -61
  15. package/rules/no-negated-condition.js +2 -2
  16. package/rules/no-negation-in-equality-check.js +2 -2
  17. package/rules/no-non-function-verb-prefix.js +25 -2
  18. package/rules/no-return-array-push.js +9 -2
  19. package/rules/no-typeof-undefined.js +2 -2
  20. package/rules/no-unnecessary-array-flat-map.js +307 -0
  21. package/rules/no-unnecessary-await.js +2 -2
  22. package/rules/no-unnecessary-fetch-options.js +624 -0
  23. package/rules/no-unsafe-promise-all-settled-values.js +865 -0
  24. package/rules/no-useless-else.js +32 -1
  25. package/rules/no-useless-iterator-to-array.js +42 -16
  26. package/rules/no-useless-spread.js +2 -2
  27. package/rules/prefer-abort-signal-any.js +1391 -0
  28. package/rules/prefer-array-flat-map.js +6 -11
  29. package/rules/prefer-array-from-range.js +98 -0
  30. package/rules/prefer-block-statement-over-iife.js +159 -0
  31. package/rules/prefer-continue.js +6 -2
  32. package/rules/prefer-early-return.js +3 -1
  33. package/rules/prefer-group-by.js +576 -0
  34. package/rules/prefer-iterator-helpers.js +196 -0
  35. package/rules/prefer-number-coercion.js +1 -1
  36. package/rules/prefer-object-iterable-methods.js +57 -14
  37. package/rules/prefer-simplified-conditions.js +584 -0
  38. package/rules/prefer-spread.js +3 -0
  39. package/rules/prefer-string-raw.js +2 -2
  40. package/rules/shared/array-range.js +66 -0
  41. package/rules/shared/iterator-helpers.js +43 -1
  42. package/rules/utils/comparison.js +65 -0
  43. package/rules/utils/is-boolean.js +21 -6
  44. package/rules/utils/track-branch-exits.js +8 -7
@@ -1,10 +1,14 @@
1
1
  import {isRegExp} from 'node:util/types';
2
- import {getPropertyName} from '@eslint-community/eslint-utils';
2
+ import {findVariable, getPropertyName, getStaticValue} from '@eslint-community/eslint-utils';
3
3
  import {renameVariable} from './fix/index.js';
4
+ import resolveVariableName from './utils/resolve-variable-name.js';
4
5
  import {
5
6
  getAvailableVariableName,
6
7
  getScopes,
7
8
  getVariableIdentifiers,
9
+ isNullishType,
10
+ isTypeParameterType,
11
+ isUnknownType,
8
12
  upperFirst,
9
13
  } from './utils/index.js';
10
14
  import {
@@ -16,9 +20,11 @@ import {
16
20
  } from './utils/is-boolean.js';
17
21
 
18
22
  const MESSAGE_ID = 'consistent-boolean-name';
23
+ const MESSAGE_ID_NON_BOOLEAN_PREFIX = 'non-boolean-prefix';
19
24
  const MESSAGE_ID_SUGGESTION = 'rename';
20
25
  const messages = {
21
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.',
22
28
  [MESSAGE_ID_SUGGESTION]: 'Rename to `{{replacement}}`.',
23
29
  };
24
30
 
@@ -33,6 +39,8 @@ const defaultPrefixes = {
33
39
  were: true,
34
40
  did: true,
35
41
  will: true,
42
+ // `requireLogin()` reads like an action or assertion, not a boolean.
43
+ requires: true,
36
44
  };
37
45
 
38
46
  const getEnabledPrefixes = ({prefixes = {}} = {}) =>
@@ -46,6 +54,48 @@ const getEnabledPrefixes = ({prefixes = {}} = {}) =>
46
54
  const formatPrefixes = prefixes =>
47
55
  prefixes.map(prefix => `\`${prefix}\``).join(', ');
48
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
+ ]);
49
99
  const isUpperCase = string => string === string.toUpperCase();
50
100
  const stripLeadingUnderscores = name => name.replace(/^_+/, '');
51
101
 
@@ -103,12 +153,12 @@ function isIgnoredName(name, ignore) {
103
153
  });
104
154
  }
105
155
 
106
- function hasBooleanPrefix(name, prefixes) {
156
+ function getBooleanPrefix(name, prefixes) {
107
157
  name = stripLeadingUnderscores(name);
108
158
 
109
159
  for (const prefix of prefixes) {
110
160
  if (name.startsWith(`${prefix.toUpperCase()}_`)) {
111
- return true;
161
+ return prefix;
112
162
  }
113
163
 
114
164
  if (
@@ -116,11 +166,9 @@ function hasBooleanPrefix(name, prefixes) {
116
166
  && name.length > prefix.length
117
167
  && /[\dA-Z_]/.test(name[prefix.length])
118
168
  ) {
119
- return true;
169
+ return prefix;
120
170
  }
121
171
  }
122
-
123
- return false;
124
172
  }
125
173
 
126
174
  function getReplacementName(name, prefix) {
@@ -184,45 +232,435 @@ const isBooleanValue = (node, context) => isFunction(node)
184
232
  ? isBooleanFunction(node, context)
185
233
  : isBooleanFunctionReference(node, context) || isBooleanExpression(node, context);
186
234
 
187
- const isBooleanVariable = (variable, context) => {
188
- 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;
189
242
 
190
243
  if (
191
- variable.defs.length > 1
192
- && 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')
193
248
  ) {
194
- const overloadDefinitions = variable.defs.filter(definition => definition.node.type === 'TSDeclareFunction');
249
+ return;
250
+ }
195
251
 
196
- return overloadDefinitions.length > 0
197
- ? overloadDefinitions.every(definition => isBooleanFunctionDefinition(definition, context))
198
- : 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;
199
261
  }
200
262
 
201
- if (variable.defs.length !== 1) {
202
- 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;
203
273
  }
204
274
 
205
- const [definition] = variable.defs;
206
- const {name} = definition;
275
+ return states.every(state => state === boolean) ? boolean : nonBoolean;
276
+ }
207
277
 
208
- if (name?.type !== 'Identifier') {
209
- return false;
278
+ function combineVariableBooleanStates(states) {
279
+ if (states.includes(nonBoolean)) {
280
+ return nonBoolean;
210
281
  }
211
282
 
212
- if (!['Variable', 'Parameter', 'FunctionName'].includes(definition.type)) {
213
- return false;
283
+ return combineBooleanStates(states);
284
+ }
285
+
286
+ function getTypeBooleanState(type, checker, visitedTypes = new Set(), functionTypesAreBoolean = true) {
287
+ if (!type) {
288
+ return unknown;
214
289
  }
215
290
 
216
- // 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.
217
- if (definition.type === 'Variable' && definition.node.id.type !== 'Identifier') {
218
- 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;
543
+ }
544
+
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;
219
619
  }
220
620
 
221
- // A setter parameter's name is positional and dictated by the accessor, not chosen freely.
222
- if (definition.type === 'Parameter' && definition.node.parent?.kind === 'set') {
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) {
223
659
  return false;
224
660
  }
225
661
 
662
+ const {name} = definition;
663
+
226
664
  const scope = sourceCode.getScope(name);
227
665
 
228
666
  if (name.typeAnnotation) {
@@ -250,12 +688,91 @@ const isBooleanVariable = (variable, context) => {
250
688
  return isBooleanValue(definition.node.init, context);
251
689
  }
252
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
+
253
721
  if (definition.type === 'FunctionName') {
254
- return isBooleanFunctionDefinition(definition, context);
722
+ return functionValuesAreBoolean
723
+ ? getFunctionBooleanState(definition.node, context, visitedVariables)
724
+ : nonBoolean;
255
725
  }
256
726
 
257
- return false;
258
- };
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
+ }
259
776
 
260
777
  function getBooleanPropertyName(node, sourceCode) {
261
778
  if (
@@ -324,6 +841,53 @@ function isBooleanProperty(node, context) {
324
841
  return false;
325
842
  }
326
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
+
327
891
  function getSuggestions(variable, prefixes, context) {
328
892
  if (
329
893
  !shouldSuggestRename(variable)
@@ -414,11 +978,30 @@ const create = context => {
414
978
  }
415
979
 
416
980
  const checkVariable = variable => {
417
- // `hasBooleanPrefix` and `ignore` are cheap string checks, so run them before
418
- // the expensive `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
+
419
1003
  if (
420
- hasBooleanPrefix(variable.name, prefixes)
421
- || isIgnoredName(variable.name, ignore)
1004
+ getVariableBooleanState(variable, context) === nonBoolean
422
1005
  || !isBooleanVariable(variable, context)
423
1006
  ) {
424
1007
  return;
@@ -456,13 +1039,31 @@ const create = context => {
456
1039
 
457
1040
  if (
458
1041
  !name
459
- || hasBooleanPrefix(name, prefixes)
460
1042
  || isIgnoredName(name, ignore)
461
- || !isBooleanProperty(node, context)
462
1043
  ) {
463
1044
  return;
464
1045
  }
465
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
+
466
1067
  context.report({
467
1068
  node: node.key,
468
1069
  messageId: MESSAGE_ID,