@blumintinc/eslint-plugin-blumint 1.14.0 → 1.16.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 (68) hide show
  1. package/README.md +36 -14
  2. package/lib/index.js +20 -12
  3. package/lib/rules/avoid-utils-directory.js +0 -4
  4. package/lib/rules/consistent-callback-naming.js +68 -3
  5. package/lib/rules/dynamic-https-errors.d.ts +1 -1
  6. package/lib/rules/dynamic-https-errors.js +119 -49
  7. package/lib/rules/enforce-boolean-naming-prefixes.d.ts +1 -0
  8. package/lib/rules/enforce-boolean-naming-prefixes.js +86 -331
  9. package/lib/rules/enforce-date-ttime.d.ts +1 -0
  10. package/lib/rules/enforce-date-ttime.js +156 -0
  11. package/lib/rules/enforce-dynamic-firebase-imports.d.ts +2 -1
  12. package/lib/rules/enforce-dynamic-firebase-imports.js +3 -2
  13. package/lib/rules/enforce-dynamic-imports.d.ts +2 -1
  14. package/lib/rules/enforce-dynamic-imports.js +42 -21
  15. package/lib/rules/enforce-f-extension-for-entry-points.d.ts +8 -0
  16. package/lib/rules/enforce-f-extension-for-entry-points.js +283 -0
  17. package/lib/rules/enforce-global-constants.js +3 -3
  18. package/lib/rules/enforce-id-capitalization.js +1 -1
  19. package/lib/rules/enforce-memoize-async.js +66 -15
  20. package/lib/rules/enforce-mui-rounded-icons.js +42 -1
  21. package/lib/rules/enforce-props-argument-name.js +42 -16
  22. package/lib/rules/enforce-stable-hash-spread-props.js +3 -3
  23. package/lib/rules/enforce-transform-memoization.js +1 -1
  24. package/lib/rules/enforce-verb-noun-naming.js +3817 -4641
  25. package/lib/rules/global-const-style.js +25 -4
  26. package/lib/rules/logical-top-to-bottom-grouping.js +80 -2
  27. package/lib/rules/memo-compare-deeply-complex-props.js +183 -6
  28. package/lib/rules/memo-nested-react-components.js +243 -103
  29. package/lib/rules/no-array-length-in-deps.js +74 -3
  30. package/lib/rules/no-async-foreach.js +7 -2
  31. package/lib/rules/no-circular-references.d.ts +2 -1
  32. package/lib/rules/no-circular-references.js +150 -489
  33. package/lib/rules/no-compositing-layer-props.js +31 -0
  34. package/lib/rules/no-console-error.js +12 -10
  35. package/lib/rules/no-empty-dependency-use-callbacks.js +1 -1
  36. package/lib/rules/no-entire-object-hook-deps.js +147 -65
  37. package/lib/rules/no-excessive-parent-chain.js +3 -0
  38. package/lib/rules/no-explicit-return-type.js +6 -0
  39. package/lib/rules/no-hungarian.js +119 -24
  40. package/lib/rules/no-inline-component-prop.js +16 -7
  41. package/lib/rules/no-margin-properties.js +7 -38
  42. package/lib/rules/no-passthrough-getters.d.ts +2 -2
  43. package/lib/rules/no-passthrough-getters.js +83 -1
  44. package/lib/rules/no-redundant-this-params.js +50 -1
  45. package/lib/rules/no-unmemoized-memo-without-props.js +1 -1
  46. package/lib/rules/no-unnecessary-destructuring-rename.js +2 -5
  47. package/lib/rules/no-unnecessary-verb-suffix.js +79 -0
  48. package/lib/rules/no-unused-props.js +215 -37
  49. package/lib/rules/no-useless-fragment.js +10 -2
  50. package/lib/rules/parallelize-async-operations.js +117 -54
  51. package/lib/rules/prefer-nullish-coalescing-boolean-props.js +109 -4
  52. package/lib/rules/prefer-params-over-parent-id.js +1 -1
  53. package/lib/rules/prefer-settings-object.js +27 -10
  54. package/lib/rules/prefer-type-alias-over-typeof-constant.js +75 -4
  55. package/lib/rules/prefer-use-deep-compare-memo.js +8 -14
  56. package/lib/rules/prefer-usememo-over-useeffect-usestate.js +1 -1
  57. package/lib/rules/prevent-children-clobber.js +9 -5
  58. package/lib/rules/react-memoize-literals.js +218 -13
  59. package/lib/rules/require-https-error-cause.js +30 -11
  60. package/lib/rules/require-memo.js +17 -9
  61. package/lib/rules/require-migration-script-metadata.d.ts +9 -0
  62. package/lib/rules/require-migration-script-metadata.js +206 -0
  63. package/lib/rules/warn-https-error-message-user-friendly.d.ts +1 -0
  64. package/lib/rules/warn-https-error-message-user-friendly.js +239 -0
  65. package/lib/utils/ASTHelpers.d.ts +49 -1
  66. package/lib/utils/ASTHelpers.js +394 -112
  67. package/package.json +7 -6
  68. package/release-manifest.json +166 -0
