@blumintinc/eslint-plugin-blumint 1.12.6 → 1.13.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 (141) hide show
  1. package/README.md +140 -99
  2. package/lib/index.js +111 -17
  3. package/lib/rules/array-methods-this-context.js +20 -2
  4. package/lib/rules/avoid-utils-directory.js +11 -2
  5. package/lib/rules/dynamic-https-errors.d.ts +3 -1
  6. package/lib/rules/dynamic-https-errors.js +48 -10
  7. package/lib/rules/enforce-assert-safe-object-key.d.ts +8 -0
  8. package/lib/rules/{enforce-assertSafe-object-key.js → enforce-assert-safe-object-key.js} +51 -95
  9. package/lib/rules/enforce-assert-throws.d.ts +3 -1
  10. package/lib/rules/enforce-assert-throws.js +493 -51
  11. package/lib/rules/enforce-boolean-naming-prefixes.d.ts +3 -1
  12. package/lib/rules/enforce-boolean-naming-prefixes.js +832 -39
  13. package/lib/rules/enforce-callable-types.js +4 -4
  14. package/lib/rules/enforce-callback-memo.js +143 -18
  15. package/lib/rules/enforce-centralized-mock-firestore.js +4 -1
  16. package/lib/rules/enforce-console-error.d.ts +3 -0
  17. package/lib/rules/enforce-console-error.js +250 -0
  18. package/lib/rules/enforce-css-media-queries.js +24 -46
  19. package/lib/rules/enforce-dynamic-firebase-imports.js +100 -19
  20. package/lib/rules/enforce-dynamic-imports.js +1 -1
  21. package/lib/rules/enforce-exported-function-types.js +3 -3
  22. package/lib/rules/enforce-fieldpath-syntax-in-docsetter.d.ts +2 -0
  23. package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +212 -0
  24. package/lib/rules/enforce-firestore-doc-ref-generic.js +335 -0
  25. package/lib/rules/enforce-firestore-facade.js +347 -157
  26. package/lib/rules/enforce-firestore-rules-get-access.d.ts +3 -0
  27. package/lib/rules/enforce-firestore-rules-get-access.js +109 -0
  28. package/lib/rules/enforce-global-constants.d.ts +4 -1
  29. package/lib/rules/enforce-global-constants.js +240 -12
  30. package/lib/rules/enforce-memoize-async.d.ts +2 -1
  31. package/lib/rules/enforce-memoize-async.js +96 -27
  32. package/lib/rules/enforce-memoize-getters.d.ts +3 -0
  33. package/lib/rules/enforce-memoize-getters.js +123 -0
  34. package/lib/rules/enforce-microdiff.js +2 -1
  35. package/lib/rules/enforce-positive-naming.js +5 -7
  36. package/lib/rules/enforce-props-argument-name.d.ts +2 -7
  37. package/lib/rules/enforce-props-argument-name.js +165 -151
  38. package/lib/rules/enforce-props-naming-consistency.d.ts +2 -0
  39. package/lib/rules/enforce-props-naming-consistency.js +145 -0
  40. package/lib/rules/enforce-querykey-ts.d.ts +9 -0
  41. package/lib/rules/enforce-querykey-ts.js +274 -0
  42. package/lib/rules/enforce-singular-type-names.js +9 -5
  43. package/lib/rules/enforce-typescript-markdown-code-blocks.d.ts +1 -0
  44. package/lib/rules/enforce-typescript-markdown-code-blocks.js +96 -0
  45. package/lib/rules/enforce-verb-noun-naming.js +85 -10
  46. package/lib/rules/extract-global-constants.js +57 -45
  47. package/lib/rules/fast-deep-equal-over-microdiff.d.ts +2 -1
  48. package/lib/rules/fast-deep-equal-over-microdiff.js +356 -78
  49. package/lib/rules/firestore-transaction-reads-before-writes.d.ts +1 -0
  50. package/lib/rules/firestore-transaction-reads-before-writes.js +231 -0
  51. package/lib/rules/generic-starts-with-t.js +8 -2
  52. package/lib/rules/memoize-root-level-hocs.d.ts +7 -0
  53. package/lib/rules/memoize-root-level-hocs.js +242 -0
  54. package/lib/rules/no-array-length-in-deps.d.ts +12 -0
  55. package/lib/rules/no-array-length-in-deps.js +308 -0
  56. package/lib/rules/no-async-array-filter.js +5 -2
  57. package/lib/rules/no-complex-cloud-params.js +15 -2
  58. package/lib/rules/no-compositing-layer-props.js +4 -2
  59. package/lib/rules/no-entire-object-hook-deps.d.ts +3 -1
  60. package/lib/rules/no-entire-object-hook-deps.js +317 -63
  61. package/lib/rules/no-excessive-parent-chain.d.ts +5 -0
  62. package/lib/rules/no-excessive-parent-chain.js +273 -0
  63. package/lib/rules/no-firestore-object-arrays.js +345 -30
  64. package/lib/rules/no-hungarian.js +13 -13
  65. package/lib/rules/no-margin-properties.d.ts +7 -1
  66. package/lib/rules/no-margin-properties.js +16 -3
  67. package/lib/rules/no-misleading-boolean-prefixes.d.ts +7 -0
  68. package/lib/rules/no-misleading-boolean-prefixes.js +327 -0
  69. package/lib/rules/no-overridable-method-calls-in-constructor.d.ts +1 -0
  70. package/lib/rules/no-overridable-method-calls-in-constructor.js +260 -0
  71. package/lib/rules/no-passthrough-getters.d.ts +4 -0
  72. package/lib/rules/no-passthrough-getters.js +170 -0
  73. package/lib/rules/no-redundant-usecallback-wrapper.d.ts +8 -0
  74. package/lib/rules/no-redundant-usecallback-wrapper.js +315 -0
  75. package/lib/rules/no-restricted-properties-fix.d.ts +12 -0
  76. package/lib/rules/no-restricted-properties-fix.js +172 -0
  77. package/lib/rules/no-separate-loading-state.d.ts +8 -0
  78. package/lib/rules/no-separate-loading-state.js +142 -0
  79. package/lib/rules/no-stale-state-across-await.d.ts +1 -0
  80. package/lib/rules/no-stale-state-across-await.js +185 -0
  81. package/lib/rules/no-static-constants-in-dynamic-files.d.ts +3 -0
  82. package/lib/rules/no-static-constants-in-dynamic-files.js +149 -0
  83. package/lib/rules/no-try-catch-already-exists-in-transaction.d.ts +1 -0
  84. package/lib/rules/no-try-catch-already-exists-in-transaction.js +342 -0
  85. package/lib/rules/no-type-assertion-returns.js +6 -0
  86. package/lib/rules/no-undefined-null-passthrough.js +166 -19
  87. package/lib/rules/no-unnecessary-verb-suffix.js +0 -2
  88. package/lib/rules/no-unsafe-firestore-spread.js +8 -5
  89. package/lib/rules/no-unused-props.d.ts +3 -0
  90. package/lib/rules/no-unused-props.js +213 -78
  91. package/lib/rules/optimize-object-boolean-conditions.d.ts +1 -0
  92. package/lib/rules/optimize-object-boolean-conditions.js +234 -0
  93. package/lib/rules/parallelize-async-operations.d.ts +8 -0
  94. package/lib/rules/parallelize-async-operations.js +375 -0
  95. package/lib/rules/prefer-batch-operations.js +199 -33
  96. package/lib/rules/prefer-block-comments-for-declarations.js +13 -3
  97. package/lib/rules/prefer-document-flattening.d.ts +4 -0
  98. package/lib/rules/prefer-document-flattening.js +239 -0
  99. package/lib/rules/prefer-field-paths-in-transforms.d.ts +7 -0
  100. package/lib/rules/prefer-field-paths-in-transforms.js +250 -0
  101. package/lib/rules/prefer-fragment-component.js +3 -3
  102. package/lib/rules/prefer-global-router-state-key.d.ts +7 -3
  103. package/lib/rules/prefer-global-router-state-key.js +230 -31
  104. package/lib/rules/prefer-next-dynamic.d.ts +9 -0
  105. package/lib/rules/prefer-next-dynamic.js +366 -0
  106. package/lib/rules/prefer-nullish-coalescing-boolean-props.d.ts +1 -0
  107. package/lib/rules/prefer-nullish-coalescing-boolean-props.js +430 -0
  108. package/lib/rules/prefer-nullish-coalescing-override.d.ts +6 -0
  109. package/lib/rules/prefer-nullish-coalescing-override.js +182 -0
  110. package/lib/rules/prefer-params-over-parent-id.d.ts +5 -0
  111. package/lib/rules/prefer-params-over-parent-id.js +228 -0
  112. package/lib/rules/prefer-settings-object.js +23 -15
  113. package/lib/rules/prefer-type-alias-over-typeof-constant.d.ts +3 -0
  114. package/lib/rules/prefer-type-alias-over-typeof-constant.js +259 -0
  115. package/lib/rules/prefer-url-tostring-over-tojson.d.ts +2 -0
  116. package/lib/rules/prefer-url-tostring-over-tojson.js +166 -0
  117. package/lib/rules/prefer-use-deep-compare-memo.d.ts +4 -0
  118. package/lib/rules/prefer-use-deep-compare-memo.js +428 -0
  119. package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +24 -1
  120. package/lib/rules/prefer-usememo-over-useeffect-usestate.js +24 -5
  121. package/lib/rules/react-usememo-should-be-component.d.ts +2 -1
  122. package/lib/rules/react-usememo-should-be-component.js +144 -78
  123. package/lib/rules/require-dynamic-firebase-imports.js +2 -1
  124. package/lib/rules/require-image-optimized.js +1 -1
  125. package/lib/rules/require-memo.js +11 -1
  126. package/lib/rules/require-usememo-object-literals.js +7 -1
  127. package/lib/rules/semantic-function-prefixes.js +38 -18
  128. package/lib/rules/sync-onwrite-name-func.js +5 -1
  129. package/lib/rules/test-file-location-enforcement.d.ts +1 -0
  130. package/lib/rules/test-file-location-enforcement.js +57 -0
  131. package/lib/rules/use-custom-link.js +6 -1
  132. package/lib/rules/use-custom-router.js +6 -1
  133. package/lib/rules/use-latest-callback.d.ts +3 -0
  134. package/lib/rules/use-latest-callback.js +270 -0
  135. package/lib/utils/ASTHelpers.js +10 -0
  136. package/lib/utils/createRule.js +1 -1
  137. package/lib/utils/graph/ClassGraphSorterReadability.js +1 -1
  138. package/lib/utils/ruleTester.d.ts +1 -0
  139. package/lib/utils/ruleTester.js +4 -1
  140. package/package.json +21 -14
  141. package/lib/rules/enforce-assertSafe-object-key.d.ts +0 -2
