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