eslint-plugin-unicorn 69.0.0 → 71.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 (124) hide show
  1. package/package.json +26 -25
  2. package/readme.md +15 -1
  3. package/rules/ast/call-or-new-expression.js +2 -8
  4. package/rules/ast/is-member-expression.js +2 -8
  5. package/rules/comment-content.js +4 -4
  6. package/rules/consistent-boolean-name.js +727 -42
  7. package/rules/consistent-compound-words.js +1 -2
  8. package/rules/consistent-json-file-read.js +2 -5
  9. package/rules/dom-node-dataset.js +3 -7
  10. package/rules/escape-case.js +4 -4
  11. package/rules/expiring-todo-comments.js +25 -2
  12. package/rules/explicit-length-check.js +3 -3
  13. package/rules/filename-case.js +4 -4
  14. package/rules/fix/{add-parenthesizes-to-return-or-throw-expression.js → add-parentheses-to-return-or-throw-expression.js} +1 -1
  15. package/rules/fix/index.js +1 -1
  16. package/rules/fix/remove-argument.js +34 -8
  17. package/rules/fix/remove-statement.js +1 -3
  18. package/rules/fix/switch-new-expression-to-call-expression.js +2 -2
  19. package/rules/id-match.js +2 -2
  20. package/rules/index.js +10 -0
  21. package/rules/isolated-functions.js +203 -19
  22. package/rules/name-replacements.js +1 -2
  23. package/rules/no-anonymous-default-export.js +1 -3
  24. package/rules/no-array-callback-reference.js +2 -10
  25. package/rules/no-async-promise-finally.js +111 -0
  26. package/rules/no-chained-comparison.js +1 -1
  27. package/rules/no-collection-bracket-access.js +27 -4
  28. package/rules/no-computed-property-existence-check.js +1 -4
  29. package/rules/no-declarations-before-early-exit.js +6 -5
  30. package/rules/no-error-property-assignment.js +3 -3
  31. package/rules/no-for-loop.js +4 -4
  32. package/rules/no-keyword-prefix.js +2 -2
  33. package/rules/no-manually-wrapped-comments.js +5 -1
  34. package/rules/no-mismatched-map-key.js +13 -20
  35. package/rules/no-named-default.js +2 -5
  36. package/rules/no-negated-array-predicate.js +6 -13
  37. package/rules/no-negated-comparison.js +31 -71
  38. package/rules/no-negated-condition.js +2 -2
  39. package/rules/no-negation-in-equality-check.js +4 -7
  40. package/rules/no-non-function-verb-prefix.js +25 -2
  41. package/rules/no-process-exit.js +6 -6
  42. package/rules/no-return-array-push.js +9 -2
  43. package/rules/no-static-only-class.js +2 -8
  44. package/rules/no-thenable.js +9 -15
  45. package/rules/no-typeof-undefined.js +2 -2
  46. package/rules/no-undeclared-class-members.js +1 -5
  47. package/rules/no-unnecessary-array-flat-map.js +308 -0
  48. package/rules/no-unnecessary-await.js +2 -2
  49. package/rules/no-unnecessary-fetch-options.js +624 -0
  50. package/rules/no-unnecessary-global-this.js +5 -9
  51. package/rules/no-unsafe-promise-all-settled-values.js +865 -0
  52. package/rules/no-unused-properties.js +0 -1
  53. package/rules/no-useless-coercion.js +1 -2
  54. package/rules/no-useless-else.js +32 -1
  55. package/rules/no-useless-iterator-to-array.js +42 -16
  56. package/rules/no-useless-spread.js +2 -2
  57. package/rules/no-useless-undefined.js +2 -2
  58. package/rules/prefer-abort-signal-any.js +1391 -0
  59. package/rules/prefer-array-find.js +2 -5
  60. package/rules/prefer-array-flat-map.js +6 -11
  61. package/rules/prefer-array-from-range.js +98 -0
  62. package/rules/prefer-bigint-literals.js +1 -0
  63. package/rules/prefer-block-statement-over-iife.js +159 -0
  64. package/rules/prefer-boolean-return.js +20 -3
  65. package/rules/prefer-continue.js +6 -2
  66. package/rules/prefer-default-parameters.js +2 -2
  67. package/rules/prefer-early-return.js +3 -1
  68. package/rules/prefer-else-if.js +5 -9
  69. package/rules/prefer-group-by.js +576 -0
  70. package/rules/prefer-has-check.js +6 -3
  71. package/rules/prefer-https.js +3 -3
  72. package/rules/prefer-identifier-import-export-specifiers.js +1 -3
  73. package/rules/prefer-iterator-helpers.js +196 -0
  74. package/rules/prefer-minimal-ternary.js +4 -4
  75. package/rules/prefer-number-coercion.js +1 -1
  76. package/rules/prefer-object-define-properties.js +1 -3
  77. package/rules/prefer-object-iterable-methods.js +57 -14
  78. package/rules/prefer-observer-apis.js +2 -5
  79. package/rules/prefer-private-class-fields.js +1 -3
  80. package/rules/prefer-simple-sort-comparator.js +6 -6
  81. package/rules/prefer-simplified-conditions.js +584 -0
  82. package/rules/prefer-single-object-destructuring.js +2 -5
  83. package/rules/prefer-single-replace.js +2 -2
  84. package/rules/prefer-spread.js +25 -32
  85. package/rules/prefer-string-pad-start-end.js +1 -6
  86. package/rules/prefer-string-raw.js +2 -2
  87. package/rules/prefer-string-repeat.js +16 -9
  88. package/rules/prefer-string-replace-all.js +1 -1
  89. package/rules/prefer-ternary.js +5 -5
  90. package/rules/prefer-toggle-attribute.js +3 -3
  91. package/rules/prefer-top-level-await.js +6 -1
  92. package/rules/prefer-uint8array-base64.js +3 -1
  93. package/rules/require-proxy-trap-boolean-return.js +2 -2
  94. package/rules/rule/to-eslint-create.js +1 -2
  95. package/rules/rule/to-eslint-listener.js +1 -3
  96. package/rules/rule/to-eslint-problem.js +1 -3
  97. package/rules/rule/to-eslint-rule-fixer.js +1 -3
  98. package/rules/rule/to-eslint-rule.js +1 -3
  99. package/rules/rule/to-eslint-rules.js +1 -3
  100. package/rules/rule/unicorn-context.js +1 -3
  101. package/rules/rule/unicorn-listeners.js +1 -3
  102. package/rules/shared/array-range.js +66 -0
  103. package/rules/shared/iterator-helpers.js +43 -1
  104. package/rules/shared/no-array-mutate-rule.js +1 -3
  105. package/rules/shared/regexp-escape.js +4 -4
  106. package/rules/utils/can-tokens-be-adjacent.js +95 -0
  107. package/rules/utils/comparison.js +65 -0
  108. package/rules/utils/escape-string.js +2 -8
  109. package/rules/utils/get-available-variable-name.js +2 -13
  110. package/rules/utils/get-precedence.js +112 -0
  111. package/rules/utils/get-token-store.js +1 -3
  112. package/rules/utils/global-reference-tracker.js +3 -6
  113. package/rules/utils/index.js +4 -0
  114. package/rules/utils/is-boolean.js +103 -30
  115. package/rules/utils/is-identifier-name.js +5 -0
  116. package/rules/utils/is-promise-type.js +1 -3
  117. package/rules/utils/is-react-hook-name.js +5 -0
  118. package/rules/utils/parentheses/get-parent-syntax-opening-parenthesis.js +1 -3
  119. package/rules/utils/parentheses/iterate-surrounding-parentheses.js +1 -1
  120. package/rules/utils/should-add-parentheses-to-await-expression-argument.js +4 -9
  121. package/rules/utils/should-add-parentheses-to-call-expression-callee.js +6 -12
  122. package/rules/utils/should-add-parentheses-to-unary-expression.js +7 -14
  123. package/rules/utils/to-location.js +1 -3
  124. package/rules/utils/track-branch-exits.js +8 -7