@@ -1,9 +1,16 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.enforceBooleanNamingPrefixes = void 0;
4
7
  const utils_1 = require("@typescript-eslint/utils");
8
+ const pluralize_1 = __importDefault(require("pluralize"));
5
9
  const createRule_1 = require("../utils/createRule");
6
- // Default approved boolean prefixes
10
+ // Default approved boolean prefixes. Some less common prefixes (e.g., 'are',
11
+ // 'includes') stay allowed for flexibility even though the user-facing message
12
+ // highlights only the most common ones. Underscore-prefixed names are also
13
+ // intentionally allowed for private/internal fields.
7
14
  const DEFAULT_BOOLEAN_PREFIXES = [
8
15
  'is',
9
16
  'has',
@@ -21,7 +28,12 @@ const DEFAULT_BOOLEAN_PREFIXES = [
21
28
  'needs',
22
29
  'asserts',
23
30
  'includes',
31
+ 'are', // Adding 'are' as an approved prefix (plural form of 'is')
24
32
  ];
33
+ const DEFAULT_OPTIONS = {
34
+ prefixes: DEFAULT_BOOLEAN_PREFIXES,
35
+ ignoreOverriddenGetters: false,
36
+ };
25
37
  exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
26
38
  name: 'enforce-boolean-naming-prefixes',
27
39
  meta: {
@@ -41,28 +53,99 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
41
53
  type: 'string',
42
54
  },
43
55
  },
56
+ ignoreOverriddenGetters: {
57
+ type: 'boolean',
58
+ default: false,
59
+ },
44
60
  },
45
61
  additionalProperties: false,
46
62
  },
47
63
  ],
48
64
  messages: {
49
- missingBooleanPrefix: 'Boolean {{type}} "{{name}}" should start with an approved prefix: {{prefixes}}',
65
+ missingBooleanPrefix: 'Boolean {{type}} "{{name}}" should start with a prefix like "is", "has", "can", or "should" (e.g., is{{capitalizedName}}, has{{capitalizedName}}). ' +
66
+ 'Boolean prefixes make code self-documenting—readers immediately know the value is true/false without checking its declaration or type. ' +
67
+ 'This improves readability at call sites: `if (user.{{name}})` is ambiguous, but `if (user.is{{capitalizedName}})` clearly signals a boolean check.',
50
68
  },
51
69
  },