@@ -4,12 +4,12 @@ exports.memoNestedReactComponents = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
5
  const minimatch_1 = require("minimatch");
6
6
  const createRule_1 = require("../utils/createRule");
7
- const ASTHelpers_1 = require("../utils/ASTHelpers");
8
- const CALLBACK_HOOKS = new Set(['useCallback', 'useDeepCompareCallback']);
9
- const HOOK_REPLACEMENT = {
10
- useCallback: 'useMemo',
11
- useDeepCompareCallback: 'useDeepCompareMemo',
12
- };
7
+ const CALLBACK_HOOKS = new Set([
8
+ 'useCallback',
9
+ 'useDeepCompareCallback',
10
+ 'useMemo',
11
+ 'useDeepCompareMemo',
12
+ ]);
13
13
  const collectReactImports = (sourceCode) => {
14
14
  const reactImports = { namespace: null, named: {} };
15
15
  for (const statement of sourceCode.ast.body) {
@@ -252,24 +252,142 @@ const getVariableName = (node) => {
252
252
  }
253
253
  return null;
254
254
  };
255
- const findMemoReference = (reactImports) => {
256
- if (reactImports.namespace) {
257
- return `${reactImports.namespace}.memo`;
255
+ const isEnclosingFunction = (node) => {
256
+ return (node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
257
+ node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
258
+ node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration);
259
+ };
260
+ /**
261
+ * Nearest ancestor function whose body directly contains `node`. Climbs from
262
+ * the node's parent so a function node itself is not treated as its own
263
+ * enclosing function.
264
+ */
265
+ const findEnclosingFunction = (node) => {
266
+ let parent = node.parent;
267
+ while (parent) {
268
+ if (isEnclosingFunction(parent)) {
269
+ return parent;
270
+ }
271
+ parent = parent.parent;
258
272
  }
259
- return reactImports.named.memo ?? null;
273
+ return null;
274
+ };
275
+ const isInsideFunction = (node) => {
276
+ return findEnclosingFunction(node) !== null;
260
277
  };
261
- const hasIdentifierInScope = (identifierName, scope) => {
262
- return !!ASTHelpers_1.ASTHelpers.findVariableInScope(scope, identifierName);
278
+ const isPascalCaseName = (name) => /^[A-Z]/.test(name);
279
+ /**
280
+ * A return value is a *component* (vs. rendered output) when it is a
281
+ * memo()/forwardRef() call, a PascalCase identifier (returned component
282
+ * reference), or an inline function that itself creates a component. Such a
283
+ * value identifies the surrounding function as an HOC factory rather than a
284
+ * render body.
285
+ */
286
+ const returnExpressionIsComponent = (expression, reactImports) => {
287
+ if (!expression) {
288
+ return false;
289
+ }
290
+ const unwrapped = unwrapExpression(expression);
291
+ if (unwrapped.type === utils_1.AST_NODE_TYPES.CallExpression) {
292
+ if (isMemoCall(unwrapped, reactImports) ||
293
+ isForwardRefCall(unwrapped, reactImports)) {
294
+ return true;
295
+ }
296
+ return false;
297
+ }
298
+ if (unwrapped.type === utils_1.AST_NODE_TYPES.Identifier) {
299
+ return isPascalCaseName(unwrapped.name);
300
+ }
301
+ if (isFunctionExpression(unwrapped)) {
302
+ return Boolean(functionCreatesComponent(unwrapped, reactImports));
303
+ }
304
+ return false;
305
+ };
306
+ const returnExpressionIsJsx = (expression) => {
307
+ if (!expression) {
308
+ return false;
309
+ }
310
+ const unwrapped = unwrapExpression(expression);
311
+ return (unwrapped.type === utils_1.AST_NODE_TYPES.JSXElement ||
312
+ unwrapped.type === utils_1.AST_NODE_TYPES.JSXFragment);
313
+ };
314
+ /**
315
+ * Direct return-statement arguments of a function, traversing control flow
316
+ * (if/switch/try/loops) but NOT descending into nested function definitions—
317
+ * returns inside nested functions belong to those functions, not this one.
318
+ */
319
+ const collectDirectReturnExpressions = (fn) => {
320
+ if (fn.body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
321
+ return [fn.body];
322
+ }
323
+ const returns = [];
324
+ const visitStatement = (statement) => {
325
+ switch (statement.type) {
326
+ case utils_1.AST_NODE_TYPES.ReturnStatement:
327
+ returns.push(statement.argument);
328
+ break;
329
+ case utils_1.AST_NODE_TYPES.BlockStatement:
330
+ statement.body.forEach(visitStatement);
331
+ break;
332
+ case utils_1.AST_NODE_TYPES.IfStatement:
333
+ visitStatement(statement.consequent);
334
+ if (statement.alternate) {
335
+ visitStatement(statement.alternate);
336
+ }
337
+ break;
338
+ case utils_1.AST_NODE_TYPES.SwitchStatement:
339
+ for (const switchCase of statement.cases) {
340
+ switchCase.consequent.forEach(visitStatement);
341
+ }
342
+ break;
343
+ case utils_1.AST_NODE_TYPES.ForStatement:
344
+ case utils_1.AST_NODE_TYPES.ForInStatement:
345
+ case utils_1.AST_NODE_TYPES.ForOfStatement:
346
+ case utils_1.AST_NODE_TYPES.WhileStatement:
347
+ case utils_1.AST_NODE_TYPES.DoWhileStatement:
348
+ visitStatement(statement.body);
349
+ break;
350
+ case utils_1.AST_NODE_TYPES.TryStatement:
351
+ visitStatement(statement.block);
352
+ if (statement.handler) {
353
+ visitStatement(statement.handler.body);
354
+ }
355
+ if (statement.finalizer) {
356
+ visitStatement(statement.finalizer);
357
+ }
358
+ break;
359
+ default:
360
+ break;
361
+ }
362
+ };
363
+ fn.body.body.forEach(visitStatement);
364
+ return returns;
365
+ };
366
+ /**
367
+ * True when the nearest enclosing function is an HOC factory—it returns a
368
+ * component (memo/forwardRef/component reference) and never returns JSX. Such
369
+ * a function runs once per call, so components defined inside it have stable
370
+ * identities and must NOT be flagged. A function that returns JSX is a render
371
+ * body, where nested components DO remount and remain flagged.
372
+ */
373
+ const isInsideHocFactory = (node, reactImports) => {
374
+ const enclosing = findEnclosingFunction(node);
375
+ if (!enclosing) {
376
+ return false;
377
+ }
378
+ const returns = collectDirectReturnExpressions(enclosing);
379
+ const returnsComponent = returns.some((expression) => returnExpressionIsComponent(expression, reactImports));
380
+ const returnsJsx = returns.some((expression) => returnExpressionIsJsx(expression));
381
+ return returnsComponent && !returnsJsx;
263
382
  };
264
383
  exports.memoNestedReactComponents = (0, createRule_1.createRule)({
265
384
  name: 'memo-nested-react-components',
266
385
  meta: {
267
386
  type: 'suggestion',
268
387
  docs: {
269
- description: 'Disallow React components defined in useCallback/useDeepCompareCallback',
388
+ description: 'Disallow React components defined in render bodies, hooks, or passed as props',
270
389
  recommended: 'error',
271
390
  },
272
- fixable: 'code',
273
391
  schema: [
274
392
  {
275
393
  type: 'object',
@@ -284,9 +402,19 @@ exports.memoNestedReactComponents = (0, createRule_1.createRule)({
284
402
  },
285
403
  ],
286
404
  messages: {
287
- memoizeNestedComponent: `What's wrong: React component "{{componentName}}" is created inside {{hookName}}.
288
- Why it matters: Components defined inside callbacks get new identities when the callback changes, causing React to remount them and drop their state and effects.
289
- How to fix: Create the component via {{replacementHook}} and wrap it in memo() so its identity stays stable across renders.`,
405
+ memoizeNestedComponent: `What's wrong: React component "{{componentName}}" is created inline inside {{locationDescription}}.
406
+
407
+ Why it matters: Inline components get new identities when their containing scope re-renders, causing React to unmount and remount them—dropping state, replaying animations, and causing UI flashes. Wrapping with memo() does NOT fix this—memo() only prevents re-renders when props change, not when the component identity itself changes.
408
+
409
+ How to fix:
410
+ 1. Define the component at MODULE SCOPE in its own file, wrapped with memo()
411
+ 2. Use React Context and/or directly provide props to supply any dynamic data the component needs
412
+ 3. Pass the stable, imported component reference to props like CatalogWrapper
413
+
414
+ Don't pass inline function components to component-type props (*Wrapper, *Component).
415
+ Render-prop callbacks (e.g., render={...}) are fine; this rule targets component-type props only.
416
+
417
+ See: https://react.dev/learn/your-first-component#nesting-and-organizing-components`,
290
418
  },
291
419
  },
292
420
  defaultOptions: [{}],
@@ -298,7 +426,16 @@ How to fix: Create the component via {{replacementHook}} and wrap it in memo() s
298
426
  }
299
427
  const sourceCode = context.sourceCode ?? context.getSourceCode();
300
428
  const reactImports = collectReactImports(sourceCode);
301
- const memoReference = findMemoReference(reactImports);
429
+ const reportNestedComponentViolation = (node, componentName, locationDescription) => {
430
+ context.report({
431
+ node,
432
+ messageId: 'memoizeNestedComponent',
433
+ data: {
434
+ componentName,
435
+ locationDescription,
436
+ },
437
+ });
438
+ };
302
439
  return {
303
440
  CallExpression(node) {
304
441
  const hook = isHookCall(node.callee);
@@ -314,95 +451,98 @@ How to fix: Create the component via {{replacementHook}} and wrap it in memo() s
314
451
  if (!componentMatch) {
315
452
  return;
316
453
  }
317
- const componentName = getVariableName(node) ??
454
+ // For useMemo, it only counts as a component if it returns a function
455
+ if (hook.name.includes('useMemo') ||
456
+ hook.name.includes('useDeepCompareMemo')) {
457
+ if (componentMatch.componentIsCallback) {
458
+ // Returns JSX directly, so it's an element, not a component
459
+ return;
460
+ }
461
+ }
462
+ const variableName = getVariableName(node);
463
+ // A non-PascalCase binding (e.g. renderHit) is a render callback used
464
+ // with a render={...} prop, not a component—skip it.
465
+ if (variableName && !isPascalCaseName(variableName)) {
466
+ return;
467
+ }
468
+ // Inside an HOC factory the binding has a stable identity, so it does
469
+ // not remount on re-render and must not be flagged.
470
+ if (isInsideHocFactory(node, reactImports)) {
471
+ return;
472
+ }
473
+ const componentName = variableName ??
318
474
  (isFunctionExpression(callback) &&
319
475
  callback.id?.type === utils_1.AST_NODE_TYPES.Identifier
320
476
  ? callback.id.name
321
477
  : 'component');
322
- const replacementHook = HOOK_REPLACEMENT[hook.name];
323
- context.report({
324
- node,
325
- messageId: 'memoizeNestedComponent',
326
- data: {
327
- componentName,
328
- hookName: `${hook.name}()`,
329
- replacementHook: `${replacementHook}()`,
330
- },
331
- fix: (fixer) => {
332
- if (!memoReference) {
333
- return null;
334
- }
335
- const memoNamespace = memoReference.endsWith('.memo')
336
- ? memoReference.slice(0, -'.memo'.length)
337
- : null;
338
- // Prefer SourceCode#getScope when available; fall back to RuleContext#getScope for ESLint compatibility.
339
- const sourceCodeWithScope = sourceCode;
340
- const scope = typeof sourceCodeWithScope.getScope === 'function'
341
- ? sourceCodeWithScope.getScope(node)
342
- : context.getScope();
343
- const replacementIdentifierAvailable = hook.name === 'useCallback'
344
- ? hasIdentifierInScope(HOOK_REPLACEMENT.useCallback, scope)
345
- : hasIdentifierInScope(HOOK_REPLACEMENT.useDeepCompareCallback, scope);
346
- if (!replacementIdentifierAvailable) {
347
- // When the standalone replacement hook (useMemo/useDeepCompareMemo) is not imported,
348
- // we can only fix member expressions that use the React namespace.
349
- if (node.callee.type !== utils_1.AST_NODE_TYPES.MemberExpression ||
350
- node.callee.object.type !== utils_1.AST_NODE_TYPES.Identifier) {
351
- return null;
352
- }
353
- // Ensure memo's namespace matches the hook's namespace (e.g., React.useCallback → React.useMemo + React.memo).
354
- if (memoReference &&
355
- memoReference.includes('.') &&
356
- memoReference !== `${node.callee.object.name}.memo`) {
357
- return null;
358
- }
359
- }
360
- // Component factories (functions returning functions that return JSX) require manual refactoring.
361
- if (!componentMatch.componentIsCallback) {
362
- return null;
363
- }
364
- // Ensure callback is a function expression before attempting to wrap it.
365
- if (!isFunctionExpression(callback)) {
366
- return null;
367
- }
368
- // React.memo() expects a synchronous component function. Wrapping async functions or
369
- // generators would produce a Promise/yielding component, which is not renderable.
370
- if (callback.async ||
371
- (callback.type === utils_1.AST_NODE_TYPES.FunctionExpression &&
372
- callback.generator)) {
373
- return null;
374
- }
375
- // Avoid rewriting non-React namespaces (e.g., Hooks.useCallback Hooks.useMemo),
376
- // even when useMemo/memo are available as named imports.
377
- if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression) {
378
- if (node.callee.object.type !== utils_1.AST_NODE_TYPES.Identifier ||
379
- !reactImports.namespace ||
380
- node.callee.object.name !== reactImports.namespace) {
381
- return null;
382
- }
383
- }
384
- // Prevent mixing namespaces: don't apply fix when hook and memo use different React imports.
385
- if (memoNamespace &&
386
- node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
387
- node.callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
388
- node.callee.object.name !== memoNamespace) {
389
- return null;
390
- }
391
- const callbackText = sourceCode.getText(callback);
392
- const callbackReplacement = `() => ${memoReference}(${callbackText})`;
393
- const fixes = [
394
- fixer.replaceText(callback, callbackReplacement),
395
- ];
396
- if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier) {
397
- fixes.push(fixer.replaceText(node.callee, HOOK_REPLACEMENT[node.callee.name]));
398
- }
399
- else if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
400
- node.callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
401
- fixes.push(fixer.replaceText(node.callee.property, HOOK_REPLACEMENT[node.callee.property.name]));
402
- }
403
- return fixes;
404
- },
405
- });
478
+ reportNestedComponentViolation(node, componentName, `${hook.name}()`);
479
+ },
480
+ VariableDeclarator(node) {
481
+ if (!node.init)
482
+ return;
483
+ if (node.id.type !== utils_1.AST_NODE_TYPES.Identifier)
484
+ return;
485
+ // Only check if name starts with uppercase (convention for components)
486
+ if (!isPascalCaseName(node.id.name))
487
+ return;
488
+ if (!isInsideFunction(node))
489
+ return;
490
+ // Inside an HOC factory the component has a stable identity (the factory
491
+ // runs once per call), so it does not remount and must not be flagged.
492
+ if (isInsideHocFactory(node, reactImports))
493
+ return;
494
+ // Skip if it's already a hook call (handled by CallExpression visitor)
495
+ if (node.init.type === utils_1.AST_NODE_TYPES.CallExpression) {
496
+ const hook = isHookCall(node.init.callee);
497
+ if (hook)
498
+ return;
499
+ }
500
+ const componentMatch = expressionCreatesComponent(node.init, reactImports);
501
+ if (!componentMatch)
502
+ return;
503
+ // JSX elements assigned to variables are fine, only report function definitions
504
+ if (componentMatch.componentIsCallback)
505
+ return;
506
+ reportNestedComponentViolation(node, node.id.name, 'a render body');
507
+ },
508
+ FunctionDeclaration(node) {
509
+ if (!node.id || !isPascalCaseName(node.id.name))
510
+ return;
511
+ if (!isInsideFunction(node))
512
+ return;
513
+ // Inside an HOC factory the component has a stable identity (the factory
514
+ // runs once per call), so it does not remount and must not be flagged.
515
+ if (isInsideHocFactory(node, reactImports))
516
+ return;
517
+ const componentMatch = functionCreatesComponent(node, reactImports);
518
+ if (!componentMatch)
519
+ return;
520
+ reportNestedComponentViolation(node.id, node.id.name, 'a render body');
521
+ },
522
+ JSXAttribute(node) {
523
+ if (node.name.type !== utils_1.AST_NODE_TYPES.JSXIdentifier)
524
+ return;
525
+ const attrName = node.name.name;
526
+ // Check if it's a component-type prop
527
+ if (!/(Wrapper|Component|Template|Header|Footer)$/.test(attrName)) {
528
+ return;
529
+ }
530
+ if (!node.value ||
531
+ node.value.type !== utils_1.AST_NODE_TYPES.JSXExpressionContainer) {
532
+ return;
533
+ }
534
+ const expression = node.value.expression;
535
+ if (expression.type === utils_1.AST_NODE_TYPES.JSXEmptyExpression)
536
+ return;
537
+ const componentMatch = expressionCreatesComponent(expression, reactImports);
538
+ if (!componentMatch)
539
+ return;
540
+ // For props, we only report if it's a function (component definition)
541
+ if (componentMatch.componentIsCallback) {
542
+ // It's a JSX element passed directly, which is usually fine
543
+ return;
544
+ }
545
+ reportNestedComponentViolation(node, attrName, `the "${attrName}" prop`);
406
546
  },
407
547
  };
408
548
  },
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.noArrayLengthInDeps = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
5
  const createRule_1 = require("../utils/createRule");
6
+ const ASTHelpers_1 = require("../utils/ASTHelpers");
6
7
  // React hooks to check
7
8
  const HOOK_NAMES = new Set(['useEffect', 'useCallback', 'useMemo']);
8
9
  const DEFAULT_HASH_IMPORT = {
@@ -58,7 +59,7 @@ function getLastPropertyName(expr) {
58
59
  return null;
59
60
  }
60
61
  function generateUniqueName(base, taken) {
61
- let candidate = `${base}Hash`;
62
+ const candidate = `${base}Hash`;
62
63
  if (!taken.has(candidate))
63
64
  return candidate;
64
65
  let i = 2;
@@ -120,6 +121,69 @@ function isUseMemoImported(sourceCode) {
120
121
  }
121
122
  return false;
122
123
  }
124
+ /**
125
+ * Hook callback = first function-typed argument (the effect/factory/memo fn).
126
+ * Deps array is the LAST argument; the callback precedes it.
127
+ */
128
+ function getHookCallback(node) {
129
+ for (const arg of node.arguments) {
130
+ if (arg.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
131
+ arg.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
132
+ return arg;
133
+ }
134
+ }
135
+ return null;
136
+ }
137
+ function rangeContains(outer, inner) {
138
+ return inner.range[0] >= outer.range[0] && inner.range[1] <= outer.range[1];
139
+ }
140
+ /**
141
+ * True when `identifier` is the object of a non-computed `.length` access,
142
+ * i.e. the reference reads only the array's length, not its contents.
143
+ */
144
+ function isLengthAccessOf(identifier) {
145
+ const parent = identifier.parent;
146
+ return (!!parent &&
147
+ parent.type === utils_1.AST_NODE_TYPES.MemberExpression &&
148
+ !parent.computed &&
149
+ parent.object === identifier &&
150
+ parent.property.type === utils_1.AST_NODE_TYPES.Identifier &&
151
+ parent.property.name === 'length');
152
+ }
153
+ /**
154
+ * Decide whether a `<array>.length` dependency is safe to keep (suppress the
155
+ * report) by inspecting how the hook callback body uses the array binding.
156
+ *
157
+ * Safe (suppress) only when the array is referenced at least once inside the
158
+ * callback body AND every such reference is the object of a `.length` access —
159
+ * then depending on `.length` correctly avoids reruns on content changes.
160
+ *
161
+ * Returns false (keep reporting) for any non-`.length` use (element access,
162
+ * spread, iteration/method calls, bare reference, passed as an argument), when
163
+ * the array is never referenced in the body, and whenever the base cannot be
164
+ * confidently resolved (complex member-chain bases) — never hide a real bug.
165
+ */
166
+ function isLengthOnlyUsage(context, callback, baseExpr) {
167
+ // Only resolvable for a bare identifier base (e.g. `items` in `items.length`).
168
+ // Member-chain bases (`a.b`, `data?.items`) have no single binding to track.
169
+ if (baseExpr.type !== utils_1.AST_NODE_TYPES.Identifier) {
170
+ return false;
171
+ }
172
+ // Resolve the binding the DEP refers to (from the deps-array identifier's
173
+ // scope), so a binding shadowed inside the callback body is not mistaken for
174
+ // it — the outer binding then has zero body references and we keep reporting.
175
+ const scope = ASTHelpers_1.ASTHelpers.getScope(context, baseExpr);
176
+ const variable = ASTHelpers_1.ASTHelpers.findVariableInScope(scope, baseExpr.name);
177
+ if (!variable) {
178
+ return false;
179
+ }
180
+ const bodyReferences = variable.references.filter((ref) => rangeContains(callback.body, ref.identifier));
181
+ // No body usage → not the content-vs-length bug, but keep current behavior.
182
+ if (bodyReferences.length === 0) {
183
+ return false;
184
+ }
185
+ return bodyReferences.every((ref) => isLengthAccessOf(ref.identifier));
186
+ }
123
187
  function isStableHashImported(sourceCode, hashSource, hashImportName) {
124
188
  const program = sourceCode.ast;
125
189
  for (const node of program.body) {
@@ -188,15 +252,22 @@ exports.noArrayLengthInDeps = (0, createRule_1.createRule)({
188
252
  return;
189
253
  // Collect .length deps
190
254
  const lengthDeps = [];
255
+ const callback = getHookCallback(node);
191
256
  for (const el of depsArg.elements) {
192
257
  if (!el)
193
258
  continue;
194
259
  if (el.type === utils_1.AST_NODE_TYPES.SpreadElement)
195
260
  continue;
196
261
  const member = getLengthMember(el);
197
- if (member) {
198
- lengthDeps.push({ element: el, member });
262
+ if (!member)
263
+ continue;
264
+ // Suppress when the callback body reads only `<array>.length`, never
265
+ // the array's contents — then `.length` is the correct dependency.
266
+ if (callback &&
267
+ isLengthOnlyUsage(context, callback, getBaseExpression(member))) {
268
+ continue;
199
269
  }
270
+ lengthDeps.push({ element: el, member });
200
271
  }
201
272
  if (lengthDeps.length === 0)
202
273
  return;
@@ -82,8 +82,13 @@ const getSourceCode = (context) => {
82
82
  `getSourceCode=${typeof context.getSourceCode}.`);
83
83
  };
84
84
  const getScope = (context, sourceCode, node) => {
85
- const typedSourceCode = sourceCode;
86
- return typedSourceCode.getScope?.(node) ?? context.getScope?.() ?? null;
85
+ try {
86
+ const typedSourceCode = sourceCode;
87
+ return typedSourceCode.getScope?.(node) ?? context.getScope?.() ?? null;
88
+ }
89
+ catch {
90
+ return context.getScope?.() ?? null;
91
+ }
87
92
  };
88
93
  const analyzeInlineCallback = (callback) => {
89
94
  if (!callback.async) {
@@ -1 +1,2 @@
1
- export declare const noCircularReferences: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"circularReference", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
1
+ import { TSESLint } from '@typescript-eslint/utils';
2
+ export declare const noCircularReferences: TSESLint.RuleModule<"circularReference", [], TSESLint.RuleListener>;