@@ -1,10 +1,16 @@
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
+ isReactHookName,
10
+ isNullishType,
11
+ isTypeParameterType,
12
+ isUnknownType,
13
+ lowerFirst,
8
14
  upperFirst,
9
15
  } from './utils/index.js';
10
16
  import {
@@ -13,12 +19,15 @@ import {
13
19
  isBooleanFunctionReference,
14
20
  isBooleanFunctionTypeAnnotation,
15
21
  isBooleanTypeAnnotation,
22
+ isFunctionTypeAnnotation,
16
23
  } from './utils/is-boolean.js';
17
24
 
18
25
  const MESSAGE_ID = 'consistent-boolean-name';
26
+ const MESSAGE_ID_NON_BOOLEAN_PREFIX = 'non-boolean-prefix';
19
27
  const MESSAGE_ID_SUGGESTION = 'rename';
20
28
  const messages = {
21
29
  [MESSAGE_ID]: 'Boolean name `{{name}}` should start with {{prefixes}}.',
30
+ [MESSAGE_ID_NON_BOOLEAN_PREFIX]: '`{{name}}` starts with `{{prefix}}`, so it should be boolean.',
22
31
  [MESSAGE_ID_SUGGESTION]: 'Rename to `{{replacement}}`.',
23
32
  };
24
33
 
@@ -33,6 +42,8 @@ const defaultPrefixes = {
33
42
  were: true,
34
43
  did: true,
35
44
  will: true,
45
+ // `requireLogin()` reads like an action or assertion, not a boolean.
46
+ requires: true,
36
47
  };
37
48
 
38
49
  const getEnabledPrefixes = ({prefixes = {}} = {}) =>
@@ -46,8 +57,51 @@ const getEnabledPrefixes = ({prefixes = {}} = {}) =>
46
57
  const formatPrefixes = prefixes =>
47
58
  prefixes.map(prefix => `\`${prefix}\``).join(', ');
48
59
 
60
+ const booleanBinaryOperators = new Set([
61
+ '>',
62
+ '>=',
63
+ '<',
64
+ '<=',
65
+ '==',
66
+ '===',
67
+ '!=',
68
+ '!==',
69
+ 'in',
70
+ 'instanceof',
71
+ ]);
72
+ const boolean = 'boolean';
73
+ const nonBoolean = 'non-boolean';
74
+ const unknown = 'unknown';
75
+ const nullishTypeAnnotationTypes = new Set([
76
+ 'TSNullKeyword',
77
+ 'TSUndefinedKeyword',
78
+ ]);
79
+ const unknownTypeAnnotationTypes = new Set([
80
+ 'TSAnyKeyword',
81
+ 'TSNeverKeyword',
82
+ 'TSUnknownKeyword',
83
+ ]);
84
+ const nonBooleanExpressionTypes = new Set([
85
+ 'ArrayExpression',
86
+ 'ObjectExpression',
87
+ 'ClassExpression',
88
+ 'NewExpression',
89
+ 'TemplateLiteral',
90
+ 'UpdateExpression',
91
+ ]);
92
+ const expressionWrapperTypes = new Set([
93
+ 'AwaitExpression',
94
+ 'TSNonNullExpression',
95
+ 'ParenthesizedExpression',
96
+ ]);
97
+ const typeScriptExpressionWrapperTypes = new Set([
98
+ 'TSAsExpression',
99
+ 'TSTypeAssertion',
100
+ 'TSSatisfiesExpression',
101
+ ]);
49
102
  const isUpperCase = string => string === string.toUpperCase();
50
103
  const stripLeadingUnderscores = name => name.replace(/^_+/, '');
104
+ const isScreamingCase = name => /^[A-Z][\dA-Z_]*$/.test(stripLeadingUnderscores(name));
51
105
 
52
106
  const isFunction = node => [
53
107
  'ArrowFunctionExpression',
@@ -103,12 +157,12 @@ function isIgnoredName(name, ignore) {
103
157
  });
104
158
  }
