@blumintinc/eslint-plugin-blumint 1.13.0 → 1.15.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 (199) hide show
  1. package/README.md +169 -139
  2. package/lib/index.js +200 -9
  3. package/lib/rules/array-methods-this-context.js +1 -1
  4. package/lib/rules/avoid-utils-directory.js +0 -4
  5. package/lib/rules/class-methods-read-top-to-bottom.js +24 -5
  6. package/lib/rules/consistent-callback-naming.js +43 -4
  7. package/lib/rules/dynamic-https-errors.d.ts +1 -1
  8. package/lib/rules/dynamic-https-errors.js +134 -44
  9. package/lib/rules/enforce-assert-safe-object-key.js +11 -11
  10. package/lib/rules/enforce-boolean-naming-prefixes.js +11 -235
  11. package/lib/rules/enforce-callback-memo.js +1 -1
  12. package/lib/rules/enforce-centralized-mock-firestore.js +1 -1
  13. package/lib/rules/enforce-console-error.js +35 -7
  14. package/lib/rules/enforce-date-ttime.d.ts +1 -0
  15. package/lib/rules/enforce-date-ttime.js +156 -0
  16. package/lib/rules/enforce-dynamic-file-naming.d.ts +2 -0
  17. package/lib/rules/enforce-dynamic-file-naming.js +53 -28
  18. package/lib/rules/enforce-dynamic-firebase-imports.d.ts +2 -1
  19. package/lib/rules/enforce-dynamic-firebase-imports.js +3 -2
  20. package/lib/rules/enforce-early-destructuring.d.ts +2 -0
  21. package/lib/rules/enforce-early-destructuring.js +980 -0
  22. package/lib/rules/enforce-empty-object-check.d.ts +11 -0
  23. package/lib/rules/enforce-empty-object-check.js +502 -0
  24. package/lib/rules/enforce-exported-function-types.js +1 -1
  25. package/lib/rules/enforce-f-extension-for-entry-points.d.ts +8 -0
  26. package/lib/rules/enforce-f-extension-for-entry-points.js +283 -0
  27. package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +100 -50
  28. package/lib/rules/enforce-firestore-doc-ref-generic.js +15 -3
  29. package/lib/rules/enforce-firestore-facade.js +4 -2
  30. package/lib/rules/enforce-firestore-set-merge.js +3 -4
  31. package/lib/rules/enforce-global-constants.js +4 -4
  32. package/lib/rules/enforce-id-capitalization.js +1 -1
  33. package/lib/rules/enforce-identifiable-firestore-type.js +108 -58
  34. package/lib/rules/enforce-memoize-async.js +71 -21
  35. package/lib/rules/enforce-memoize-getters.js +39 -15
  36. package/lib/rules/enforce-microdiff.js +1 -1
  37. package/lib/rules/enforce-object-literal-as-const.js +4 -4
  38. package/lib/rules/enforce-positive-naming.js +4 -0
  39. package/lib/rules/enforce-props-argument-name.js +44 -18
  40. package/lib/rules/enforce-safe-stringify.js +1 -1
  41. package/lib/rules/enforce-serializable-params.js +3 -3
  42. package/lib/rules/enforce-singular-type-names.js +1 -1
  43. package/lib/rules/enforce-stable-hash-spread-props.d.ts +13 -0
  44. package/lib/rules/enforce-stable-hash-spread-props.js +342 -0
  45. package/lib/rules/enforce-storage-context.d.ts +9 -0
  46. package/lib/rules/enforce-storage-context.js +646 -0
  47. package/lib/rules/enforce-timestamp-now.js +23 -7
  48. package/lib/rules/enforce-transform-memoization.d.ts +4 -0
  49. package/lib/rules/enforce-transform-memoization.js +416 -0
  50. package/lib/rules/enforce-typescript-markdown-code-blocks.js +7 -3
  51. package/lib/rules/enforce-unique-cursor-headers.d.ts +16 -0
  52. package/lib/rules/enforce-unique-cursor-headers.js +365 -0
  53. package/lib/rules/enforce-verb-noun-naming.js +3818 -4644
  54. package/lib/rules/ensure-pointer-events-none.js +28 -2
  55. package/lib/rules/export-if-in-doubt.js +27 -4
  56. package/lib/rules/extract-global-constants.js +40 -13
  57. package/lib/rules/fast-deep-equal-over-microdiff.js +29 -39
  58. package/lib/rules/firestore-transaction-reads-before-writes.js +50 -4
  59. package/lib/rules/flatten-push-calls.d.ts +2 -0
  60. package/lib/rules/flatten-push-calls.js +428 -0
  61. package/lib/rules/global-const-style.js +66 -23
  62. package/lib/rules/jsdoc-above-field.d.ts +11 -0
  63. package/lib/rules/jsdoc-above-field.js +208 -0
  64. package/lib/rules/key-only-outermost-element.js +21 -3
  65. package/lib/rules/logical-top-to-bottom-grouping.d.ts +5 -0
  66. package/lib/rules/logical-top-to-bottom-grouping.js +1258 -0
  67. package/lib/rules/memo-compare-deeply-complex-props.d.ts +3 -0
  68. package/lib/rules/memo-compare-deeply-complex-props.js +801 -0
  69. package/lib/rules/memo-nested-react-components.d.ts +8 -0
  70. package/lib/rules/memo-nested-react-components.js +415 -0
  71. package/lib/rules/no-always-true-false-conditions.d.ts +2 -1
  72. package/lib/rules/no-always-true-false-conditions.js +68 -8
  73. package/lib/rules/no-array-length-in-deps.js +9 -3
  74. package/lib/rules/no-async-foreach.js +215 -12
  75. package/lib/rules/no-circular-references.d.ts +2 -1
  76. package/lib/rules/no-circular-references.js +31 -32
  77. package/lib/rules/no-class-instance-destructuring.js +55 -12
  78. package/lib/rules/no-complex-cloud-params.js +7 -3
  79. package/lib/rules/no-compositing-layer-props.js +2 -2
  80. package/lib/rules/no-conditional-literals-in-jsx.js +37 -12
  81. package/lib/rules/no-console-error.d.ts +9 -0
  82. package/lib/rules/no-console-error.js +527 -0
  83. package/lib/rules/no-curly-brackets-around-commented-properties.d.ts +2 -0
  84. package/lib/rules/no-curly-brackets-around-commented-properties.js +258 -0
  85. package/lib/rules/no-empty-dependency-use-callbacks.d.ts +11 -0
  86. package/lib/rules/no-empty-dependency-use-callbacks.js +576 -0
  87. package/lib/rules/no-entire-object-hook-deps.js +52 -7
  88. package/lib/rules/no-excessive-parent-chain.js +4 -1
  89. package/lib/rules/no-explicit-return-type.d.ts +2 -1
  90. package/lib/rules/no-explicit-return-type.js +215 -26
  91. package/lib/rules/no-filter-without-return.js +5 -1
  92. package/lib/rules/no-firestore-jest-mock.d.ts +2 -1
  93. package/lib/rules/no-firestore-jest-mock.js +126 -8
  94. package/lib/rules/no-firestore-object-arrays.js +67 -12
  95. package/lib/rules/no-handler-suffix.d.ts +12 -0
  96. package/lib/rules/no-handler-suffix.js +305 -0
  97. package/lib/rules/no-hungarian.js +1 -1
  98. package/lib/rules/no-inline-component-prop.d.ts +10 -0
  99. package/lib/rules/no-inline-component-prop.js +465 -0
  100. package/lib/rules/no-jsx-in-hooks.js +6 -1
  101. package/lib/rules/no-jsx-whitespace-literal.js +8 -2
  102. package/lib/rules/no-margin-properties.js +6 -6
  103. package/lib/rules/no-memoize-on-static.js +9 -1
  104. package/lib/rules/no-misleading-boolean-prefixes.js +7 -3
  105. package/lib/rules/no-misused-switch-case.js +22 -1
  106. package/lib/rules/no-mixed-firestore-transactions.d.ts +3 -1
  107. package/lib/rules/no-mixed-firestore-transactions.js +296 -34
  108. package/lib/rules/no-mock-firebase-admin.js +5 -2
  109. package/lib/rules/no-object-values-on-strings.js +10 -2
  110. package/lib/rules/no-overridable-method-calls-in-constructor.js +29 -17
  111. package/lib/rules/no-passthrough-getters.d.ts +2 -2
  112. package/lib/rules/no-passthrough-getters.js +120 -2
  113. package/lib/rules/no-redundant-annotation-assertion.d.ts +2 -0
  114. package/lib/rules/no-redundant-annotation-assertion.js +402 -0
  115. package/lib/rules/no-redundant-param-types.js +16 -6
  116. package/lib/rules/no-redundant-this-params.d.ts +3 -0
  117. package/lib/rules/no-redundant-this-params.js +508 -0
  118. package/lib/rules/no-redundant-usecallback-wrapper.js +16 -3
  119. package/lib/rules/no-res-error-status-in-onrequest.d.ts +4 -0
  120. package/lib/rules/no-res-error-status-in-onrequest.js +521 -0
  121. package/lib/rules/no-restricted-properties-fix.js +11 -10
  122. package/lib/rules/no-separate-loading-state.js +7 -16
  123. package/lib/rules/no-stale-state-across-await.js +1 -1
  124. package/lib/rules/no-type-assertion-returns.js +43 -49
  125. package/lib/rules/no-undefined-null-passthrough.js +61 -31
  126. package/lib/rules/no-unmemoized-memo-without-props.d.ts +8 -0
  127. package/lib/rules/no-unmemoized-memo-without-props.js +426 -0
  128. package/lib/rules/no-unnecessary-destructuring-rename.d.ts +2 -0
  129. package/lib/rules/no-unnecessary-destructuring-rename.js +344 -0
  130. package/lib/rules/no-unnecessary-destructuring.js +14 -4
  131. package/lib/rules/no-unnecessary-verb-suffix.js +2 -1
  132. package/lib/rules/no-unpinned-dependencies.js +36 -5
  133. package/lib/rules/no-unused-props.d.ts +2 -2
  134. package/lib/rules/no-unused-props.js +295 -91
  135. package/lib/rules/no-unused-usestate.js +6 -2
  136. package/lib/rules/no-useless-fragment.js +28 -2
  137. package/lib/rules/no-useless-usememo-primitives.d.ts +9 -0
  138. package/lib/rules/no-useless-usememo-primitives.js +393 -0
  139. package/lib/rules/no-usememo-for-pass-by-value.d.ts +13 -0
  140. package/lib/rules/no-usememo-for-pass-by-value.js +757 -0
  141. package/lib/rules/no-uuidv4-base62-as-key.js +18 -13
  142. package/lib/rules/omit-index-html.d.ts +2 -1
  143. package/lib/rules/omit-index-html.js +62 -7
  144. package/lib/rules/optimize-object-boolean-conditions.js +6 -4
  145. package/lib/rules/parallelize-async-operations.js +143 -59
  146. package/lib/rules/prefer-batch-operations.d.ts +1 -3
  147. package/lib/rules/prefer-batch-operations.js +32 -5
  148. package/lib/rules/prefer-block-comments-for-declarations.js +9 -4
  149. package/lib/rules/prefer-clone-deep.js +3 -3
  150. package/lib/rules/prefer-destructuring-no-class.d.ts +2 -1
  151. package/lib/rules/prefer-destructuring-no-class.js +244 -59
  152. package/lib/rules/prefer-docsetter-setall.d.ts +2 -0
  153. package/lib/rules/prefer-docsetter-setall.js +243 -0
  154. package/lib/rules/prefer-field-paths-in-transforms.js +4 -3
  155. package/lib/rules/prefer-fragment-component.js +1 -1
  156. package/lib/rules/prefer-fragment-shorthand.js +2 -1
  157. package/lib/rules/prefer-getter-over-parameterless-method.d.ts +13 -0
  158. package/lib/rules/prefer-getter-over-parameterless-method.js +648 -0
  159. package/lib/rules/prefer-global-router-state-key.js +88 -13
  160. package/lib/rules/prefer-memoized-props.d.ts +3 -0
  161. package/lib/rules/prefer-memoized-props.js +445 -0
  162. package/lib/rules/prefer-next-dynamic.js +60 -69
  163. package/lib/rules/prefer-nullish-coalescing-boolean-props.js +118 -8
  164. package/lib/rules/prefer-nullish-coalescing-override.d.ts +2 -1
  165. package/lib/rules/prefer-nullish-coalescing-override.js +11 -5
  166. package/lib/rules/prefer-params-over-parent-id.js +221 -172
  167. package/lib/rules/prefer-settings-object.js +27 -10
  168. package/lib/rules/prefer-type-alias-over-typeof-constant.js +9 -0
  169. package/lib/rules/prefer-type-over-interface.js +7 -2
  170. package/lib/rules/prefer-url-tostring-over-tojson.js +6 -3
  171. package/lib/rules/prefer-use-deep-compare-memo.js +8 -5
  172. package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +1 -1
  173. package/lib/rules/prefer-usememo-over-useeffect-usestate.js +1 -1
  174. package/lib/rules/prefer-utility-function-over-private-static.js +34 -2
  175. package/lib/rules/prevent-children-clobber.d.ts +2 -0
  176. package/lib/rules/prevent-children-clobber.js +540 -0
  177. package/lib/rules/react-memoize-literals.d.ts +4 -0
  178. package/lib/rules/react-memoize-literals.js +614 -0
  179. package/lib/rules/react-usememo-should-be-component.js +2 -2
  180. package/lib/rules/require-hooks-default-params.js +17 -16
  181. package/lib/rules/require-https-error-cause.d.ts +4 -0
  182. package/lib/rules/require-https-error-cause.js +155 -0
  183. package/lib/rules/require-https-error.js +43 -21
  184. package/lib/rules/require-image-optimized.js +1 -1
  185. package/lib/rules/require-memo.js +18 -10
  186. package/lib/rules/require-memoize-jsx-returners.d.ts +3 -0
  187. package/lib/rules/require-memoize-jsx-returners.js +485 -0
  188. package/lib/rules/require-usememo-object-literals.js +2 -3
  189. package/lib/rules/test-file-location-enforcement.js +1 -1
  190. package/lib/rules/use-latest-callback.js +6 -5
  191. package/lib/rules/vertically-group-related-functions.d.ts +16 -0
  192. package/lib/rules/vertically-group-related-functions.js +480 -0
  193. package/lib/utils/ASTHelpers.d.ts +47 -2
  194. package/lib/utils/ASTHelpers.js +370 -112
  195. package/lib/utils/getMethodName.d.ts +27 -0
  196. package/lib/utils/getMethodName.js +35 -0
  197. package/lib/utils/tsTypeClassifier.d.ts +30 -0
  198. package/lib/utils/tsTypeClassifier.js +149 -0
  199. package/package.json +3 -1