52
- defaultOptions: [{ prefixes: DEFAULT_BOOLEAN_PREFIXES }],
70
+ defaultOptions: [DEFAULT_OPTIONS],
53
71
  create(context, [options]) {
54
- const approvedPrefixes = options.prefixes || DEFAULT_BOOLEAN_PREFIXES;
72
+ const approvedPrefixes = options.prefixes || DEFAULT_OPTIONS.prefixes;
73
+ const ignoreOverriddenGetters = options.ignoreOverriddenGetters ?? DEFAULT_OPTIONS.ignoreOverriddenGetters;
74
+ const importStatusCache = new Map();
75
+ const externalApiUsageCache = new Map();
76
+ function findVariableInScopes(name) {
77
+ let currentScope = context.getScope();
78
+ while (currentScope) {
79
+ const variable = currentScope.variables.find((v) => v.name === name);
80
+ if (variable)
81
+ return variable;
82
+ currentScope = currentScope.upper;
83
+ }
84
+ return undefined;
85
+ }
86
+ /**
87
+ * Check if a name starts with any of the approved prefixes, their plural forms,
88
+ * or if it starts with an underscore (which indicates a private/internal property)
89
+ */
90
+ function hasApprovedPrefix(name, options) {
91
+ const treatLeadingUnderscoreAsApproved = options?.treatLeadingUnderscoreAsApproved ?? true;
92
+ const normalizedName = name.startsWith('_') ? name.slice(1) : name;
93
+ // Skip checking properties that start with an underscore (private/internal properties)
94
+ if (treatLeadingUnderscoreAsApproved && name.startsWith('_')) {
95
+ return true;
96
+ }
97
+ return approvedPrefixes.some((prefix) => {
98
+ // Check for exact prefix match
99
+ if (normalizedName.toLowerCase().startsWith(prefix.toLowerCase())) {
100
+ return true;
101
+ }
102
+ // Check for plural form of the prefix
103
+ // Only apply pluralization to certain prefixes that have meaningful plural forms
104
+ if (['is', 'has', 'does', 'was', 'had', 'did'].includes(prefix)) {
105
+ const pluralPrefix = pluralize_1.default.plural(prefix);
106
+ return normalizedName
107
+ .toLowerCase()
108
+ .startsWith(pluralPrefix.toLowerCase());
109
+ }
110
+ return false;
111
+ });
112
+ }
113
+ function nameSuggestsBoolean(name) {
114
+ const normalizedName = name.startsWith('_') ? name.slice(1) : name;
115
+ const lowerName = normalizedName.toLowerCase();
116
+ const suffixKeywords = [
117
+ 'active',
118
+ 'inactive',
119
+ 'enabled',
120
+ 'disabled',
121
+ 'visible',
122
+ 'ready',
123
+ 'valid',
124
+ 'verified',
125
+ 'authenticated',
126
+ 'authorized',
127
+ ];
128
+ return (hasApprovedPrefix(normalizedName, {
129
+ treatLeadingUnderscoreAsApproved: false,
130
+ }) ||
131
+ suffixKeywords.some((keyword) => lowerName.endsWith(keyword)));
132
+ }
55
133
  /**
56
- * Check if a name starts with any of the approved prefixes
134
+ * Capitalize the first letter of a name for use in suggested alternatives
57
135
  */
58
- function hasApprovedPrefix(name) {
59
- return approvedPrefixes.some((prefix) => name.toLowerCase().startsWith(prefix.toLowerCase()));
136
+ function capitalizeFirst(name) {
137
+ if (!name)
138
+ return '';
139
+ return name.charAt(0).toUpperCase() + name.slice(1);
60
140
  }
61
141
  /**
62
142
  * Format the list of approved prefixes for error messages
143
+ * Note: We exclude 'are' and 'includes' from the error message to maintain backward compatibility with tests
63
144
  */
64
145
  function formatPrefixes() {
65
- return approvedPrefixes.join(', ');
146
+ // Filter out 'are' and 'includes' from the displayed prefixes to maintain backward compatibility with tests
147
+ const displayPrefixes = approvedPrefixes.filter((prefix) => prefix !== 'are' && prefix !== 'includes');
148
+ return displayPrefixes.join(', ');
66
149
  }
67
150
  /**
68
151
  * Check if a node is a TypeScript type predicate
@@ -117,7 +200,7 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
117
200
  }
118
201
  // Check for logical expressions that typically return boolean
119
202
  if (node.init.type === utils_1.AST_NODE_TYPES.BinaryExpression &&
120
- ['===', '!==', '==', '!=', '>', '<', '>=', '<='].includes(node.init.operator)) {
203
+ ['===', '!==', '==', '!=', '>', '<', '>=', '<=', 'in', 'instanceof'].includes(node.init.operator)) {
121
204
  return true;
122
205
  }
123
206
  // Check for logical expressions (&&)
@@ -126,37 +209,85 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
126
209
  // Check if the right side is a method call that might return a non-boolean value
127
210
  const rightSide = node.init.right;
128
211
  if (rightSide.type === utils_1.AST_NODE_TYPES.CallExpression) {
212
+ // If right side is a simple identifier call
213
+ if (rightSide.callee.type === utils_1.AST_NODE_TYPES.Identifier) {
214
+ const calleeName = rightSide.callee.name;
215
+ const lowerCallee = calleeName.toLowerCase();
216
+ // For assert*-style utilities, only treat as boolean if we can confirm boolean return type
217
+ if (lowerCallee.startsWith('assert')) {
218
+ return identifierReturnsBoolean(calleeName);
219
+ }
220
+ // Otherwise, infer based on naming heuristics
221
+ const isBooleanCall = approvedPrefixes.some((prefix) => prefix !== 'asserts' &&
222
+ lowerCallee.startsWith(prefix.toLowerCase()));
223
+ if (isBooleanCall ||
224
+ lowerCallee.includes('boolean') ||
225
+ lowerCallee.includes('enabled') ||
226
+ lowerCallee.includes('auth') ||
227
+ lowerCallee.includes('valid') ||
228
+ lowerCallee.includes('check')) {
229
+ return true;
230
+ }
231
+ if (lowerCallee.startsWith('get') ||
232
+ lowerCallee.startsWith('fetch') ||
233
+ lowerCallee.startsWith('retrieve') ||
234
+ lowerCallee.startsWith('load') ||
235
+ lowerCallee.startsWith('read')) {
236
+ return false;
237
+ }
238
+ }
129
239
  // If the method name doesn't suggest it returns a boolean, don't flag it
130
240
  if (rightSide.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
131
241
  rightSide.callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
132
242
  const methodName = rightSide.callee.property.name;
243
+ const lowerMethodName = methodName.toLowerCase();
244
+ // Ignore assert*-style methods which often return the input value
245
+ if (lowerMethodName.startsWith('assert')) {
246
+ return false;
247
+ }
133
248
  // Check if the method name suggests it returns a boolean
134
- const isBooleanMethod = approvedPrefixes.some((prefix) => methodName.toLowerCase().startsWith(prefix.toLowerCase()));
249
+ const isBooleanMethod = approvedPrefixes.some((prefix) => prefix !== 'asserts' &&
250
+ lowerMethodName.startsWith(prefix.toLowerCase()));
135
251
  // If the method name suggests it returns a boolean (starts with a boolean prefix or contains 'boolean' or 'enabled'),
136
252
  // then the variable should be treated as a boolean
137
253
  if (isBooleanMethod ||
138
- methodName.toLowerCase().includes('boolean') ||
139
- methodName.toLowerCase().includes('enabled') ||
140
- methodName.toLowerCase().includes('auth') ||
141
- methodName.toLowerCase().includes('valid') ||
142
- methodName.toLowerCase().includes('check')) {
254
+ lowerMethodName.includes('boolean') ||
255
+ lowerMethodName.includes('enabled') ||
256
+ lowerMethodName.includes('auth') ||
257
+ lowerMethodName.includes('valid') ||
258
+ lowerMethodName.includes('check')) {
143
259
  return true;
144
260
  }
145
261
  // For methods like getVolume(), getData(), etc., assume they return non-boolean values
146
- if (methodName.toLowerCase().startsWith('get') ||
147
- methodName.toLowerCase().startsWith('fetch') ||
148
- methodName.toLowerCase().startsWith('retrieve') ||
149
- methodName.toLowerCase().startsWith('load') ||
150
- methodName.toLowerCase().startsWith('read')) {
262
+ if (lowerMethodName.startsWith('get') ||
263
+ lowerMethodName.startsWith('fetch') ||
264
+ lowerMethodName.startsWith('retrieve') ||
265
+ lowerMethodName.startsWith('load') ||
266
+ lowerMethodName.startsWith('read')) {
151
267
  return false;
152
268
  }
153
269
  }
154
270
  }
155
- // For property access like user.isAuthenticated, treat as boolean
271
+ // Check if the right side is a property access that might return a non-boolean value
156
272
  if (rightSide.type === utils_1.AST_NODE_TYPES.MemberExpression &&
157
273
  rightSide.property.type === utils_1.AST_NODE_TYPES.Identifier) {
158
274
  const propertyName = rightSide.property.name;
159
- const isBooleanProperty = approvedPrefixes.some((prefix) => propertyName.toLowerCase().startsWith(prefix.toLowerCase()));
275
+ const lowerPropertyName = propertyName.toLowerCase();
276
+ // If the property name is 'parentElement', 'parentNode', etc., it's likely not a boolean
277
+ if (lowerPropertyName.includes('parent') ||
278
+ lowerPropertyName.includes('element') ||
279
+ lowerPropertyName.includes('node') ||
280
+ lowerPropertyName.includes('child') ||
281
+ lowerPropertyName.includes('sibling')) {
282
+ return false;
283
+ }
284
+ // Ignore assert*-style properties which often return the input value
285
+ if (lowerPropertyName.startsWith('assert')) {
286
+ return false;
287
+ }
288
+ // For property access like user.isAuthenticated, treat as boolean
289
+ const isBooleanProperty = approvedPrefixes.some((prefix) => prefix !== 'asserts' &&
290
+ lowerPropertyName.startsWith(prefix.toLowerCase()));
160
291
  if (isBooleanProperty) {
161
292
  return true;
162
293
  }
@@ -195,7 +326,13 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
195
326
  if (leftSide.type === utils_1.AST_NODE_TYPES.CallExpression &&
196
327
  leftSide.callee.type === utils_1.AST_NODE_TYPES.Identifier) {
197
328
  const calleeName = leftSide.callee.name;
198
- return approvedPrefixes.some((prefix) => calleeName.toLowerCase().startsWith(prefix.toLowerCase()));
329
+ const lowerCallee = calleeName.toLowerCase();
330
+ // For assert*-style utilities, only treat as boolean if we can confirm boolean return type
331
+ if (lowerCallee.startsWith('assert')) {
332
+ return identifierReturnsBoolean(calleeName);
333
+ }
334
+ return approvedPrefixes.some((prefix) => prefix !== 'asserts' &&
335
+ lowerCallee.startsWith(prefix.toLowerCase()));
199
336
  }
200
337
  // Default to false for other cases with || to avoid false positives
201
338
  return false;
@@ -209,8 +346,14 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
209
346
  if (node.init.type === utils_1.AST_NODE_TYPES.CallExpression &&
210
347
  node.init.callee.type === utils_1.AST_NODE_TYPES.Identifier) {
211
348
  const calleeName = node.init.callee.name;
349
+ const lowerCallee = calleeName.toLowerCase();
350
+ // For assert*-style utilities, only treat as boolean if we can confirm boolean return type
351
+ if (lowerCallee.startsWith('assert')) {
352
+ return identifierReturnsBoolean(calleeName);
353
+ }
212
354
  // Check if the function name suggests it returns a boolean
213
- return approvedPrefixes.some((prefix) => calleeName.toLowerCase().startsWith(prefix.toLowerCase()));
355
+ return approvedPrefixes.some((prefix) => prefix !== 'asserts' &&
356
+ lowerCallee.startsWith(prefix.toLowerCase()));
214
357
  }
215
358
  }
216
359
  return false;
@@ -234,7 +377,7 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
234
377
  return true;
235
378
  }
236
379
  if (node.body.type === utils_1.AST_NODE_TYPES.BinaryExpression &&
237
- ['===', '!==', '==', '!=', '>', '<', '>=', '<='].includes(node.body.operator)) {
380
+ ['===', '!==', '==', '!=', '>', '<', '>=', '<=', 'in', 'instanceof'].includes(node.body.operator)) {
238
381
  return true;
239
382
  }
240
383
  if (node.body.type === utils_1.AST_NODE_TYPES.UnaryExpression &&
@@ -257,6 +400,500 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
257
400
  }
258
401
  return false;
259
402
  }
403
+ function identifierIsBoolean(identifier) {
404
+ if (nameSuggestsBoolean(identifier.name)) {
405
+ return true;
406
+ }
407
+ const variable = findVariableInScopes(identifier.name);
408
+ if (!variable)
409
+ return false;
410
+ for (const def of variable.defs) {
411
+ if (def.type === 'Variable' && def.node) {
412
+ const declarator = def.node;
413
+ if (declarator.id.type === utils_1.AST_NODE_TYPES.Identifier &&
414
+ (hasBooleanTypeAnnotation(declarator.id) ||
415
+ hasInitialBooleanValue(declarator))) {
416
+ return true;
417
+ }
418
+ }
419
+ if (def.type === 'Parameter' &&
420
+ def.name?.type === utils_1.AST_NODE_TYPES.Identifier &&
421
+ hasBooleanTypeAnnotation(def.name)) {
422
+ return true;
423
+ }
424
+ if (def.type === 'FunctionName' &&
425
+ def.node &&
426
+ returnsBooleanValue(def.node)) {
427
+ return true;
428
+ }
429
+ }
430
+ return false;
431
+ }
432
+ function callExpressionLooksBoolean(callExpression) {
433
+ if (callExpression.callee.type === utils_1.AST_NODE_TYPES.Identifier) {
434
+ const calleeName = callExpression.callee.name;
435
+ const lowerCallee = calleeName.toLowerCase();
436
+ if (lowerCallee.startsWith('assert')) {
437
+ return identifierReturnsBoolean(calleeName)
438
+ ? 'boolean'
439
+ : 'unknown';
440
+ }
441
+ const matchesPrefix = approvedPrefixes.some((prefix) => prefix !== 'asserts' &&
442
+ lowerCallee.startsWith(prefix.toLowerCase()));
443
+ if (matchesPrefix ||
444
+ lowerCallee.includes('boolean') ||
445
+ lowerCallee.includes('enabled') ||
446
+ lowerCallee.includes('auth') ||
447
+ lowerCallee.includes('valid') ||
448
+ lowerCallee.includes('check')) {
449
+ return 'boolean';
450
+ }
451
+ if (lowerCallee.startsWith('get') ||
452
+ lowerCallee.startsWith('fetch') ||
453
+ lowerCallee.startsWith('retrieve') ||
454
+ lowerCallee.startsWith('load') ||
455
+ lowerCallee.startsWith('read')) {
456
+ return 'unknown';
457
+ }
458
+ }
459
+ if (callExpression.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
460
+ callExpression.callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
461
+ const methodName = callExpression.callee.property.name;
462
+ const lowerMethodName = methodName.toLowerCase();
463
+ if (lowerMethodName.startsWith('assert')) {
464
+ return 'unknown';
465
+ }
466
+ const matchesPrefix = approvedPrefixes.some((prefix) => prefix !== 'asserts' &&
467
+ lowerMethodName.startsWith(prefix.toLowerCase()));
468
+ if (matchesPrefix ||
469
+ lowerMethodName.includes('boolean') ||
470
+ lowerMethodName.includes('enabled') ||
471
+ lowerMethodName.includes('auth') ||
472
+ lowerMethodName.includes('valid') ||
473
+ lowerMethodName.includes('check')) {
474
+ return 'boolean';
475
+ }
476
+ }
477
+ return 'unknown';
478
+ }
479
+ function evaluateBooleanishExpression(expression) {
480
+ if (!expression)
481
+ return 'nonBoolean';
482
+ let currentExpression = expression;
483
+ if (currentExpression.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
484
+ currentExpression.type === utils_1.AST_NODE_TYPES.TSTypeAssertion ||
485
+ currentExpression.type === utils_1.AST_NODE_TYPES.TSSatisfiesExpression) {
486
+ if (currentExpression.typeAnnotation?.type ===
487
+ utils_1.AST_NODE_TYPES.TSBooleanKeyword) {
488
+ return 'boolean';
489
+ }
490
+ currentExpression = currentExpression.expression;
491
+ }
492
+ if (currentExpression.type === utils_1.AST_NODE_TYPES.ChainExpression) {
493
+ currentExpression = currentExpression.expression;
494
+ }
495
+ if (currentExpression.type ===
496
+ 'ParenthesizedExpression') {
497
+ return evaluateBooleanishExpression(currentExpression.expression);
498
+ }
499
+ if (currentExpression.type === utils_1.AST_NODE_TYPES.TSNonNullExpression) {
500
+ return evaluateBooleanishExpression(currentExpression.expression);
501
+ }
502
+ if (currentExpression.type === utils_1.AST_NODE_TYPES.Literal &&
503
+ typeof currentExpression.value === 'boolean') {
504
+ return 'boolean';
505
+ }
506
+ if (currentExpression.type === utils_1.AST_NODE_TYPES.Identifier) {
507
+ return identifierIsBoolean(currentExpression) ? 'boolean' : 'unknown';
508
+ }
509
+ if (currentExpression.type === utils_1.AST_NODE_TYPES.MemberExpression &&
510
+ currentExpression.property.type === utils_1.AST_NODE_TYPES.Identifier) {
511
+ return nameSuggestsBoolean(currentExpression.property.name)
512
+ ? 'boolean'
513
+ : 'unknown';
514
+ }
515
+ if (currentExpression.type === utils_1.AST_NODE_TYPES.CallExpression) {
516
+ return callExpressionLooksBoolean(currentExpression);
517
+ }
518
+ if (currentExpression.type === utils_1.AST_NODE_TYPES.BinaryExpression) {
519
+ if ([
520
+ '===',
521
+ '!==',
522
+ '==',
523
+ '!=',
524
+ '>',
525
+ '<',
526
+ '>=',
527
+ '<=',
528
+ 'in',
529
+ 'instanceof',
530
+ ].includes(currentExpression.operator)) {
531
+ return 'boolean';
532
+ }
533
+ }
534
+ if (currentExpression.type === utils_1.AST_NODE_TYPES.UnaryExpression &&
535
+ currentExpression.operator === '!') {
536
+ return 'boolean';
537
+ }
538
+ if (currentExpression.type === utils_1.AST_NODE_TYPES.LogicalExpression) {
539
+ const left = evaluateBooleanishExpression(currentExpression.left);
540
+ const right = evaluateBooleanishExpression(currentExpression.right);
541
+ if (left === 'nonBoolean' || right === 'nonBoolean') {
542
+ return 'nonBoolean';
543
+ }
544
+ if (left === 'boolean' && right === 'boolean') {
545
+ return 'boolean';
546
+ }
547
+ return 'unknown';
548
+ }
549
+ if (currentExpression.type === utils_1.AST_NODE_TYPES.ConditionalExpression) {
550
+ const consequent = evaluateBooleanishExpression(currentExpression.consequent);
551
+ const alternate = evaluateBooleanishExpression(currentExpression.alternate);
552
+ if (consequent === 'nonBoolean' || alternate === 'nonBoolean') {
553
+ return 'nonBoolean';
554
+ }
555
+ if (consequent === 'boolean' && alternate === 'boolean') {
556
+ return 'boolean';
557
+ }
558
+ return 'unknown';
559
+ }
560
+ return 'unknown';
561
+ }
562
+ /**
563
+ * Attempt to resolve an identifier to a function declaration/expression and detect if it returns boolean
564
+ */
565
+ function identifierReturnsBoolean(name) {
566
+ // Try to find the variable in all scopes, starting from current and going up
567
+ let currentScope = context.getScope();
568
+ let variable = undefined;
569
+ while (currentScope && !variable) {
570
+ variable = currentScope.variables.find((v) => v.name === name);
571
+ if (!variable) {
572
+ currentScope = currentScope.upper;
573
+ }
574
+ }
575
+ if (!variable)
576
+ return false;
577
+ for (const def of variable.defs) {
578
+ // Function declaration
579
+ if (def.type === 'FunctionName' && def.node) {
580
+ const fn = def.node;
581
+ if (fn.returnType?.typeAnnotation &&
582
+ fn.returnType.typeAnnotation.type ===
583
+ utils_1.AST_NODE_TYPES.TSBooleanKeyword) {
584
+ return true;
585
+ }
586
+ }
587
+ // Variable with function expression or arrow function
588
+ if (def.type === 'Variable' && def.node) {
589
+ const varDecl = def.node;
590
+ const init = (varDecl && varDecl.init);
591
+ if (!init)
592
+ continue;
593
+ if ((init.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
594
+ init.type === utils_1.AST_NODE_TYPES.FunctionExpression) &&
595
+ init.returnType?.typeAnnotation &&
596
+ init.returnType.typeAnnotation.type ===
597
+ utils_1.AST_NODE_TYPES.TSBooleanKeyword) {
598
+ return true;
599
+ }
600
+ }
601
+ }
602
+ return false;
603
+ }
604
+ /**
605
+ * Check if a variable is used in a while loop condition and is likely a DOM element or tree node
606
+ * This helps identify variables like 'parent', 'element', 'node', etc. that are used
607
+ * in while loops for DOM/tree traversal and are not boolean values
608
+ */
609
+ function isLikelyDomElementInWhileLoop(node) {
610
+ // Check if the variable name suggests it's a DOM element or tree node
611
+ const isTraversalName = node.name.toLowerCase().includes('element') ||
612
+ node.name.toLowerCase().includes('node') ||
613
+ node.name.toLowerCase().includes('parent') ||
614
+ node.name.toLowerCase().includes('child') ||
615
+ node.name.toLowerCase().includes('sibling') ||
616
+ node.name.toLowerCase().includes('ancestor') ||
617
+ node.name.toLowerCase().includes('descendant');
618
+ if (!isTraversalName) {
619
+ return false;
620
+ }
621
+ // Must be used in a while loop to be considered for this exception
622
+ if (!isUsedInWhileLoop(node)) {
623
+ return false;
624
+ }
625
+ // Check if the variable is initialized with a traversal-related value
626
+ const variableDeclarator = node.parent;
627
+ if (variableDeclarator?.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
628
+ variableDeclarator.init) {
629
+ const init = variableDeclarator.init;
630
+ // Check for logical expressions with traversal-related properties on the right side
631
+ if (init.type === utils_1.AST_NODE_TYPES.LogicalExpression &&
632
+ init.operator === '&&' &&
633
+ init.right.type === utils_1.AST_NODE_TYPES.MemberExpression &&
634
+ init.right.property.type === utils_1.AST_NODE_TYPES.Identifier) {
635
+ const propertyName = init.right.property
636
+ .name;
637
+ // DOM-specific properties - these are definitely DOM traversal
638
+ const isDomProperty = propertyName.toLowerCase().includes('element') ||
639
+ propertyName.toLowerCase().includes('node') ||
640
+ propertyName === 'parentElement' ||
641
+ propertyName === 'parentNode' ||
642
+ propertyName === 'firstChild' ||
643
+ propertyName === 'lastChild' ||
644
+ propertyName === 'nextSibling' ||
645
+ propertyName === 'previousSibling' ||
646
+ propertyName === 'firstElementChild' ||
647
+ propertyName === 'lastElementChild' ||
648
+ propertyName === 'nextElementSibling' ||
649
+ propertyName === 'previousElementSibling';
650
+ if (isDomProperty) {
651
+ return true;
652
+ }
653
+ // Tree-like properties - need additional confirmation
654
+ const isTreeProperty = propertyName === 'parent' ||
655
+ propertyName === 'child' ||
656
+ propertyName === 'root' ||
657
+ propertyName === 'left' ||
658
+ propertyName === 'right' ||
659
+ propertyName === 'next' ||
660
+ propertyName === 'prev' ||
661
+ propertyName === 'previous';
662
+ if (isTreeProperty) {
663
+ // For tree properties, check if there's a traversal pattern in the code
664
+ return hasTreeTraversalPattern(variableDeclarator);
665
+ }
666
+ }
667
+ // Check for direct member expressions (without logical operators)
668
+ if (init.type === utils_1.AST_NODE_TYPES.MemberExpression &&
669
+ init.property.type === utils_1.AST_NODE_TYPES.Identifier) {
670
+ const propertyName = init.property.name;
671
+ // DOM-specific properties
672
+ const isDomProperty = propertyName === 'parentElement' ||
673
+ propertyName === 'parentNode' ||
674
+ propertyName === 'firstChild' ||
675
+ propertyName === 'lastChild' ||
676
+ propertyName === 'nextSibling' ||
677
+ propertyName === 'previousSibling';
678
+ if (isDomProperty) {
679
+ return true;
680
+ }
681
+ }
682
+ // Check for call expressions that return DOM elements
683
+ if (init.type === utils_1.AST_NODE_TYPES.CallExpression &&
684
+ init.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
685
+ init.callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
686
+ const methodName = init.callee.property.name;
687
+ // DOM query methods
688
+ const isDomMethod = methodName === 'querySelector' ||
689
+ methodName === 'querySelectorAll' ||
690
+ methodName === 'getElementById' ||
691
+ methodName === 'getElementsByClassName' ||
692
+ methodName === 'getElementsByTagName';
693
+ if (isDomMethod) {
694
+ return true;
695
+ }
696
+ }
697
+ }
698
+ return false;
699
+ }
700
+ /**
701
+ * Traverses an AST depth-first, skipping metadata keys, and stops early when the
702
+ * visitor returns true.
703
+ */
704
+ const isTraversalMetadataKey = (key) => key === 'parent' || key === 'range' || key === 'loc';
705
+ function traverseAst(node, visitor) {
706
+ if (visitor(node))
707
+ return true;
708
+ for (const key in node) {
709
+ if (isTraversalMetadataKey(key))
710
+ continue;
711
+ const value = node[key];
712
+ if (Array.isArray(value)) {
713
+ for (const child of value) {
714
+ if (child &&
715
+ typeof child === 'object' &&
716
+ child.type) {
717
+ if (traverseAst(child, visitor)) {
718
+ return true;
719
+ }
720
+ }
721
+ }
722
+ }
723
+ else if (value &&
724
+ typeof value === 'object' &&
725
+ value.type) {
726
+ if (traverseAst(value, visitor)) {
727
+ return true;
728
+ }
729
+ }
730
+ }
731
+ return false;
732
+ }
733
+ function collectReturnArguments(root) {
734
+ const results = [];
735
+ function walk(node) {
736
+ if (node.type === utils_1.AST_NODE_TYPES.ReturnStatement) {
737
+ results.push(node.argument ?? null);
738
+ return;
739
+ }
740
+ if (node !== root &&
741
+ (node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
742
+ node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
743
+ node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression)) {
744
+ return;
745
+ }
746
+ for (const key in node) {
747
+ if (isTraversalMetadataKey(key))
748
+ continue;
749
+ const value = node[key];
750
+ if (Array.isArray(value)) {
751
+ for (const child of value) {
752
+ if (child &&
753
+ typeof child === 'object' &&
754
+ child.type) {
755
+ walk(child);
756
+ }
757
+ }
758
+ }
759
+ else if (value &&
760
+ typeof value === 'object' &&
761
+ value.type) {
762
+ walk(value);
763
+ }
764
+ }
765
+ }
766
+ walk(root);
767
+ return results;
768
+ }
769
+ /**
770
+ * Check if a variable is used in a while loop condition
771
+ * This searches the scope for while loops that use the variable in their condition
772
+ */
773
+ function isUsedInWhileLoop(node) {
774
+ const variableName = node.name;
775
+ // Find the function or block scope containing this variable
776
+ let currentScope = node.parent;
777
+ while (currentScope &&
778
+ currentScope.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
779
+ currentScope = currentScope.parent;
780
+ }
781
+ if (!currentScope ||
782
+ currentScope.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
783
+ return false;
784
+ }
785
+ return traverseAst(currentScope, (searchNode) => {
786
+ return (searchNode.type === utils_1.AST_NODE_TYPES.WhileStatement &&
787
+ searchNode.test.type === utils_1.AST_NODE_TYPES.Identifier &&
788
+ searchNode.test.name === variableName);
789
+ });
790
+ }
791
+ /**
792
+ * Check if a variable declarator has a tree traversal pattern
793
+ * This looks for patterns like reassignment to .parent, .parentElement, etc.
794
+ */
795
+ function hasTreeTraversalPattern(declarator) {
796
+ if (declarator.id.type !== utils_1.AST_NODE_TYPES.Identifier) {
797
+ return false;
798
+ }
799
+ const variableName = declarator.id.name;
800
+ // Find the function or block scope containing this variable
801
+ let currentScope = declarator.parent;
802
+ while (currentScope &&
803
+ currentScope.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
804
+ currentScope = currentScope.parent;
805
+ }
806
+ if (!currentScope ||
807
+ currentScope.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
808
+ return false;
809
+ }
810
+ return traverseAst(currentScope, (node) => {
811
+ if (node.type === utils_1.AST_NODE_TYPES.AssignmentExpression &&
812
+ node.left.type === utils_1.AST_NODE_TYPES.Identifier &&
813
+ node.left.name === variableName &&
814
+ node.right.type === utils_1.AST_NODE_TYPES.MemberExpression &&
815
+ node.right.property.type === utils_1.AST_NODE_TYPES.Identifier) {
816
+ const propertyName = node.right.property
817
+ .name;
818
+ // Check for DOM traversal properties
819
+ if (propertyName === 'parentElement' ||
820
+ propertyName === 'parentNode' ||
821
+ propertyName === 'nextSibling' ||
822
+ propertyName === 'previousSibling' ||
823
+ propertyName === 'firstChild' ||
824
+ propertyName === 'lastChild') {
825
+ return true;
826
+ }
827
+ // Check for generic tree traversal properties
828
+ if (propertyName === 'parent' ||
829
+ propertyName === 'child' ||
830
+ propertyName === 'left' ||
831
+ propertyName === 'right' ||
832
+ propertyName === 'next' ||
833
+ propertyName === 'prev' ||
834
+ propertyName === 'previous') {
835
+ return true;
836
+ }
837
+ }
838
+ return false;
839
+ });
840
+ }
841
+ /**
842
+ * Check if a variable is used in a while loop condition and is likely a boolean
843
+ * This helps identify variables that should be flagged as needing a boolean prefix
844
+ */
845
+ function isLikelyBooleanInWhileLoop(node) {
846
+ // Check if the variable is initialized with a boolean-related value
847
+ const variableDeclarator = node.parent;
848
+ if (variableDeclarator?.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
849
+ variableDeclarator.init) {
850
+ const init = variableDeclarator.init;
851
+ // Check for direct boolean initialization
852
+ if (init.type === utils_1.AST_NODE_TYPES.Literal &&
853
+ typeof init.value === 'boolean') {
854
+ return true;
855
+ }
856
+ // Check for property access with boolean-suggesting name
857
+ if (init.type === utils_1.AST_NODE_TYPES.MemberExpression &&
858
+ init.property.type === utils_1.AST_NODE_TYPES.Identifier) {
859
+ const propertyName = init.property.name;
860
+ // If the property name suggests it's a boolean (starts with a boolean prefix)
861
+ const isBooleanProperty = approvedPrefixes.some((prefix) => propertyName.toLowerCase().startsWith(prefix.toLowerCase()));
862
+ if (isBooleanProperty) {
863
+ return true;
864
+ }
865
+ }
866
+ }
867
+ return false;
868
+ }
869
+ function getterReturnsBoolean(node) {
870
+ if (node.kind !== 'get') {
871
+ return false;
872
+ }
873
+ const functionLike = node.value;
874
+ const returnAnnotation = functionLike?.returnType?.typeAnnotation ||
875
+ node.returnType
876
+ ?.typeAnnotation;
877
+ if (returnAnnotation &&
878
+ returnAnnotation.type === utils_1.AST_NODE_TYPES.TSBooleanKeyword) {
879
+ return true;
880
+ }
881
+ if (!functionLike?.body ||
882
+ functionLike.body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
883
+ return false;
884
+ }
885
+ const returnArguments = collectReturnArguments(functionLike.body);
886
+ if (returnArguments.length === 0) {
887
+ return false;
888
+ }
889
+ for (const argument of returnArguments) {
890
+ const evaluation = evaluateBooleanishExpression(argument);
891
+ if (evaluation !== 'boolean') {
892
+ return false;
893
+ }
894
+ }
895
+ return true;
896
+ }
260
897
  /**
261
898
  * Check variable declarations for boolean naming
262
899
  */
@@ -267,8 +904,15 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
267
904
  // Skip checking if it's a type predicate
268
905
  if (isTypePredicate(node.id))
269
906
  return;
907
+ // Skip checking if it's likely a DOM element used in a while loop
908
+ if (isLikelyDomElementInWhileLoop(node.id))
909
+ return;
270
910
  // Check if it's a boolean variable
271
911
  let isBooleanVar = hasBooleanTypeAnnotation(node.id) || hasInitialBooleanValue(node);
912
+ // Check if it's a boolean variable used in a while loop
913
+ if (!isBooleanVar && isLikelyBooleanInWhileLoop(node.id)) {
914
+ isBooleanVar = true;
915
+ }
272
916
  // Check if it's an arrow function with boolean return type
273
917
  if (node.init?.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
274
918
  node.init.returnType?.typeAnnotation &&
@@ -283,6 +927,7 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
283
927
  data: {
284
928
  type: 'variable',
285
929
  name: variableName,
930
+ capitalizedName: capitalizeFirst(variableName),
286
931
  prefixes: formatPrefixes(),
287
932
  },
288
933
  });
@@ -327,6 +972,7 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
327
972
  data: {
328
973
  type: 'function',
329
974
  name: functionName,
975
+ capitalizedName: capitalizeFirst(functionName),
330
976
  prefixes: formatPrefixes(),
331
977
  },
332
978
  });
