@blumintinc/eslint-plugin-blumint 1.14.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 (49) hide show
  1. package/README.md +10 -11
  2. package/lib/index.js +14 -12
  3. package/lib/rules/avoid-utils-directory.js +0 -4
  4. package/lib/rules/consistent-callback-naming.js +42 -3
  5. package/lib/rules/dynamic-https-errors.d.ts +1 -1
  6. package/lib/rules/dynamic-https-errors.js +132 -41
  7. package/lib/rules/enforce-boolean-naming-prefixes.js +0 -212
  8. package/lib/rules/enforce-date-ttime.d.ts +1 -0
  9. package/lib/rules/enforce-date-ttime.js +156 -0
  10. package/lib/rules/enforce-dynamic-firebase-imports.d.ts +2 -1
  11. package/lib/rules/enforce-dynamic-firebase-imports.js +3 -2
  12. package/lib/rules/enforce-f-extension-for-entry-points.d.ts +8 -0
  13. package/lib/rules/enforce-f-extension-for-entry-points.js +283 -0
  14. package/lib/rules/enforce-global-constants.js +3 -3
  15. package/lib/rules/enforce-id-capitalization.js +1 -1
  16. package/lib/rules/enforce-memoize-async.js +69 -15
  17. package/lib/rules/enforce-props-argument-name.js +42 -16
  18. package/lib/rules/enforce-stable-hash-spread-props.js +3 -3
  19. package/lib/rules/enforce-transform-memoization.js +1 -1
  20. package/lib/rules/enforce-verb-noun-naming.js +3814 -4641
  21. package/lib/rules/global-const-style.js +16 -4
  22. package/lib/rules/memo-compare-deeply-complex-props.js +16 -3
  23. package/lib/rules/memo-nested-react-components.js +108 -103
  24. package/lib/rules/no-async-foreach.js +7 -2
  25. package/lib/rules/no-circular-references.d.ts +2 -1
  26. package/lib/rules/no-circular-references.js +13 -15
  27. package/lib/rules/no-console-error.js +12 -10
  28. package/lib/rules/no-empty-dependency-use-callbacks.js +1 -1
  29. package/lib/rules/no-entire-object-hook-deps.js +48 -1
  30. package/lib/rules/no-excessive-parent-chain.js +3 -0
  31. package/lib/rules/no-inline-component-prop.js +16 -7
  32. package/lib/rules/no-passthrough-getters.d.ts +2 -2
  33. package/lib/rules/no-passthrough-getters.js +83 -1
  34. package/lib/rules/no-redundant-this-params.js +50 -1
  35. package/lib/rules/no-unmemoized-memo-without-props.js +1 -1
  36. package/lib/rules/no-unnecessary-destructuring-rename.js +2 -5
  37. package/lib/rules/parallelize-async-operations.js +119 -54
  38. package/lib/rules/prefer-nullish-coalescing-boolean-props.js +109 -4
  39. package/lib/rules/prefer-params-over-parent-id.js +1 -1
  40. package/lib/rules/prefer-settings-object.js +27 -10
  41. package/lib/rules/prefer-type-alias-over-typeof-constant.js +9 -0
  42. package/lib/rules/prefer-usememo-over-useeffect-usestate.js +1 -1
  43. package/lib/rules/prevent-children-clobber.js +9 -5
  44. package/lib/rules/react-memoize-literals.js +131 -12
  45. package/lib/rules/require-https-error-cause.js +30 -11
  46. package/lib/rules/require-memo.js +17 -9
  47. package/lib/utils/ASTHelpers.d.ts +34 -1
  48. package/lib/utils/ASTHelpers.js +346 -112
  49. package/package.json +1 -1
@@ -47,6 +47,7 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
47
47
  defaultOptions: [defaultOptions],