@@ -12,6 +12,47 @@ const DEFAULT_USE_DYNAMIC_SOURCES = [
12
12
  function matchesAllowedSource(source, allowedSources) {
13
13
  return allowedSources.some((pattern) => source === pattern || source.endsWith(`/${pattern}`));
14
14
  }
15
+ function hasNodeStructure(value) {
16
+ return (!!value &&
17
+ typeof value === 'object' &&
18
+ 'type' in value &&
19
+ typeof value.type === 'string');
20
+ }
21
+ /**
22
+ * Walks the AST and invokes the visitor on each node.
23
+ * Uses WeakSet to prevent infinite cycles.
24
+ *
25
+ * @param visitor - Callback invoked on each node. Return `false` to stop
26
+ * traversal; return `undefined` (or nothing) to continue walking.
27
+ * @returns `false` if traversal stopped early; `true` when traversal completes.
28
+ */
29
+ function walkAst(node, visitor, visited = new WeakSet()) {
30
+ if (visited.has(node))
31
+ return true;
32
+ visited.add(node);
33
+ const shouldContinue = visitor(node);
34
+ if (shouldContinue === false)
35
+ return false;
36
+ const anyNode = node;
37
+ for (const key of Object.keys(anyNode)) {
38
+ if (key === 'parent')
39
+ continue;
40
+ const child = anyNode[key];
41
+ if (Array.isArray(child)) {
42
+ for (const c of child) {
43
+ if (hasNodeStructure(c)) {
44
+ if (!walkAst(c, visitor, visited))
45
+ return false;
46
+ }
47
+ }
48
+ }
49
+ else if (hasNodeStructure(child)) {
50
+ if (!walkAst(child, visitor, visited))
51
+ return false;
52
+ }
53
+ }
54
+ return true;
55
+ }
15
56
  function isUseDynamicCall(node) {
16
57
  const { callee, arguments: args } = node;
17
58
  if (callee.type === utils_1.AST_NODE_TYPES.Identifier &&
@@ -126,7 +167,7 @@ exports.preferNextDynamic = (0, createRule_1.createRule)({
126
167
  docs: {
127
168
  description: 'Prefer Next.js dynamic() over custom useDynamic() for component imports',
128
169
  recommended: 'error',
129
- requiresTypeChecking: true,
170
+ requiresTypeChecking: false,
130
171
  },
131
172
  fixable: 'code',
132
173
  schema: [
@@ -143,9 +184,9 @@ exports.preferNextDynamic = (0, createRule_1.createRule)({
143
184
  },
144
185
  ],
145
186
  messages: {
146
- preferNextDynamic: 'Use Next.js dynamic() instead of useDynamic(import(...)) for component imports',
147
- addNextDynamicImport: "Add default import: import dynamic from 'next/dynamic'",
148
- removeUseDynamicImport: 'Remove unused useDynamic import',
187
+ preferNextDynamic: 'Component "{{componentName}}" is created with useDynamic(import(...)), which bypasses Next.js dynamic() handling for client-only components and leaves SSR control to a custom wrapper. Wrap the import in dynamic(() => import(...), { ssr: false }) so Next.js manages code-splitting and disables server rendering safely.',
188
+ addNextDynamicImport: "The auto-fix will replace useDynamic(import(...)) with dynamic(() => import(...), { ssr: false }), which references Next.js's dynamic function. Without importing dynamic from 'next/dynamic', the fixed code will throw a ReferenceError at runtime when the module loads. Add `import dynamic from 'next/dynamic'` at the top of the file to make the dynamic identifier available.",
189
+ removeUseDynamicImport: 'Remove the unused useDynamic import after migrating to dynamic(); leaving the custom hook imported invites accidental reuse and keeps dead code in the bundle.',
149
190
  },
150
191
  },
151
192
  defaultOptions: [{}],
@@ -154,7 +195,7 @@ exports.preferNextDynamic = (0, createRule_1.createRule)({
154
195
  const allowedUseDynamicSources = options.useDynamicSources && options.useDynamicSources.length > 0
155
196
  ? options.useDynamicSources
156
197
  : DEFAULT_USE_DYNAMIC_SOURCES;
157
- const sourceCode = context.getSourceCode();
198
+ const sourceCode = context.sourceCode;
158
199
  return {
159
200
  VariableDeclarator(node) {
160
201
  const info = inferVariableInfo(node);
@@ -169,50 +210,18 @@ exports.preferNextDynamic = (0, createRule_1.createRule)({
169
210
  if (!useDynamicImportInfo)
170
211
  return;
171
212
  const identifierName = info.idText;
172
- let usedInJsx = false;
173
- const scopeBody = program.body;
174
- // Walk AST to find JSX usage; track visited nodes to avoid cycles
175
- // Note: We intentionally avoid relying on tokens here.
176
- // Fallback AST traversal for JSX usage
177
- const visited = new WeakSet();
178
- const checkJsxUsage = (n) => {
179
- if (visited.has(n))
180
- return;
181
- visited.add(n);
182
- if (usedInJsx)
183
- return;
213
+ // walkAst returns false when visitor returns false (early exit on match)
214
+ const usedInJsx = program.body.some((b) => walkAst(b, (n) => {
184
215
  if (n.type === utils_1.AST_NODE_TYPES.JSXOpeningElement) {
185
216
  const name = n.name;
186
217
  if (name.type === utils_1.AST_NODE_TYPES.JSXIdentifier) {
187
- if (name.name === identifierName)
188
- usedInJsx = true;
189
- }
190
- else if (name.type === utils_1.AST_NODE_TYPES.JSXMemberExpression) {
191
- // Not matching identifier simple usage
192
- }
193
- }
194
- // recurse
195
- const anyNode = n;
196
- for (const key of Object.keys(anyNode)) {
197
- if (key === 'parent')
198
- continue;
199
- const child = anyNode[key];
200
- if (Array.isArray(child)) {
201
- for (const c of child) {
202
- if (c && typeof c.type === 'string') {
203
- checkJsxUsage(c);
204
- }
205
- }
206
- }
207
- else if (child && typeof child === 'object') {
208
- if (child.type &&
209
- typeof child.type === 'string') {
210
- checkJsxUsage(child);
218
+ if (name.name === identifierName) {
219
+ return false;
211
220
  }
212
221
  }
213
222
  }
214
- };
215
- scopeBody.forEach((b) => checkJsxUsage(b));
223
+ return undefined;
224
+ }) === false);
216
225
  if (!usedInJsx) {
217
226
  // Skip to avoid flagging non-component dynamic imports
218
227
  return;
@@ -223,10 +232,11 @@ exports.preferNextDynamic = (0, createRule_1.createRule)({
223
232
  context.report({
224
233
  node: init,
225
234
  messageId: 'preferNextDynamic',
235
+ data: { componentName: identifierName },
226
236
  fix(fixer) {
227
237
  const fixes = [];
228
238
  // ensure dynamic import is present
229
- const programNode = findProgramNode(node);
239
+ const programNode = program;
230
240
  let dynamicLocal = getNextDynamicLocalName(programNode);
231
241
  const hasDynamic = !!dynamicLocal;
232
242
  if (!hasDynamic) {
@@ -266,33 +276,14 @@ exports.preferNextDynamic = (0, createRule_1.createRule)({
266
276
  const latestUseDynamicImport = findUseDynamicImport(programNode, allowedUseDynamicSources);
267
277
  if (latestUseDynamicImport) {
268
278
  // Abort removal if there are other useDynamic(import(...)) calls in the file
269
- let otherUseDynamicCalls = false;
270
- const visit = (n) => {
271
- if (otherUseDynamicCalls)
272
- return;
279
+ const otherUseDynamicCalls = programNode.body.some((b) => walkAst(b, (n) => {
273
280
  if (n.type === utils_1.AST_NODE_TYPES.CallExpression &&
274
- isUseDynamicCall(n)) {
275
- if (n !== init)
276
- otherUseDynamicCalls = true;
277
- }
278
- const anyNode = n;
279
- for (const key of Object.keys(anyNode)) {
280
- if (key === 'parent')
281
- continue;
282
- const child = anyNode[key];
283
- if (Array.isArray(child)) {
284
- for (const c of child)
285
- if (c && typeof c.type === 'string')
286
- visit(c);
287
- }
288
- else if (child &&
289
- typeof child === 'object' &&
290
- child.type) {
291
- visit(child);
292
- }
281
+ isUseDynamicCall(n) &&
282
+ n !== init) {
283
+ return false;
293
284
  }
294
- };
295
- programNode.body.forEach((b) => visit(b));
285
+ return undefined;
286
+ }) === false);
296
287
  if (otherUseDynamicCalls) {
297
288
  return fixes; // keep the import; other occurrences still rely on it
298
289
  }
@@ -1,9 +1,73 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
2
25
  Object.defineProperty(exports, "__esModule", { value: true });
3
26
  exports.preferNullishCoalescingBooleanProps = void 0;
4
27
  const utils_1 = require("@typescript-eslint/utils");
28
+ const ts = __importStar(require("typescript"));
5
29
  const createRule_1 = require("../utils/createRule");
6
30
  const BOOLEAN_PROP_REGEX = /^(is|has|should|can|will|do|does|did|was|were|enable|disable)/;
31
+ function isBooleanType(type, checker) {
32
+ if (type.isUnion()) {
33
+ return type.types.every((t) => isBooleanType(t, checker));
34
+ }
35
+ // For type parameters, check if the constraint is a boolean type
36
+ if (type.getFlags() & ts.TypeFlags.TypeParameter) {
37
+ if (checker) {
38
+ const constraint = checker.getBaseConstraintOfType(type);
39
+ if (constraint && constraint !== type) {
40
+ return isBooleanType(constraint, checker);
41
+ }
42
+ }
43
+ return false;
44
+ }
45
+ return ((type.getFlags() & (ts.TypeFlags.Boolean | ts.TypeFlags.BooleanLiteral)) !==
46
+ 0);
47
+ }
48
+ function isPossiblyNullish(type, checker) {
49
+ if (type.isUnion()) {
50
+ return type.types.some((t) => isPossiblyNullish(t, checker));
51
+ }
52
+ // For type parameters, check if the constraint excludes nullish
53
+ if (type.getFlags() & ts.TypeFlags.TypeParameter) {
54
+ if (checker) {
55
+ const constraint = checker.getBaseConstraintOfType(type);
56
+ if (constraint && constraint !== type) {
57
+ return isPossiblyNullish(constraint, checker);
58
+ }
59
+ }
60
+ // If we can't determine the constraint, assume it could be nullish
61
+ return true;
62
+ }
63
+ return ((type.getFlags() &
64
+ (ts.TypeFlags.Null |
65
+ ts.TypeFlags.Undefined |
66
+ ts.TypeFlags.Void |
67
+ ts.TypeFlags.Any |
68
+ ts.TypeFlags.Unknown)) !==
69
+ 0);
70
+ }
7
71
  function isInJSXBooleanAttribute(node) {
8
72
  const parent = node.parent;
9
73
  if (parent?.type !== utils_1.AST_NODE_TYPES.JSXAttribute)
@@ -57,7 +121,34 @@ function isInConditionalContext(node) {
57
121
  /**
58
122
  * Determines if a node is within a boolean context in JSX props or other boolean contexts
59
123
  */
60
- function isInBooleanContext(node) {
124
+ function isInBooleanContext(node, checker, parserServices) {
125
+ if (checker && parserServices) {
126
+ try {
127
+ const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
128
+ const contextualType = checker.getContextualType(tsNode);
129
+ if (contextualType && isBooleanType(contextualType, checker)) {
130
+ return true;
131
+ }
132
+ // Also check if the expression itself is a boolean type
133
+ const actualType = checker.getTypeAtLocation(tsNode);
134
+ if (actualType && isBooleanType(actualType, checker)) {
135
+ return true;
136
+ }
137
+ // If this is a logical expression, also check if the left operand is a boolean
138
+ if (node.type === utils_1.AST_NODE_TYPES.LogicalExpression) {
139
+ const leftTSNode = parserServices.esTreeNodeToTSNodeMap.get(node.left);
140
+ const leftType = checker.getTypeAtLocation(leftTSNode);
141
+ if (leftType && isBooleanType(leftType, checker)) {
142
+ return true;
143
+ }
144
+ }
145
+ }
146
+ catch {
147
+ // esTreeNodeToTSNodeMap may fail for synthetic nodes or nodes without
148
+ // source positions; getContextualType may return undefined for non-expression
149
+ // contexts. Fall back to AST-based heuristics in these cases.
150
+ }
151
+ }
61
152
  let current = node;
62
153
  // Traverse up the AST to find if we're in a boolean context
63
154
  while (current && current.parent) {
@@ -365,7 +456,19 @@ function isInBooleanContext(node) {
365
456
  /**
366
457
  * Checks if the left operand could be nullish (null or undefined)
367
458
  */
368
- function couldBeNullish(node) {
459
+ function couldBeNullish(node, checker, parserServices) {
460
+ if (checker && parserServices) {
461
+ try {
462
+ const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
463
+ const type = checker.getTypeAtLocation(tsNode);
464
+ return isPossiblyNullish(type, checker);
465
+ }
466
+ catch {
467
+ // esTreeNodeToTSNodeMap may fail for synthetic nodes or nodes without
468
+ // source positions; getTypeAtLocation may throw for nodes without
469
+ // type information. Fall back to manual check.
470
+ }
471
+ }
369
472
  // For literals, check the actual value
370
473
  if (node.type === utils_1.AST_NODE_TYPES.Literal) {
371
474
  return node.value === null || node.value === undefined;
@@ -396,29 +499,36 @@ exports.preferNullishCoalescingBooleanProps = (0, createRule_1.createRule)({
396
499
  },
397
500
  fixable: 'code',
398
501
  messages: {
399
- preferNullishCoalescing: 'Prefer using nullish coalescing operator (??) instead of logical OR operator (||) when checking for null/undefined',
502
+ preferNullishCoalescing: 'Logical OR between "{{left}}" and "{{right}}" treats every falsy value (false, 0, "", NaN) as missing and will override intentional boolean or empty states. Use the nullish coalescing operator (??) so "{{right}}" only applies when "{{left}}" is null or undefined, preserving explicit false/0/"" values.',
400
503
  },
401
504
  schema: [],
402
505
  },
403
506
  defaultOptions: [],
404
507
  create(context) {
508
+ const parserServices = utils_1.ESLintUtils.getParserServices(context, true);
509
+ const checker = parserServices?.program?.getTypeChecker();
405
510
  return {
406
511
  LogicalExpression(node) {
407
512
  if (node.operator === '||') {
408
513
  // If the node is in a boolean context, we allow logical OR
409
- if (isInBooleanContext(node)) {
514
+ if (isInBooleanContext(node, checker, parserServices)) {
410
515
  return;
411
516
  }
412
517
  // Check if this could benefit from nullish coalescing
413
518
  // We only suggest nullish coalescing when the left operand could be nullish
414
- if (couldBeNullish(node.left)) {
519
+ if (couldBeNullish(node.left, checker, parserServices)) {
520
+ const sourceCode = context.getSourceCode();
521
+ const leftText = sourceCode.getText(node.left);
522
+ const rightText = sourceCode.getText(node.right);
415
523
  context.report({
416
524
  node,
417
525
  messageId: 'preferNullishCoalescing',
526
+ data: {
527
+ left: leftText,
528
+ right: rightText,
529
+ },
418
530
  fix(fixer) {
419
- return fixer.replaceText(node, `${context.getSourceCode().getText(node.left)} ?? ${context
420
- .getSourceCode()
421
- .getText(node.right)}`);
531
+ return fixer.replaceText(node, `${leftText} ?? ${rightText}`);
422
532
  },
423
533
  });
424
534
  }
@@ -1,6 +1,7 @@
1
+ import { TSESLint } from '@typescript-eslint/utils';
1
2
  /**
2
3
  * This rule overrides the behavior of @typescript-eslint/prefer-nullish-coalescing
3
4
  * to only suggest using the nullish coalescing operator when checking for null/undefined,
4
5
  * not when intentionally checking for all falsy values.
5
6
  */
6
- export declare const preferNullishCoalescingOverride: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"preferNullishCoalescing", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
7
+ export declare const preferNullishCoalescingOverride: TSESLint.RuleModule<"preferNullishCoalescing", [], TSESLint.RuleListener>;
@@ -19,11 +19,13 @@ exports.preferNullishCoalescingOverride = (0, createRule_1.createRule)({
19
19
  fixable: 'code',
20
20
  schema: [],
21
21
  messages: {
22
- preferNullishCoalescing: 'Prefer using nullish coalescing operator (`??`) instead of logical OR operator (`||`) when only checking for null/undefined.',
22
+ preferNullishCoalescing: 'Replace "{{left}} || {{right}}" with nullish coalescing when you only want a fallback for null or undefined. The logical OR operator treats "", 0, and false as missing values and silently swaps in the fallback, which hides valid data. Use "{{left}} ?? {{right}}" so only nullish inputs trigger the fallback.',
23
23
  },
24
24
  },
25
25
  defaultOptions: [],
26
26
  create(context) {
27
+ const contextWithSource = context;
28
+ const sourceCode = contextWithSource.sourceCode ?? contextWithSource.getSourceCode();
27
29
  /**
28
30
  * Checks if a node is in a boolean context where truthiness matters
29
31
  */
@@ -154,7 +156,8 @@ exports.preferNullishCoalescingOverride = (0, createRule_1.createRule)({
154
156
  /**
155
157
  * Checks if this logical OR should be converted to nullish coalescing
156
158
  */
157
- function shouldConvertToNullishCoalescing(_node) {
159
+ function shouldConvertToNullishCoalescing(node) {
160
+ void node;
158
161
  return false;
159
162
  }
160
163
  void isInBooleanContext;
@@ -164,12 +167,15 @@ exports.preferNullishCoalescingOverride = (0, createRule_1.createRule)({
164
167
  return {
165
168
  LogicalExpression(node) {
166
169
  if (shouldConvertToNullishCoalescing(node)) {
167
- const sc = context.getSourceCode();
168
- const leftText = sc.getText(node.left);
169
- const rightText = sc.getText(node.right);
170
+ const leftText = sourceCode.getText(node.left);
171
+ const rightText = sourceCode.getText(node.right);
170
172
  context.report({
171
173
  node,
172
174
  messageId: 'preferNullishCoalescing',
175
+ data: {
176
+ left: leftText,
177
+ right: rightText,
178
+ },
173
179
  fix(fixer) {
174
180
  return fixer.replaceText(node, `${leftText} ?? ${rightText}`);
175
181
  },