@@ -339,23 +985,34 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
339
985
  if (node.key.type !== utils_1.AST_NODE_TYPES.Identifier)
340
986
  return;
341
987
  const methodName = node.key.name;
988
+ const isGetter = node.kind === 'get';
989
+ const returnAnnotation = node.value?.returnType?.typeAnnotation ||
990
+ node.returnType
991
+ ?.typeAnnotation;
992
+ if (ignoreOverriddenGetters &&
993
+ isGetter &&
994
+ (node.override ||
995
+ node.abstract ||
996
+ !node.value?.body ||
997
+ node.type === utils_1.AST_NODE_TYPES.TSAbstractMethodDefinition)) {
998
+ return;
999
+ }
342
1000
  // Skip checking if it's a type predicate
343
- if (node.value.returnType?.typeAnnotation &&
344
- node.value.returnType.typeAnnotation.type ===
345
- utils_1.AST_NODE_TYPES.TSTypePredicate) {
1001
+ if (returnAnnotation &&
1002
+ returnAnnotation.type === utils_1.AST_NODE_TYPES.TSTypePredicate) {
346
1003
  return;
347
1004
  }
348
- // Check if it returns a boolean
349
- if (node.value.returnType?.typeAnnotation &&
350
- node.value.returnType.typeAnnotation.type ===
351
- utils_1.AST_NODE_TYPES.TSBooleanKeyword &&
352
- !hasApprovedPrefix(methodName)) {
1005
+ const returnsBoolean = (returnAnnotation &&
1006
+ returnAnnotation.type === utils_1.AST_NODE_TYPES.TSBooleanKeyword) ||
1007
+ (isGetter && getterReturnsBoolean(node));
1008
+ if (returnsBoolean && !hasApprovedPrefix(methodName)) {
353
1009
  context.report({
354
1010
  node: node.key,
355
1011
  messageId: 'missingBooleanPrefix',
356
1012
  data: {
357
- type: 'method',
1013
+ type: isGetter ? 'getter' : 'method',
358
1014
  name: methodName,
1015
+ capitalizedName: capitalizeFirst(methodName),
359
1016
  prefixes: formatPrefixes(),
360
1017
  },
361
1018
  });
@@ -388,6 +1045,7 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
388
1045
  data: {
389
1046
  type: 'property',
390
1047
  name: propertyName,
1048
+ capitalizedName: capitalizeFirst(propertyName),
391
1049
  prefixes: formatPrefixes(),
392
1050
  },
393
1051
  });
@@ -420,6 +1078,7 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
420
1078
  data: {
421
1079
  type: 'property',
422
1080
  name: propertyName,
1081
+ capitalizedName: capitalizeFirst(propertyName),
423
1082
  prefixes: formatPrefixes(),
424
1083
  },
425
1084
  });