48
48
  create(context, [options]) {
49
49
  const finalOptions = { ...defaultOptions, ...options };
50
+ const { sourceCode } = context;
50
51
  function getParameterType(param) {
51
52
  if (param.type === utils_1.AST_NODE_TYPES.AssignmentPattern) {
52
53
  return getParameterType(param.left);
@@ -56,13 +57,10 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
56
57
  const typeNode = param.typeAnnotation.typeAnnotation;
57
58
  if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
58
59
  // Include type parameters in the type signature to differentiate generic types
59
- const typeName = typeNode.typeName.type === utils_1.AST_NODE_TYPES.Identifier
60
- ? typeNode.typeName.name
61
- : 'unknown';
60
+ const typeName = sourceCode.getText(typeNode.typeName);
62
61
  // If there are type parameters, include them in the type signature
63
62
  if (typeNode.typeParameters &&
64
63
  typeNode.typeParameters.params.length > 0) {
65
- const sourceCode = context.sourceCode;
66
64
  const typeParams = typeNode.typeParameters.params
67
65
  .map((param) => sourceCode.getText(param))
68
66
  .join(', ');
@@ -76,13 +74,10 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
76
74
  const typeNode = param.typeAnnotation.typeAnnotation;
77
75
  if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
78
76
  // Include type parameters in the type signature to differentiate generic types
79
- const typeName = typeNode.typeName.type === utils_1.AST_NODE_TYPES.Identifier
80
- ? typeNode.typeName.name
81
- : 'unknown';
77
+ const typeName = sourceCode.getText(typeNode.typeName);
82
78
  // If there are type parameters, include them in the type signature
83
79
  if (typeNode.typeParameters &&
84
80
  typeNode.typeParameters.params.length > 0) {
85
- const sourceCode = context.sourceCode;
86
81
  const typeParams = typeNode.typeParameters.params
87
82
  .map((param) => sourceCode.getText(param))
88
83
  .join(', ');
@@ -98,12 +93,14 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
98
93
  return 'boolean';
99
94
  return typeNode.type;
100
95
  }
101
- return 'unknown';
96
+ return null;
102
97
  }
103
98
  function findDuplicateParameterType(params) {
104
99
  const typeMap = new Map();
105
100
  for (const param of params) {
106
101
  const type = getParameterType(param);
102
+ if (type === null)
103
+ continue;
107
104
  const count = (typeMap.get(type) || 0) + 1;
108
105
  typeMap.set(type, count);
109
106
  if (count > 1) {
@@ -248,6 +245,24 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
248
245
  }
249
246
  return false;
250
247
  }
248
+ function hasImplicitlyTypedParams(params) {
249
+ return params.some((param) => {
250
+ let current = param;
251
+ if (current.type === utils_1.AST_NODE_TYPES.TSParameterProperty) {
252
+ current = current.parameter;
253
+ }
254
+ if (current.type === utils_1.AST_NODE_TYPES.AssignmentPattern) {
255
+ current = current.left;
256
+ }
257
+ if (current.type === utils_1.AST_NODE_TYPES.Identifier ||
258
+ current.type === utils_1.AST_NODE_TYPES.ObjectPattern ||
259
+ current.type === utils_1.AST_NODE_TYPES.ArrayPattern ||
260
+ current.type === utils_1.AST_NODE_TYPES.RestElement) {
261
+ return !current.typeAnnotation;
262
+ }
263
+ return true;
264
+ });
265
+ }
251
266
  function shouldIgnoreNode(node) {
252
267
  // Ignore built-in objects and third-party modules
253
268
  if (isBuiltInOrThirdParty(node))
@@ -270,7 +285,7 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
270
285
  parent = parent.parent;
271
286
  }
272
287
  }
273
- // Ignore functions with A/B pattern parameters
288
+ // Ignore functions with A/B pattern parameters or missing type annotations
274
289
  if ((node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
275
290
  node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
276
291
  node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
@@ -279,6 +294,8 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
279
294
  Array.isArray(node.params)) {
280
295
  if (hasABPattern(node.params))
281
296
  return true;
297
+ if (hasImplicitlyTypedParams(node.params))
298
+ return true;
282
299
  }
283
300
  // Check if the function is a handler with transaction parameter
284
301
  // This is a common pattern in Firebase/Firestore handlers
@@ -225,6 +225,15 @@ exports.preferTypeAliasOverTypeofConstant = (0, createRule_1.createRule)({
225
225
  TSTypeQuery(node) {
226
226
  if (!collected)
227
227
  return;
228
+ // Skip if inside a type alias declaration (Issue #1117)
229
+ // This allows 'type T = typeof CONST' as the canonical way to define the alias.
230
+ let current = node.parent;
231
+ while (current) {
232
+ if (current.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) {
233
+ return;
234
+ }
235
+ current = current.parent;
236
+ }
228
237
  // Skip `keyof typeof X`
229
238
  if (node.parent &&
230
239
  node.parent.type === utils_1.AST_NODE_TYPES.TSTypeOperator &&
@@ -11,7 +11,7 @@ exports.preferUseMemoOverUseEffectUseState = (0, createRule_1.createRule)({
11
11
  recommended: 'error',
12
12
  },
13
13
  messages: {
14
- preferUseMemo: 'Derived state "{{stateName}}" is computed inside useEffect and copied into React state even though the value comes from a pure calculation. That extra render cycle and state indirection make components re-render more and risk stale snapshots when dependencies change. Compute the value with useMemo (or inline in render) and read it directly instead of mirroring it into state.',
14
+ preferUseMemo: 'Use useMemo to compute derived state "{{stateName}}" instead of useEffect + useState to avoid extra render cycles and stale snapshots.',
15
15
  },
16
16
  schema: [],
17
17
  },
@@ -44,12 +44,12 @@ function resolveFunctionName(node) {
44
44
  }
45
45
  return null;
46
46
  }
47
- function isComponentLike(node) {
47
+ function isComponentLike(node, context) {
48
48
  const name = resolveFunctionName(node);
49
49
  if (name && /^[A-Z]/.test(name)) {
50
50
  return true;
51
51
  }
52
- return ASTHelpers_1.ASTHelpers.returnsJSX(node.body);
52
+ return ASTHelpers_1.ASTHelpers.returnsJSX(node.body, context);
53
53
  }
54
54
  function patternHasChildrenProperty(pattern) {
55
55
  return pattern.properties.some((prop) => {
@@ -266,7 +266,8 @@ function bindingMayContainChildren(binding, context) {
266
266
  return false;
267
267
  if (binding.typeAnnotationExcludesProperty)
268
268
  return false;
269
- const services = context.sourceCode?.parserServices ?? context.parserServices;
269
+ const services = context.sourceCode?.parserServices ??
270
+ context.parserServices;
270
271
  if (!services?.program || !services?.esTreeNodeToTSNodeMap) {
271
272
  return true;
272
273
  }
@@ -410,7 +411,7 @@ exports.preventChildrenClobber = (0, createRule_1.createRule)({
410
411
  return {
411
412
  ':function'(node) {
412
413
  const ctx = {
413
- isComponent: isComponentLike(node),
414
+ isComponent: isComponentLike(node, context),
414
415
  bindings: new Map(),
415
416
  propsLikeIdentifiers: new Set(),
416
417
  childrenValueSourceIds: new Map(),
@@ -510,7 +511,10 @@ exports.preventChildrenClobber = (0, createRule_1.createRule)({
510
511
  if (!bindingMayContainChildren(binding, context)) {
511
512
  continue;
512
513
  }
513
- offendingSpreads.push({ name, childrenSourceId: binding.childrenSourceId });
514
+ offendingSpreads.push({
515
+ name,
516
+ childrenSourceId: binding.childrenSourceId,
517
+ });
514
518
  }
515
519
  if (offendingSpreads.length === 0)
516
520
  return;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.reactMemoizeLiterals = 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
  const LITERAL_DESCRIPTOR_BY_TYPE = {
7
8
  [utils_1.AST_NODE_TYPES.ObjectExpression]: {
8
9
  literalType: 'object literal',
@@ -39,6 +40,11 @@ const SAFE_HOOK_ARGUMENTS = new Set([
39
40
  'useDeferredValue',
40
41
  'useTransition',
41
42
  'useId',
43
+ 'useLatestCallback',
44
+ 'useDeepCompareMemo',
45
+ 'useDeepCompareCallback',
46
+ 'useDeepCompareEffect',
47
+ 'useProgressionCallback',
42
48
  ]);
43
49
  const MEMOIZATION_DEPS_TODO_PLACEHOLDER = '__TODO_MEMOIZATION_DEPENDENCIES__';
44
50
  const TODO_DEPS_COMMENT = `/* ${MEMOIZATION_DEPS_TODO_PLACEHOLDER} */`;
@@ -228,17 +234,26 @@ function findEnclosingComponentOrHook(node) {
228
234
  function isInsideAllowedHookCallback(node) {
229
235
  let current = node;
230
236
  while (current) {
231
- if (current.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
232
- current.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
233
- let parent = current.parent;
234
- while (parent && isExpressionWrapper(parent)) {
235
- parent = parent.parent;
237
+ if (isFunctionNode(current)) {
238
+ if (current.async && current !== node) {
239
+ return true;
236
240
  }
237
- if (parent?.type === utils_1.AST_NODE_TYPES.CallExpression) {
238
- const hookName = getHookNameFromCallee(parent.callee);
239
- const matchesCallback = parent.arguments.some((arg) => unwrapNestedExpressions(arg) === current);
240
- if (hookName && SAFE_HOOK_ARGUMENTS.has(hookName) && matchesCallback) {
241
- return true;
241
+ if (current.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
242
+ current.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
243
+ let parent = current.parent;
244
+ // Skip through TypeScript type assertions and parentheses to find
245
+ // the actual CallExpression that invokes the hook.
246
+ while (parent && isExpressionWrapper(parent)) {
247
+ parent = parent.parent;
248
+ }
249
+ if (parent?.type === utils_1.AST_NODE_TYPES.CallExpression) {
250
+ const hookName = getHookNameFromCallee(parent.callee);
251
+ const matchesCallback = parent.arguments.some((arg) => unwrapNestedExpressions(arg) === current);
252
+ if (hookName &&
253
+ SAFE_HOOK_ARGUMENTS.has(hookName) &&
254
+ matchesCallback) {
255
+ return true;
256
+ }
242
257
  }
243
258
  }
244
259
  }
@@ -427,16 +442,120 @@ exports.reactMemoizeLiterals = (0, createRule_1.createRule)({
427
442
  defaultOptions: [],
428
443
  create(context) {
429
444
  const sourceCode = context.getSourceCode();
445
+ /**
446
+ * Checks whether a VariableDeclarator's sole usage is being thrown
447
+ * in the same function scope.
448
+ */
449
+ function isVariableAlwaysThrown(declarator) {
450
+ const variables = ASTHelpers_1.ASTHelpers.getDeclaredVariables(context, declarator);
451
+ if (variables.length === 0) {
452
+ return false;
453
+ }
454
+ const variable = variables[0];
455
+ const usages = variable.references.filter((ref) => !ref.init);
456
+ // Variables with no usages (dead code) don't bypass memoization checks
457
+ // because we can't prove the literal is thrown.
458
+ if (usages.length === 0) {
459
+ return false;
460
+ }
461
+ const owningFunction = findOwningFunction(declarator);
462
+ return usages.every((ref) => {
463
+ let refParent = ref.identifier
464
+ .parent;
465
+ while (refParent) {
466
+ if (isFunctionNode(refParent)) {
467
+ // Stop at function boundaries: throws inside nested functions don't
468
+ // abort the outer render cycle, so they don't make the literal terminal
469
+ // from the perspective of the declaring function.
470
+ return false;
471
+ }
472
+ if (refParent.type === utils_1.AST_NODE_TYPES.ThrowStatement) {
473
+ // Found a throw in the same lexical scope as the usage.
474
+ // Verify the throw is in the same function where the variable was declared
475
+ // to ensure the throw terminates the render before memoization matters.
476
+ let throwCheckParent = refParent.parent;
477
+ while (throwCheckParent) {
478
+ if (isFunctionNode(throwCheckParent)) {
479
+ return throwCheckParent === owningFunction;
480
+ }
481
+ throwCheckParent =
482
+ throwCheckParent.parent;
483
+ }
484
+ return owningFunction === null;
485
+ }
486
+ if (refParent === declarator) {
487
+ break;
488
+ }
489
+ refParent = refParent.parent;
490
+ }
491
+ return false;
492
+ });
493
+ }
494
+ /**
495
+ * Checks whether a literal is destined to be thrown, making referential
496
+ * stability irrelevant.
497
+ */
498
+ function isTerminalUsage(node) {
499
+ let current = node.parent;
500
+ // Tracks whether the literal passed through a node that represents non-container usage.
501
+ // Once true, any throw we encounter won't count as "terminal" because the literal
502
+ // is already used in an expression that might need memoization.
503
+ let hasPassedForbiddenNode = false;
504
+ while (current) {
505
+ if (current.type === utils_1.AST_NODE_TYPES.ThrowStatement) {
506
+ return !hasPassedForbiddenNode;
507
+ }
508
+ if (current.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
509
+ current.id.type === utils_1.AST_NODE_TYPES.Identifier &&
510
+ current.init) {
511
+ if (!hasPassedForbiddenNode && isVariableAlwaysThrown(current)) {
512
+ return true;
513
+ }
514
+ }
515
+ // Stop at function boundaries as throws across functions are not "terminal"
516
+ // in the same render cycle sense for the current component/hook.
517
+ if (isFunctionNode(current)) {
518
+ break;
519
+ }
520
+ // We distinguish between "containers" (nodes that build up an error value
521
+ // or wrap an expression) and "usages" (nodes where the value is consumed
522
+ // in a way that might benefit from memoization).
523
+ //
524
+ // Containers (NewExpression for errors, wrappers, or object/array builders)
525
+ // are allowed because they are part of the path to a 'throw'.
526
+ // Other nodes (like function calls or being a prop) mark the literal as
527
+ // having a non-terminal usage.
528
+ //
529
+ // Note: AST_NODE_TYPES.CallExpression is intentionally excluded from allowed
530
+ // containers. Passing a literal into a CallExpression allows the callee to
531
+ // observe or store the reference, making its identity relevant even if
532
+ // the result is eventually thrown (e.g., const err = fn({ ... }); throw err;).
533
+ if (current.type !== utils_1.AST_NODE_TYPES.ObjectExpression &&
534
+ current.type !== utils_1.AST_NODE_TYPES.Property &&
535
+ current.type !== utils_1.AST_NODE_TYPES.ArrayExpression &&
536
+ current.type !== utils_1.AST_NODE_TYPES.NewExpression &&
537
+ current.type !== utils_1.AST_NODE_TYPES.ConditionalExpression &&
538
+ current.type !== utils_1.AST_NODE_TYPES.LogicalExpression &&
539
+ !isExpressionWrapper(current)) {
540
+ hasPassedForbiddenNode = true;
541
+ }
542
+ current = current.parent;
543
+ }
544
+ return false;
545
+ }
430
546
  function reportLiteral(node) {
431
547
  const descriptor = getLiteralDescriptor(node);
432
548
  if (!descriptor)
433
549
  return;
550
+ const owner = findEnclosingComponentOrHook(node);
551
+ if (!owner)
552
+ return;
434
553
  if (isInsideAllowedHookCallback(node)) {
435
554
  return;
436
555
  }
437
- const owner = findEnclosingComponentOrHook(node);
438
- if (!owner)
556
+ if (isTerminalUsage(node)) {
439
557
  return;
558
+ }
440
559
  const hookCall = findEnclosingHookCall(node);
441
560
  if (hookCall) {
442
561
  if (hookCall.isDirectArgument) {
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.requireHttpsErrorCause = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
+ const ASTHelpers_1 = require("../utils/ASTHelpers");
5
6
  const createRule_1 = require("../utils/createRule");
6
7
  const isHttpsErrorCallee = (callee) => {
7
8
  if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
@@ -42,17 +43,8 @@ exports.requireHttpsErrorCause = (0, createRule_1.createRule)({
42
43
  },
43
44
  defaultOptions: [],
44
45
  create(context) {
45
- const sourceCode = context.getSourceCode();
46
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
46
47
  const catchStack = [];
47
- // ESLint 9 moves getScope onto sourceCode; ESLint 8 exposes context.getScope().
48
- // This shim keeps the rule compatible until the codebase drops ESLint 8 support.
49
- const getScopeForNode = (node) => {
50
- const sourceCodeWithScope = sourceCode;
51
- if (typeof sourceCodeWithScope.getScope === 'function') {
52
- return sourceCodeWithScope.getScope(node);
53
- }
54
- return context.getScope();
55
- };
56
48
  const reportMissingCause = (node, catchName) => {
57
49
  context.report({
58
50
  node,
@@ -76,7 +68,7 @@ exports.requireHttpsErrorCause = (0, createRule_1.createRule)({
76
68
  if (!frame.paramName || identifier.name !== frame.paramName) {
77
69
  return false;
78
70
  }
79
- const variable = findVariableInScopeChain(getScopeForNode(identifier), identifier.name);
71
+ const variable = findVariableInScopeChain(ASTHelpers_1.ASTHelpers.getScope(context, identifier), identifier.name);
80
72
  return (variable?.defs.some((definition) => definition.type === 'CatchClause' && definition.node === frame.node) ?? false);
81
73
  };
82
74
  const validateHttpsError = (node) => {
@@ -95,6 +87,33 @@ exports.requireHttpsErrorCause = (0, createRule_1.createRule)({
95
87
  return;
96
88
  }
97
89
  const catchName = activeCatch.paramName;
90
+ // HttpsError accepts both positional and object-based constructors. This signature
91
+ // is supported for compatibility with HttpsError overloads, ensuring the rule
92
+ // can validate the 'cause' property within a settings object to preserve
93
+ // error chaining for better diagnostics.
94
+ if (node.arguments.length === 1 &&
95
+ node.arguments[0].type === utils_1.AST_NODE_TYPES.ObjectExpression) {
96
+ const settingsObj = node.arguments[0];
97
+ const causeProp = settingsObj.properties.find((prop) => prop.type === utils_1.AST_NODE_TYPES.Property &&
98
+ !prop.computed &&
99
+ ((prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
100
+ prop.key.name === 'cause') ||
101
+ (prop.key.type === utils_1.AST_NODE_TYPES.Literal &&
102
+ prop.key.value === 'cause')));
103
+ if (!causeProp) {
104
+ reportMissingCause(node, catchName);
105
+ return;
106
+ }
107
+ const causeValue = causeProp.value;
108
+ if (causeValue.type !== utils_1.AST_NODE_TYPES.Identifier) {
109
+ reportWrongCause(causeValue, catchName, sourceCode.getText(causeValue));
110
+ return;
111
+ }
112
+ if (!isCatchBindingReference(causeValue, activeCatch)) {
113
+ reportWrongCause(causeValue, catchName, sourceCode.getText(causeValue));
114
+ }
115
+ return;
116
+ }
98
117
  if (node.arguments.length < 4) {
99
118
  reportMissingCause(node, catchName);
100
119
  return;
@@ -8,21 +8,25 @@ const ASTHelpers_1 = require("../utils/ASTHelpers");
8
8
  const isComponentExplicitlyUnmemoized = (componentName) => componentName.toLowerCase().includes('unmemoized');
9
9
  function isFunction(node) {
10
10
  return (node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
11
- node.type === utils_1.AST_NODE_TYPES.FunctionExpression);
11
+ node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
12
+ node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration);
12
13
  }
13
- function isHigherOrderFunctionReturningJSX(node) {
14
+ function isHigherOrderFunctionReturningJSX(node, context) {
14
15
  if (isFunction(node)) {
15
- // Check if function takes another function as an argument
16
- const hasFunctionParam = 'params' in node && node.params.some(isFunction);
17
16
  if (node.body && node.body.type === 'BlockStatement') {
18
17
  for (const statement of node.body.body) {
19
18
  if (statement.type === 'ReturnStatement' && statement.argument) {
20
- const returnsJSX = ASTHelpers_1.ASTHelpers.returnsJSX(statement.argument);
19
+ const returnsJSX = ASTHelpers_1.ASTHelpers.returnsJSX(statement.argument, context);
21
20
  const returnsFunction = isFunction(statement.argument);
22
- return (hasFunctionParam || returnsFunction) && returnsJSX;
21
+ return returnsFunction && returnsJSX;
23
22
  }
24
23
  }
25
24
  }
25
+ else if (node.body && isFunction(node.body)) {
26
+ // Shorthand arrow HOC: (Comp) => (props) => <Comp {...props} />
27
+ // Here node.body is the inner function; check if it returns JSX.
28
+ return ASTHelpers_1.ASTHelpers.returnsJSX(node.body, context);
29
+ }
26
30
  }
27
31
  return false;
28
32
  }
@@ -52,14 +56,15 @@ function checkFunction(context, node) {
52
56
  if (!fileName.endsWith('.tsx')) {
53
57
  return;
54
58
  }
55
- if (isHigherOrderFunctionReturningJSX(node)) {
59
+ if (isHigherOrderFunctionReturningJSX(node, context)) {
56
60
  return;
57
61
  }
58
62
  const parentNode = node.parent;
59
63
  if (node.parent.type === 'CallExpression') {
60
64
  return;
61
65
  }
62
- if (ASTHelpers_1.ASTHelpers.returnsJSX(node.body) && ASTHelpers_1.ASTHelpers.hasParameters(node)) {
66
+ if (ASTHelpers_1.ASTHelpers.returnsJSX(node.body, context) &&
67
+ ASTHelpers_1.ASTHelpers.hasParameters(node)) {
63
68
  const results = [
64
69
  isUnmemoizedArrowFunction,
65
70
  isUnmemoizedFunctionComponent,
@@ -79,6 +84,9 @@ function checkFunction(context, node) {
79
84
  },
80
85
  fix: results[2] || results[1]
81
86
  ? function fix(fixer) {
87
+ if (node.async || node.generator) {
88
+ return null;
89
+ }
82
90
  const sourceCode = context.sourceCode;
83
91
  let importFix = null;
84
92
  // Search for memo import statement
@@ -118,7 +126,7 @@ function checkFunction(context, node) {
118
126
  const functionNameReplacement = `function ${node.id.name}Unmemoized`;
119
127
  const fixes = [
120
128
  fixer.replaceTextRange(functionKeywordRange, functionKeywordReplacement),
121
- fixer.insertTextAfterRange([node.range[1], node.range[1]], ')'),
129
+ fixer.insertTextAfterRange([node.range[1], node.range[1]], ');'),
122
130
  fixer.replaceTextRange([node.id.range[0] - 1, node.id.range[1]], functionNameReplacement),
123
131
  ];
124
132
  if (importFix) {
@@ -1,18 +1,51 @@
1
1
  import { TSESLint, TSESTree } from '@typescript-eslint/utils';
2
2
  import { Graph } from './graph/ClassGraphBuilder';
3
3
  export declare class ASTHelpers {
4
+ /**
5
+ * AST node shapes vary across ESLint/typescript-eslint versions, with some
6
+ * node types (ParenthesizedExpression, TSSatisfiesExpression) not consistently
7
+ * available in type definitions. Type guards and runtime checks ensure correctness
8
+ * despite these discrepancies, trading compile-time safety for cross-version
9
+ * compatibility until type definitions stabilize.
10
+ *
11
+ * Semantics Contract:
12
+ * - Helpers like getScope and returnsJSX must be invoked from the active
13
+ * visitor traversal context (ESLint 8 compatible).
14
+ * - returnsJSX is a heuristic and does not perform a full control-flow proof.
15
+ * - Reliance on runtime type guards (isParenthesizedExpression,
16
+ * isLoopOrLabeledStatement) is intentional.
17
+ */
4
18
  /**
5
19
  * Finds a variable by name in the scope chain starting from the given scope.
6
20
  */
7
21
  static findVariableInScope(scope: TSESLint.Scope.Scope, name: string): TSESLint.Scope.Variable | null;
22
+ /**
23
+ * Compatibility wrapper for getting the scope of a node across ESLint versions.
24
+ * ESLint 9 moves getScope onto sourceCode; ESLint 8 exposes context.getScope().
25
+ */
26
+ static getScope(context: Readonly<TSESLint.RuleContext<string, readonly unknown[]>>, node: TSESTree.Node): TSESLint.Scope.Scope;
8
27
  static blockIncludesIdentifier(block: TSESTree.BlockStatement): boolean;
9
28
  static declarationIncludesIdentifier(node: TSESTree.Node | null): boolean;
29
+ /**
30
+ * Checks if a pattern (in a declaration or parameter) contains any dependencies.
31
+ * Patterns themselves define new bindings (Identifiers), but they can contain
32
+ * dependencies in computed keys or default values (AssignmentPattern).
33
+ */
34
+ private static patternHasDependency;
10
35
  static classMethodDependenciesOf(node: TSESTree.Node | null, graph: Graph, className: string): string[];
11
36
  static isNode(value: unknown): value is TSESTree.Node;
12
37
  static hasReturnStatement(node: TSESTree.Node): boolean;
13
38
  static isNodeExported(node: TSESTree.Node): boolean;
14
- static returnsJSX(node: TSESTree.Node): boolean;
39
+ private static isLoopOrLabeledStatement;
40
+ private static isParenthesizedExpression;
41
+ private static returnsJSXValue;
42
+ private static returnsJSXFromStatement;
43
+ static returnsJSX(node: TSESTree.Node | null | undefined, context?: Readonly<TSESLint.RuleContext<string, readonly unknown[]>>): boolean;
15
44
  static hasParameters(node: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression | TSESTree.FunctionDeclaration): boolean;
45
+ /**
46
+ * Compatibility wrapper for getting declared variables across ESLint versions.
47
+ */
48
+ static getDeclaredVariables(context: Readonly<TSESLint.RuleContext<string, readonly unknown[]>>, node: TSESTree.Node): readonly TSESLint.Scope.Variable[];
16
49
  /**
17
50
  * Helper to get ancestors of a node in a way that is compatible with both ESLint v8 and v9.
18
51
  * In ESLint v9, context.getAncestors() is deprecated and moved to context.sourceCode.getAncestors(node).