105
159
 
106
- function hasBooleanPrefix(name, prefixes) {
160
+ function getBooleanPrefix(name, prefixes) {
107
161
  name = stripLeadingUnderscores(name);
108
162
 
109
163
  for (const prefix of prefixes) {
110
164
  if (name.startsWith(`${prefix.toUpperCase()}_`)) {
111
- return true;
165
+ return prefix;
112
166
  }
113
167
 
114
168
  if (
@@ -116,11 +170,9 @@ function hasBooleanPrefix(name, prefixes) {
116
170
  && name.length > prefix.length
117
171
  && /[\dA-Z_]/.test(name[prefix.length])
118
172
  ) {
119
- return true;
173
+ return prefix;
120
174
  }
121
175
  }
122
-
123
- return false;
124
176
  }
125
177
 
126
178
  function getReplacementName(name, prefix) {
@@ -132,6 +184,18 @@ function getReplacementName(name, prefix) {
132
184
  : `${leadingUnderscores}${prefix}${upperFirst(nameWithoutLeadingUnderscores)}`;
133
185
  }
134
186
 
187
+ function getReactHookReplacementName({name, nameForPrefixCheck}, prefix) {
188
+ if (name === nameForPrefixCheck) {
189
+ return getReplacementName(name, prefix);
190
+ }
191
+
192
+ const hookName = isScreamingCase(nameForPrefixCheck)
193
+ ? getReplacementName(nameForPrefixCheck, prefix)
194
+ : `${prefix}${upperFirst(nameForPrefixCheck)}`;
195
+
196
+ return `use${upperFirst(hookName)}`;
197
+ }
198
+
135
199
  const isExportedIdentifier = identifier => {
136
200
  if (
137
201
  identifier.parent.type === 'VariableDeclarator'
@@ -175,6 +239,23 @@ const shouldSuggestRename = variable => getVariableIdentifiers(variable)
175
239
  && identifier.type !== 'JSXIdentifier',
176
240
  );
177
241
 
242
+ function isDestructuredDefinition(definition) {
243
+ for (let node = definition.name; node && node !== definition.node; node = node.parent) {
244
+ if (
245
+ node.type === 'ObjectPattern'
246
+ || node.type === 'ArrayPattern'
247
+ ) {
248
+ return true;
249
+ }
250
+ }
251
+
252
+ return false;
253
+ }
254
+
255
+ const isDestructuredVariable = variable =>
256
+ variable.defs.length > 0
257
+ && variable.defs.every(definition => isDestructuredDefinition(definition));
258
+
178
259
  const isBooleanFunctionDefinition = (definition, context) =>
179
260
  definition.type === 'FunctionName'
180
261
  && isFunction(definition.node)
@@ -184,45 +265,470 @@ const isBooleanValue = (node, context) => isFunction(node)
184
265
  ? isBooleanFunction(node, context)
185
266
  : isBooleanFunctionReference(node, context) || isBooleanExpression(node, context);
186
267
 
187
- const isBooleanVariable = (variable, context) => {
188
- const {sourceCode} = context;
268
+ function getSupportedVariableDefinition(variable) {
269
+ if (variable.defs.length !== 1) {
270
+ return;
271
+ }
272
+
273
+ const [definition] = variable.defs;
274
+ const {name} = definition;
189
275
 
190
276
  if (
191
- variable.defs.length > 1
192
- && variable.defs.every(definition => definition.type === 'FunctionName')
277
+ name?.type !== 'Identifier'
278
+ || !['Variable', 'Parameter', 'FunctionName'].includes(definition.type)
279
+ || (definition.type === 'Variable' && definition.node.id.type !== 'Identifier')
280
+ || (definition.type === 'Parameter' && definition.node.parent?.kind === 'set')
193
281
  ) {
194
- const overloadDefinitions = variable.defs.filter(definition => definition.node.type === 'TSDeclareFunction');
282
+ return;
283
+ }
195
284
 
196
- return overloadDefinitions.length > 0
197
- ? overloadDefinitions.every(definition => isBooleanFunctionDefinition(definition, context))
198
- : variable.defs.every(definition => isBooleanFunctionDefinition(definition, context));
285
+ return definition;
286
+ }
287
+
288
+ function getFunctionDefinitions(variable) {
289
+ if (
290
+ variable.defs.length <= 1
291
+ || variable.defs.some(definition => definition.type !== 'FunctionName')
292
+ ) {
293
+ return;
199
294
  }
200
295
 
201
- if (variable.defs.length !== 1) {
296
+ const overloadDefinitions = variable.defs.filter(definition => definition.node.type === 'TSDeclareFunction');
297
+ return overloadDefinitions.length > 0 ? overloadDefinitions : variable.defs;
298
+ }
299
+
300
+ function isReactHookFunctionBinding(variable, context) {
301
+ if (
302
+ variable.name === 'use'
303
+ || !isReactHookName(variable.name)
304
+ ) {
202
305
  return false;
203
306
  }
204
307
 
205
- const [definition] = variable.defs;
206
- const {name} = definition;
308
+ if (getFunctionDefinitions(variable)) {
309
+ return true;
310
+ }
207
311
 
208
- if (name?.type !== 'Identifier') {
209
- return false;
312
+ const definition = getSupportedVariableDefinition(variable);
313
+
314
+ if (definition?.type === 'FunctionName') {
315
+ return isFunction(definition.node);
210
316
  }
211
317
 
212
- if (!['Variable', 'Parameter', 'FunctionName'].includes(definition.type)) {
213
- return false;
318
+ return definition?.type === 'Variable'
319
+ && (
320
+ isFunction(definition.node.init)
321
+ || isFunctionTypeAnnotation(definition.name.typeAnnotation, context, context.sourceCode.getScope(definition.name))
322
+ );
323
+ }
324
+
325
+ function getNameForPrefixCheck(variable, context) {
326
+ if (!isReactHookFunctionBinding(variable, context)) {
327
+ return variable.name;
214
328
  }
215
329
 
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;
330
+ const hookName = variable.name.slice(3);
331
+
332
+ return isScreamingCase(hookName) ? hookName : lowerFirst(hookName);
333
+ }
334
+
335
+ function combineBooleanStates(states) {
336
+ if (
337
+ states.length === 0
338
+ || states.includes(unknown)
339
+ ) {
340
+ return unknown;
341
+ }
342
+
343
+ return states.every(state => state === boolean) ? boolean : nonBoolean;
344
+ }
345
+
346
+ function combineVariableBooleanStates(states) {
347
+ if (states.includes(nonBoolean)) {
348
+ return nonBoolean;
349
+ }
350
+
351
+ return combineBooleanStates(states);
352
+ }
353
+
354
+ function getTypeBooleanState(type, checker, visitedTypes = new Set(), functionTypesAreBoolean = true) {
355
+ if (!type) {
356
+ return unknown;
357
+ }
358
+
359
+ if (
360
+ isUnknownType(type)
361
+ || type.intrinsicName === 'never'
362
+ ) {
363
+ return unknown;
364
+ }
365
+
366
+ if (visitedTypes.has(type)) {
367
+ return unknown;
368
+ }
369
+
370
+ visitedTypes.add(type);
371
+
372
+ if (isTypeParameterType(type)) {
373
+ const constraint = type.getConstraint();
374
+ const result = constraint ? getTypeBooleanState(constraint, checker, visitedTypes, functionTypesAreBoolean) : unknown;
375
+ visitedTypes.delete(type);
376
+ return result;
377
+ }
378
+
379
+ const nonNullableType = checker.getNonNullableType(type);
380
+ if (nonNullableType !== type) {
381
+ const result = getTypeBooleanState(nonNullableType, checker, visitedTypes, functionTypesAreBoolean);
382
+ visitedTypes.delete(type);
383
+ return result;
384
+ }
385
+
386
+ if (type.isUnion()) {
387
+ const result = combineBooleanStates(
388
+ type.types
389
+ .filter(type => !isNullishType(type))
390
+ .map(type => getTypeBooleanState(type, checker, visitedTypes, functionTypesAreBoolean)),
391
+ );
392
+ visitedTypes.delete(type);
393
+ return result;
394
+ }
395
+
396
+ const signatures = type.getCallSignatures();
397
+ if (signatures.length > 0) {
398
+ const result = functionTypesAreBoolean
399
+ ? combineBooleanStates(signatures.map(signature => getTypeBooleanState(signature.getReturnType(), checker, visitedTypes, false)))
400
+ : nonBoolean;
401
+ visitedTypes.delete(type);
402
+ return result;
403
+ }
404
+
405
+ const constraint = checker.getBaseConstraintOfType(type);
406
+ if (constraint && constraint !== type) {
407
+ const result = getTypeBooleanState(constraint, checker, visitedTypes, functionTypesAreBoolean);
408
+ visitedTypes.delete(type);
409
+ return result;
410
+ }
411
+
412
+ if (type.getProperties().length === 0) {
413
+ visitedTypes.delete(type);
414
+ return unknown;
415
+ }
416
+
417
+ const typeString = checker.typeToString(checker.getWidenedType(checker.getBaseTypeOfLiteralType(type)));
418
+ visitedTypes.delete(type);
419
+
420
+ return typeString === 'boolean' ? boolean : nonBoolean;
421
+ }
422
+
423
+ function getTypeInformationBooleanState(node, context, functionTypesAreBoolean = true) {
424
+ const {parserServices} = context.sourceCode;
425
+ if (!parserServices?.program) {
426
+ return unknown;
427
+ }
428
+
429
+ try {
430
+ return getTypeBooleanState(
431
+ parserServices.getTypeAtLocation(node),
432
+ parserServices.program.getTypeChecker(),
433
+ new Set(),
434
+ functionTypesAreBoolean,
435
+ );
436
+ } catch {
437
+ return unknown;
438
+ }
439
+ }
440
+
441
+ function getTypeReferenceName(typeName) {
442
+ if (typeName?.type === 'Identifier') {
443
+ return typeName.name;
444
+ }
445
+ }
446
+
447
+ const getTypeState = typeState => ({
448
+ visitedTypeReferenceNames: new Set(),
449
+ functionTypesAreBoolean: true,
450
+ ...typeState,
451
+ });
452
+
453
+ function getTypeMembersBooleanState(members, context, scope, typeState) {
454
+ const normalizedTypeState = getTypeState(typeState);
455
+ const callSignatures = members.filter(member => member.type === 'TSCallSignatureDeclaration');
456
+
457
+ if (callSignatures.length > 0) {
458
+ return normalizedTypeState.functionTypesAreBoolean
459
+ ? combineBooleanStates(callSignatures.map(member => getTypeAnnotationBooleanState(member.returnType, context, scope, {...normalizedTypeState, functionTypesAreBoolean: false})))
460
+ : nonBoolean;
461
+ }
462
+
463
+ return members.length > 0 ? nonBoolean : unknown;
464
+ }
465
+
466
+ function getTypeReferenceBooleanState(node, context, scope, typeState) {
467
+ const normalizedTypeState = getTypeState(typeState);
468
+ const {visitedTypeReferenceNames} = normalizedTypeState;
469
+ const name = getTypeReferenceName(node.typeName);
470
+ if (!name || visitedTypeReferenceNames.has(name)) {
471
+ return unknown;
472
+ }
473
+
474
+ visitedTypeReferenceNames.add(name);
475
+
476
+ const [definition] = resolveVariableName(name, scope)?.defs ?? [];
477
+ let result = unknown;
478
+
479
+ if (definition?.type === 'Type') {
480
+ const definitionScope = context.sourceCode.getScope(definition.node);
481
+
482
+ if (definition.node.type === 'TSTypeAliasDeclaration') {
483
+ result = getTypeAnnotationBooleanState(definition.node.typeAnnotation, context, definitionScope, normalizedTypeState);
484
+ } else if (definition.node.type === 'TSInterfaceDeclaration') {
485
+ result = getTypeMembersBooleanState(definition.node.body.body, context, definitionScope, normalizedTypeState);
486
+ }
487
+ }
488
+
489
+ visitedTypeReferenceNames.delete(name);
490
+ return result;
491
+ }
492
+
493
+ function getUnionTypeAnnotationBooleanState(node, context, scope, typeState) {
494
+ return combineBooleanStates(
495
+ node.types
496
+ .filter(type => !nullishTypeAnnotationTypes.has(type.type))
497
+ .map(type => getTypeAnnotationBooleanState(type, context, scope, typeState)),
498
+ );
499
+ }
500
+
501
+ function getSimpleTypeAnnotationBooleanState(node) {
502
+ if (!node) {
503
+ return unknown;
504
+ }
505
+
506
+ if (
507
+ nullishTypeAnnotationTypes.has(node.type)
508
+ || unknownTypeAnnotationTypes.has(node.type)
509
+ ) {
510
+ return unknown;
511
+ }
512
+
513
+ if (node.type === 'TSBooleanKeyword') {
514
+ return boolean;
515
+ }
516
+
517
+ if (node.type === 'TSLiteralType') {
518
+ return typeof node.literal.value === 'boolean' ? boolean : nonBoolean;
519
+ }
520
+
521
+ if (node.type === 'TSTypePredicate') {
522
+ return node.asserts ? nonBoolean : boolean;
523
+ }
524
+
525
+ if (node.type === 'TypeAnnotation') {
526
+ return node.typeAnnotation?.type === 'BooleanTypeAnnotation' ? boolean : nonBoolean;
527
+ }
528
+
529
+ return nonBoolean;
530
+ }
531
+
532
+ function getTypeAnnotationBooleanState(node, context, scope, typeState) {
533
+ const normalizedTypeState = getTypeState(typeState);
534
+
535
+ if (
536
+ node?.type === 'TSTypeAnnotation'
537
+ || node?.type === 'TSParenthesizedType'
538
+ ) {
539
+ return getTypeAnnotationBooleanState(node.typeAnnotation, context, scope, normalizedTypeState);
540
+ }
541
+
542
+ if (node?.type === 'TSFunctionType') {
543
+ return normalizedTypeState.functionTypesAreBoolean
544
+ ? getTypeAnnotationBooleanState(node.returnType, context, scope, {...normalizedTypeState, functionTypesAreBoolean: false})
545
+ : nonBoolean;
546
+ }
547
+
548
+ if (node?.type === 'TSUnionType') {
549
+ return getUnionTypeAnnotationBooleanState(node, context, scope, normalizedTypeState);
550
+ }
551
+
552
+ if (node?.type === 'TSTypeReference') {
553
+ return getTypeReferenceBooleanState(node, context, scope, normalizedTypeState);
554
+ }
555
+
556
+ if (node?.type === 'TSTypeLiteral') {
557
+ return getTypeMembersBooleanState(node.members, context, scope, normalizedTypeState);
558
+ }
559
+
560
+ return getSimpleTypeAnnotationBooleanState(node);
561
+ }
562
+
563
+ function getFunctionBooleanState(node, context, visitedVariables = new Set()) {
564
+ if (node.async || node.generator) {
565
+ return nonBoolean;
566
+ }
567
+
568
+ const stateFromTypeInformation = getTypeInformationBooleanState(node, context);
569
+ if (stateFromTypeInformation !== unknown) {
570
+ return stateFromTypeInformation;
571
+ }
572
+
573
+ const scope = context.sourceCode.getScope(node);
574
+ const stateFromReturnType = getTypeAnnotationBooleanState(node.returnType, context, scope, {functionTypesAreBoolean: false});
575
+ if (stateFromReturnType !== unknown) {
576
+ return stateFromReturnType;
577
+ }
578
+
579
+ if (!node.body) {
580
+ return unknown;
581
+ }
582
+
583
+ if (node.body.type === 'BlockStatement') {
584
+ if (node.body.body.length === 0) {
585
+ return nonBoolean;
586
+ }
587
+
588
+ if (
589
+ node.body.body.length === 1
590
+ && node.body.body[0].type === 'ReturnStatement'
591
+ ) {
592
+ return node.body.body[0].argument
593
+ ? getExpressionBooleanState(node.body.body[0].argument, context, visitedVariables, false)
594
+ : nonBoolean;
595
+ }
596
+ }
597
+
598
+ return node.type === 'ArrowFunctionExpression' && node.body.type !== 'BlockStatement'
599
+ ? getExpressionBooleanState(node.body, context, visitedVariables, false)
600
+ : unknown;
601
+ }
602
+
603
+ function getKnownIdentifierBooleanState(node, context, visitedVariables, functionValuesAreBoolean) {
604
+ const variable = findVariable(context.sourceCode.getScope(node), node);
605
+ return variable ? getVariableBooleanState(variable, context, visitedVariables, functionValuesAreBoolean) : unknown;
606
+ }
607
+
608
+ function getStaticExpressionBooleanState(node, scope) {
609
+ if (node.type === 'Identifier') {
610
+ return unknown;
611
+ }
612
+
613
+ const staticValue = getStaticValue(node, scope)?.value;
614
+
615
+ return staticValue === undefined
616
+ ? unknown
617
+ : (typeof staticValue === 'boolean' ? boolean : nonBoolean);
618
+ }
619
+
620
+ function getSimpleExpressionBooleanState(node) {
621
+ if (nonBooleanExpressionTypes.has(node.type)) {
622
+ return nonBoolean;
623
+ }
624
+
625
+ if (node.type === 'Literal') {
626
+ return node.value === null ? unknown : nonBoolean;
627
+ }
628
+
629
+ if (node.type === 'UnaryExpression') {
630
+ return ['!', 'delete'].includes(node.operator) ? boolean : nonBoolean;
631
+ }
632
+
633
+ if (node.type === 'BinaryExpression') {
634
+ return booleanBinaryOperators.has(node.operator) ? boolean : nonBoolean;
635
+ }
636
+
637
+ return unknown;
638
+ }
639
+
640
+ function getWrappedExpression(node) {
641
+ if (expressionWrapperTypes.has(node.type)) {
642
+ return node.argument ?? node.expression;
643
+ }
644
+
645
+ if (typeScriptExpressionWrapperTypes.has(node.type)) {
646
+ return node.expression;
647
+ }
648
+ }
649
+
650
+ function getDerivedExpressionBooleanState(node, context, visitedVariables, functionValuesAreBoolean) {
651
+ if (node.type === 'Identifier') {
652
+ return getKnownIdentifierBooleanState(node, context, visitedVariables, functionValuesAreBoolean);
653
+ }
654
+
655
+ const wrappedExpression = getWrappedExpression(node);
656
+ if (wrappedExpression) {
657
+ return getExpressionBooleanState(wrappedExpression, context, visitedVariables, functionValuesAreBoolean);
658
+ }
659
+
660
+ if (node.type === 'AssignmentExpression') {
661
+ return node.operator === '=' ? getExpressionBooleanState(node.right, context, visitedVariables, functionValuesAreBoolean) : unknown;
219
662
  }
220
663
 
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') {
664
+ if (node.type === 'SequenceExpression') {
665
+ return getExpressionBooleanState(node.expressions.at(-1), context, visitedVariables, functionValuesAreBoolean);
666
+ }
667
+
668
+ if (node.type === 'ConditionalExpression') {
669
+ return combineBooleanStates([
670
+ getExpressionBooleanState(node.consequent, context, visitedVariables, functionValuesAreBoolean),
671
+ getExpressionBooleanState(node.alternate, context, visitedVariables, functionValuesAreBoolean),
672
+ ]);
673
+ }
674
+
675
+ return unknown;
676
+ }
677
+
678
+ function getExpressionBooleanState(node, context, visitedVariables = new Set(), functionValuesAreBoolean = true) {
679
+ if (!node) {
680
+ return unknown;
681
+ }
682
+
683
+ if (isFunction(node)) {
684
+ return functionValuesAreBoolean
685
+ ? getFunctionBooleanState(node, context, visitedVariables)
686
+ : nonBoolean;
687
+ }
688
+
689
+ const stateFromTypeInformation = getTypeInformationBooleanState(node, context, functionValuesAreBoolean);
690
+ if (stateFromTypeInformation !== unknown) {
691
+ return stateFromTypeInformation;
692
+ }
693
+
694
+ const scope = context.sourceCode.getScope(node);
695
+ const stateFromTypeAnnotation = getTypeAnnotationBooleanState(node.typeAnnotation, context, scope, {functionTypesAreBoolean: functionValuesAreBoolean});
696
+ if (stateFromTypeAnnotation !== unknown) {
697
+ return stateFromTypeAnnotation;
698
+ }
699
+
700
+ if (isBooleanExpression(node, context, visitedVariables)) {
701
+ return boolean;
702
+ }
703
+
704
+ const stateFromStaticValue = getStaticExpressionBooleanState(node, scope);
705
+ if (stateFromStaticValue !== unknown) {
706
+ return stateFromStaticValue;
707
+ }
708
+
709
+ const stateFromSimpleExpression = getSimpleExpressionBooleanState(node);
710
+ if (stateFromSimpleExpression !== unknown) {
711
+ return stateFromSimpleExpression;
712
+ }
713
+
714
+ return getDerivedExpressionBooleanState(node, context, visitedVariables, functionValuesAreBoolean);
715
+ }
716
+
717
+ const isBooleanVariable = (variable, context) => {
718
+ const {sourceCode} = context;
719
+
720
+ const functionDefinitions = getFunctionDefinitions(variable);
721
+ if (functionDefinitions) {
722
+ return functionDefinitions.every(definition => isBooleanFunctionDefinition(definition, context));
723
+ }
724
+
725
+ const definition = getSupportedVariableDefinition(variable);
726
+ if (!definition) {
223
727
  return false;
224
728
  }
225
729
 
730
+ const {name} = definition;
731
+
226
732
  const scope = sourceCode.getScope(name);
227
733
 
228
734
  if (name.typeAnnotation) {
@@ -250,12 +756,91 @@ const isBooleanVariable = (variable, context) => {
250
756
  return isBooleanValue(definition.node.init, context);
251
757
  }
252
758
 
759
+ return definition.type === 'FunctionName' && isBooleanFunctionDefinition(definition, context);
760
+ };
761
+
762
+ function getParameterBooleanState(definition, context, visitedVariables, functionValuesAreBoolean) {
763
+ if (!isFunction(definition.node)) {
764
+ return unknown;
765
+ }
766
+
767
+ const parameter = findParameter(definition.node.params, definition.name);
768
+
769
+ return parameter?.type === 'AssignmentPattern'
770
+ ? getExpressionBooleanState(parameter.right, context, visitedVariables, functionValuesAreBoolean)
771
+ : unknown;
772
+ }
773
+
774
+ function getDefinitionBooleanState(definition, context, visitedVariables, functionValuesAreBoolean) {
775
+ const scope = context.sourceCode.getScope(definition.name);
776
+ const stateFromTypeAnnotation = getTypeAnnotationBooleanState(definition.name.typeAnnotation, context, scope, {functionTypesAreBoolean: functionValuesAreBoolean});
777
+ if (stateFromTypeAnnotation !== unknown) {
778
+ return stateFromTypeAnnotation;
779
+ }
780
+
781
+ if (definition.type === 'Parameter') {
782
+ return getParameterBooleanState(definition, context, visitedVariables, functionValuesAreBoolean);
783
+ }
784
+
785
+ if (definition.type === 'Variable') {
786
+ return getExpressionBooleanState(definition.node.init, context, visitedVariables, functionValuesAreBoolean);
787
+ }
788
+
253
789
  if (definition.type === 'FunctionName') {
254
- return isBooleanFunctionDefinition(definition, context);
790
+ return functionValuesAreBoolean
791
+ ? getFunctionBooleanState(definition.node, context, visitedVariables)
792
+ : nonBoolean;
255
793
  }
256
794
 
257
- return false;
258
- };
795
+ return unknown;
796
+ }
797
+
798
+ function getVariableBooleanState(variable, context, visitedVariables = new Set(), functionValuesAreBoolean = true) {
799
+ if (!variable || visitedVariables.has(variable)) {
800
+ return unknown;
801
+ }
802
+
803
+ visitedVariables.add(variable);
804
+
805
+ const functionDefinitions = getFunctionDefinitions(variable);
806
+ const definition = getSupportedVariableDefinition(variable);
807
+ if (
808
+ !functionDefinitions
809
+ && !definition
810
+ ) {
811
+ visitedVariables.delete(variable);
812
+ return unknown;
813
+ }
814
+
815
+ let result;
816
+ if (functionDefinitions && !functionValuesAreBoolean) {
817
+ result = nonBoolean;
818
+ } else if (functionDefinitions) {
819
+ result = combineBooleanStates(functionDefinitions.map(definition => getFunctionBooleanState(definition.node, context, visitedVariables)));
820
+ } else {
821
+ result = getDefinitionBooleanState(definition, context, visitedVariables, functionValuesAreBoolean);
822
+ }
823
+
824
+ if (variable.references.some(reference => reference.writeExpr)) {
825
+ result = combineVariableBooleanStates([
826
+ result,
827
+ ...variable.references
828
+ .filter(reference => reference.writeExpr)
829
+ .map(reference => getExpressionBooleanState(reference.writeExpr, context, visitedVariables, functionValuesAreBoolean)),
830
+ ]);
831
+ }
832
+
833
+ if (
834
+ result === unknown
835
+ && functionValuesAreBoolean
836
+ && isBooleanVariable(variable, context)
837
+ ) {
838
+ result = boolean;
839
+ }
840
+
841
+ visitedVariables.delete(variable);
842
+ return result;
843
+ }
259
844
 
260
845
  function getBooleanPropertyName(node, sourceCode) {
261
846
  if (
@@ -324,7 +909,54 @@ function isBooleanProperty(node, context) {
324
909
  return false;
325
910
  }
326
911
 
327
- function getSuggestions(variable, prefixes, context) {
912
+ function getExplicitPropertyBooleanState(node, context) {
913
+ const {sourceCode} = context;
914
+
915
+ if (node.type === 'Property') {
916
+ if (
917
+ node.parent.type !== 'ObjectExpression'
918
+ || node.shorthand
919
+ || node.kind === 'set'
920
+ ) {
921
+ return unknown;
922
+ }
923
+
924
+ return getExpressionBooleanState(node.value, context);
925
+ }
926
+
927
+ if (
928
+ node.type === 'MethodDefinition'
929
+ || node.type === 'TSAbstractMethodDefinition'
930
+ ) {
931
+ return ['constructor', 'set'].includes(node.kind) ? unknown : getFunctionBooleanState(node.value, context);
932
+ }
933
+
934
+ if (propertyDefinitionTypes.has(node.type)) {
935
+ const scope = sourceCode.getScope(node);
936
+ const stateFromTypeAnnotation = getTypeAnnotationBooleanState(node.typeAnnotation, context, scope);
937
+
938
+ return stateFromTypeAnnotation === unknown
939
+ ? getExpressionBooleanState(node.value, context)
940
+ : stateFromTypeAnnotation;
941
+ }
942
+
943
+ if (node.type === 'TSPropertySignature') {
944
+ return getTypeAnnotationBooleanState(node.typeAnnotation, context, sourceCode.getScope(node));
945
+ }
946
+
947
+ if (node.type === 'TSMethodSignature') {
948
+ return getTypeAnnotationBooleanState(node.returnType, context, sourceCode.getScope(node), {functionTypesAreBoolean: false});
949
+ }
950
+
951
+ return unknown;
952
+ }
953
+
954
+ function getPropertyBooleanState(node, context) {
955
+ const state = getExplicitPropertyBooleanState(node, context);
956
+ return state === unknown && isBooleanProperty(node, context) ? boolean : state;
957
+ }
958
+
959
+ function getSuggestions(variable, prefixes, context, nameForPrefixCheck) {
328
960
  if (
329
961
  !shouldSuggestRename(variable)
330
962
  || variable.references.some(reference => reference.vueUsedInTemplate)
@@ -340,7 +972,7 @@ function getSuggestions(variable, prefixes, context) {
340
972
  const suggestions = [];
341
973
 
342
974
  for (const prefix of prefixes) {
343
- const replacement = getAvailableVariableName(getReplacementName(variable.name, prefix), scopes);
975
+ const replacement = getAvailableVariableName(getReactHookReplacementName({name: variable.name, nameForPrefixCheck}, prefix), scopes);
344
976
 
345
977
  if (!replacement || usedReplacements.has(replacement)) {
346
978
  continue;
@@ -390,7 +1022,13 @@ function isInDeclareContext(node) {
390
1022
  return false;
391
1023
  }
392
1024
 
393
- function getAutofix(variable, prefixes, context, suggestions) {
1025
+ function getAutofix({
1026
+ variable,
1027
+ prefixes,
1028
+ context,
1029
+ suggestions,
1030
+ nameForPrefixCheck,
1031
+ }) {
394
1032
  if (
395
1033
  !suggestions
396
1034
  || !isAutofixableVariable(variable, context)
@@ -399,7 +1037,7 @@ function getAutofix(variable, prefixes, context, suggestions) {
399
1037
  }
400
1038
 
401
1039
  const [prefix] = prefixes;
402
- const replacement = getReplacementName(variable.name, prefix);
1040
+ const replacement = getReactHookReplacementName({name: variable.name, nameForPrefixCheck}, prefix);
403
1041
  const suggestion = suggestions.find(suggestion => suggestion.data.replacement === replacement);
404
1042
 
405
1043
  return suggestion?.fix;
@@ -414,27 +1052,56 @@ const create = context => {
414
1052
  }
415
1053
 
416
1054
  const checkVariable = variable => {
417
- // `hasBooleanPrefix` and `ignore` are cheap string checks, so run them before
418
- // the expensive `isBooleanVariable` analysis.
419
1055
  if (
420
- hasBooleanPrefix(variable.name, prefixes)
421
- || isIgnoredName(variable.name, ignore)
1056
+ isIgnoredName(variable.name, ignore)
1057
+ || isDestructuredVariable(variable)
1058
+ ) {
1059
+ return;
1060
+ }
1061
+
1062
+ const nameForPrefixCheck = getNameForPrefixCheck(variable, context);
1063
+ const booleanPrefix = getBooleanPrefix(nameForPrefixCheck, prefixes);
1064
+ if (booleanPrefix) {
1065
+ if (getVariableBooleanState(variable, context) === nonBoolean) {
1066
+ const [definition] = variable.defs;
1067
+
1068
+ context.report({
1069
+ node: definition.name,
1070
+ messageId: MESSAGE_ID_NON_BOOLEAN_PREFIX,
1071
+ data: {
1072
+ name: nameForPrefixCheck,
1073
+ prefix: booleanPrefix,
1074
+ },
1075
+ });
1076
+ }
1077
+
1078
+ return;
1079
+ }
1080
+
1081
+ if (
1082
+ getVariableBooleanState(variable, context) === nonBoolean
422
1083
  || !isBooleanVariable(variable, context)
423
1084
  ) {
424
1085
  return;
425
1086
  }
426
1087
 
427
1088
  const [definition] = variable.defs;
428
- const suggest = getSuggestions(variable, prefixes, context);
1089
+ const suggest = getSuggestions(variable, prefixes, context, nameForPrefixCheck);
429
1090
 
430
1091
  context.report({
431
1092
  node: definition.name,
432
1093
  messageId: MESSAGE_ID,
433
1094
  data: {
434
- name: variable.name,
1095
+ name: nameForPrefixCheck,
435
1096
  prefixes: formatPrefixes(prefixes),
436
1097
  },
437
- fix: getAutofix(variable, prefixes, context, suggest),
1098
+ fix: getAutofix({
1099
+ variable,
1100
+ prefixes,
1101
+ context,
1102
+ suggestions: suggest,
1103
+ nameForPrefixCheck,
1104
+ }),
438
1105
  suggest,
439
1106
  });
440
1107
  };
@@ -456,13 +1123,31 @@ const create = context => {
456
1123
 
457
1124
  if (
458
1125
  !name
459
- || hasBooleanPrefix(name, prefixes)
460
1126
  || isIgnoredName(name, ignore)
461
- || !isBooleanProperty(node, context)
462
1127
  ) {
463
1128
  return;
464
1129
  }
465
1130
 
1131
+ const booleanPrefix = getBooleanPrefix(name, prefixes);
1132
+ if (booleanPrefix) {
1133
+ if (getPropertyBooleanState(node, context) === nonBoolean) {
1134
+ context.report({
1135
+ node: node.key,
1136
+ messageId: MESSAGE_ID_NON_BOOLEAN_PREFIX,
1137
+ data: {
1138
+ name,
1139
+ prefix: booleanPrefix,
1140
+ },
1141
+ });
1142
+ }
1143
+
1144
+ return;
1145
+ }
1146
+
1147
+ if (!isBooleanProperty(node, context)) {
1148
+ return;
1149
+ }
1150
+
466
1151
  context.report({
467
1152
  node: node.key,
468
1153
  messageId: MESSAGE_ID,