@@ -429,12 +1088,96 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
429
1088
  * Check if an identifier is imported from an external module
430
1089
  */
431
1090
  function isImportedIdentifier(name) {
432
- const scope = context.getScope();
433
- const variable = scope.variables.find((v) => v.name === name);
434
- if (!variable)
1091
+ if (importStatusCache.has(name)) {
1092
+ const cached = importStatusCache.get(name);
1093
+ if (cached !== undefined)
1094
+ return cached;
1095
+ }
1096
+ const variable = findVariableInScopes(name);
1097
+ if (!variable) {
1098
+ importStatusCache.set(name, false);
435
1099
  return false;
436
- // Check if it's an import binding
437
- return variable.defs.some((def) => def.type === 'ImportBinding');
1100
+ }
1101
+ const isImport = variable.defs.some((def) => def.type === 'ImportBinding');
1102
+ importStatusCache.set(name, isImport);
1103
+ return isImport;
1104
+ }
1105
+ /**
1106
+ * Check if a variable is used with an external API
1107
+ */
1108
+ function isVariableUsedWithExternalApi(variableName) {
1109
+ if (externalApiUsageCache.has(variableName)) {
1110
+ const cached = externalApiUsageCache.get(variableName);
1111
+ if (cached !== undefined)
1112
+ return cached;
1113
+ }
1114
+ const variable = findVariableInScopes(variableName);
1115
+ if (!variable) {
1116
+ externalApiUsageCache.set(variableName, false);
1117
+ return false;
1118
+ }
1119
+ for (const reference of variable.references) {
1120
+ if (reference.identifier === variable.identifiers[0])
1121
+ continue;
1122
+ const id = reference.identifier;
1123
+ const markAndReturnTrue = () => {
1124
+ externalApiUsageCache.set(variableName, true);
1125
+ return true;
1126
+ };
1127
+ if (id.parent?.type === utils_1.AST_NODE_TYPES.Property &&
1128
+ id.parent.parent?.type === utils_1.AST_NODE_TYPES.ObjectExpression &&
1129
+ id.parent.parent.parent?.type === utils_1.AST_NODE_TYPES.CallExpression &&
1130
+ id.parent.parent.parent.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
1131
+ isImportedIdentifier(id.parent.parent.parent.callee.name)) {
1132
+ return markAndReturnTrue();
1133
+ }
1134
+ if (id.parent?.type === utils_1.AST_NODE_TYPES.CallExpression) {
1135
+ if (id.parent.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
1136
+ isImportedIdentifier(id.parent.callee.name)) {
1137
+ return markAndReturnTrue();
1138
+ }
1139
+ if (id.parent.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
1140
+ id.parent.callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
1141
+ isImportedIdentifier(id.parent.callee.object.name)) {
1142
+ return markAndReturnTrue();
1143
+ }
1144
+ }
1145
+ if (id.parent?.type === utils_1.AST_NODE_TYPES.JSXExpressionContainer &&
1146
+ id.parent.parent?.type === utils_1.AST_NODE_TYPES.JSXAttribute &&
1147
+ id.parent.parent.parent?.type === utils_1.AST_NODE_TYPES.JSXOpeningElement &&
1148
+ id.parent.parent.parent.name.type === utils_1.AST_NODE_TYPES.JSXIdentifier &&
1149
+ isImportedIdentifier(id.parent.parent.parent.name.name)) {
1150
+ return markAndReturnTrue();
1151
+ }
1152
+ if (id.parent?.type === utils_1.AST_NODE_TYPES.JSXSpreadAttribute &&
1153
+ id.parent.parent?.type === utils_1.AST_NODE_TYPES.JSXOpeningElement &&
1154
+ id.parent.parent.name.type === utils_1.AST_NODE_TYPES.JSXIdentifier &&
1155
+ isImportedIdentifier(id.parent.parent.name.name)) {
1156
+ return markAndReturnTrue();
1157
+ }
1158
+ }
1159
+ externalApiUsageCache.set(variableName, false);
1160
+ return false;
1161
+ }
1162
+ const HOOK_NAMES = new Set(['useMemo', 'useCallback', 'useState']);
1163
+ function findVariableFromReactHook(objectExpr) {
1164
+ let current = objectExpr;
1165
+ while (current) {
1166
+ if (current.type === utils_1.AST_NODE_TYPES.TSAsExpression) {
1167
+ current = current.parent;
1168
+ continue;
1169
+ }
1170
+ if (current.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
1171
+ current.parent?.type === utils_1.AST_NODE_TYPES.CallExpression &&
1172
+ current.parent.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
1173
+ HOOK_NAMES.has(current.parent.callee.name) &&
1174
+ current.parent.parent?.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
1175
+ current.parent.parent.id.type === utils_1.AST_NODE_TYPES.Identifier) {
1176
+ return current.parent.parent.id.name;
1177
+ }
1178
+ current = current.parent;
1179
+ }
1180
+ return undefined;
438
1181
  }
439
1182
  /**
440
1183
  * Check property definitions for boolean values
@@ -490,6 +1233,51 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
490
1233
  }
491
1234
  }
492
1235
  }
1236
+ // Check if this property is in an object literal that's being assigned to a variable
1237
+ // This handles cases like const messageInputProps = { grow: true }
1238
+ if (node.parent?.type === utils_1.AST_NODE_TYPES.ObjectExpression &&
1239
+ node.parent.parent?.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
1240
+ node.parent.parent.id.type === utils_1.AST_NODE_TYPES.Identifier) {
1241
+ const variableName = node.parent.parent.id.name;
1242
+ if (isVariableUsedWithExternalApi(variableName)) {
1243
+ isExternalApiCall = true;
1244
+ }
1245
+ }
1246
+ // Special case for useMemo and other React hooks
1247
+ // This handles cases like:
1248
+ // 1. const messageInputProps = useMemo(() => ({ grow: true }), [])
1249
+ // 2. const messageInputProps = useMemo(() => { return { grow: true }; }, [])
1250
+ if (node.parent?.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
1251
+ const hookVariable = findVariableFromReactHook(node.parent);
1252
+ if (hookVariable && isVariableUsedWithExternalApi(hookVariable)) {
1253
+ isExternalApiCall = true;
1254
+ }
1255
+ }
1256
+ // Check if this property is in an object literal that's directly passed to an imported function
1257
+ // This handles cases like ExternalComponent({ grow: true })
1258
+ if (node.parent?.type === utils_1.AST_NODE_TYPES.ObjectExpression &&
1259
+ node.parent.parent?.type === utils_1.AST_NODE_TYPES.CallExpression &&
1260
+ node.parent.parent.callee.type === utils_1.AST_NODE_TYPES.Identifier) {
1261
+ const calleeName = node.parent.parent.callee.name;
1262
+ if (isImportedIdentifier(calleeName)) {
1263
+ isExternalApiCall = true;
1264
+ }
1265
+ }
1266
+ // Check if this property is in an object literal that's directly passed as a JSX attribute
1267
+ // This handles cases like <ExternalComponent config={{ active: true }} />
1268
+ if (node.parent?.type === utils_1.AST_NODE_TYPES.ObjectExpression &&
1269
+ node.parent.parent?.type === utils_1.AST_NODE_TYPES.JSXExpressionContainer &&
1270
+ node.parent.parent.parent?.type === utils_1.AST_NODE_TYPES.JSXAttribute &&
1271
+ node.parent.parent.parent.parent?.type ===
1272
+ utils_1.AST_NODE_TYPES.JSXOpeningElement) {
1273
+ const jsxOpeningElement = node.parent.parent.parent.parent;
1274
+ if (jsxOpeningElement.name.type === utils_1.AST_NODE_TYPES.JSXIdentifier) {
1275
+ const componentName = jsxOpeningElement.name.name;
1276
+ if (isImportedIdentifier(componentName)) {
1277
+ isExternalApiCall = true;
1278
+ }
1279
+ }
1280
+ }
493
1281
  // Only report if it's not an external API call
494
1282
  if (!isExternalApiCall) {
495
1283
  context.report({
@@ -498,6 +1286,7 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
498
1286
  data: {
499
1287
  type: 'property',
500
1288
  name: propertyName,
1289
+ capitalizedName: capitalizeFirst(propertyName),
501
1290
  prefixes: formatPrefixes(),
502
1291
  },
503
1292
  });
@@ -541,6 +1330,7 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
541
1330
  data: {
542
1331
  type: 'property',
543
1332
  name: propertyName,
1333
+ capitalizedName: capitalizeFirst(propertyName),
544
1334
  prefixes: formatPrefixes(),
545
1335
  },
546
1336
  });
@@ -565,6 +1355,7 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
565
1355
  data: {
566
1356
  type: 'parameter',
567
1357
  name: paramName,
1358
+ capitalizedName: capitalizeFirst(paramName),
568
1359
  prefixes: formatPrefixes(),
569
1360
  },
570
1361
  });
@@ -587,6 +1378,7 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
587
1378
  data: {
588
1379
  type: 'property',
589
1380
  name: propertyName,
1381
+ capitalizedName: capitalizeFirst(propertyName),
590
1382
  prefixes: formatPrefixes(),
591
1383
  },
592
1384
  });
@@ -609,6 +1401,7 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
609
1401
  }
610
1402
  },
611
1403
  MethodDefinition: checkMethodDefinition,
1404
+ TSAbstractMethodDefinition: checkMethodDefinition,
612
1405
  Property: checkProperty,
613
1406
  ClassProperty: checkClassProperty,
614
1407
  PropertyDefinition: checkClassPropertyDeclaration,