@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
@@ -0,0 +1,283 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.enforceFExtensionForEntryPoints = void 0;
7
+ const utils_1 = require("@typescript-eslint/utils");
8
+ const createRule_1 = require("../utils/createRule");
9
+ const path_1 = __importDefault(require("path"));
10
+ const ASTHelpers_1 = require("../utils/ASTHelpers");
11
+ const DEFAULT_ENTRY_POINTS = [
12
+ 'onCall',
13
+ 'onCallVaripotent',
14
+ 'onRequest',
15
+ 'onQueueTask',
16
+ 'onWebhook',
17
+ 'sequentialDocumentWritten',
18
+ 'onDocumentWritten',
19
+ 'onDocumentCreated',
20
+ 'onDocumentDeleted',
21
+ 'onDocumentUpdated',
22
+ 'onSchedule',
23
+ 'onValueWritten',
24
+ 'onValueCreated',
25
+ 'onValueUpdated',
26
+ 'onValueDeleted',
27
+ 'sequentialValueWritten',
28
+ 'sequentialValueCreated',
29
+ 'sequentialValueUpdated',
30
+ 'sequentialValueDeleted',
31
+ ];
32
+ /**
33
+ * Entry points must be defined at the top level because Firebase deployment
34
+ * discovers and registers them by evaluating the module scope. Nested definitions
35
+ * inside functions or classes won't be discovered during the deployment process.
36
+ */
37
+ function isTopLevel(node) {
38
+ let current = node.parent;
39
+ while (current) {
40
+ if (current.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
41
+ current.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
42
+ current.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
43
+ current.type === utils_1.AST_NODE_TYPES.ClassDeclaration ||
44
+ current.type === utils_1.AST_NODE_TYPES.ClassExpression ||
45
+ current.type === utils_1.AST_NODE_TYPES.MethodDefinition) {
46
+ return false;
47
+ }
48
+ current = current.parent;
49
+ }
50
+ return true;
51
+ }
52
+ /**
53
+ * Checks if a node directly defines an entry point as a function or variable declaration.
54
+ * This exempts wrapper implementation files (e.g., functions/src/v2/) but intentionally
55
+ * does not exempt re-export files (e.g., `const myCall = onCall; export { myCall }`).
56
+ * Re-export files that also invoke entry points must still follow the .f.ts naming convention
57
+ * and will be flagged by the CallExpression handler.
58
+ *
59
+ * This function specifically checks AST node types FunctionDeclaration and
60
+ * VariableDeclaration but does not catch ExportSpecifiers used in re-exports.
61
+ */
62
+ function isNodeDefiningEntryPoint(node, entryPoints) {
63
+ // Handle function declaration: export function onCall() {}
64
+ if (node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration &&
65
+ node.id &&
66
+ entryPoints.has(node.id.name)) {
67
+ return true;
68
+ }
69
+ // Handle variable declaration: export const onCall = ...
70
+ if (node.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
71
+ return node.declarations.some((decl) => decl.id.type === utils_1.AST_NODE_TYPES.Identifier &&
72
+ entryPoints.has(decl.id.name));
73
+ }
74
+ return false;
75
+ }
76
+ /**
77
+ * Obtains the scope for a node in an ESLint 9+ compatible way.
78
+ *
79
+ * Note: In ESLint 8, falls back to context.getScope() which returns the scope
80
+ * of the currently visited node—only call this from within visitor handlers
81
+ * for the target node.
82
+ */
83
+ function getScopeForNode(context, node) {
84
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
85
+ if (isEslint9OrLater(sourceCode)) {
86
+ return getEslint9Scope(sourceCode, node);
87
+ }
88
+ return context.getScope();
89
+ }
90
+ /**
91
+ * Checks if the current environment is ESLint 9 or later.
92
+ */
93
+ function isEslint9OrLater(sourceCode) {
94
+ return typeof sourceCode.getScope === 'function';
95
+ }
96
+ /**
97
+ * Gets the scope for a node using the ESLint 9+ SourceCode.getScope() method.
98
+ */
99
+ function getEslint9Scope(sourceCode, node) {
100
+ return sourceCode.getScope(node);
101
+ }
102
+ /**
103
+ * Gets the import definition for a given name in the current scope.
104
+ */
105
+ function getImportDef(context, calleeName, node) {
106
+ const scope = getScopeForNode(context, node);
107
+ const variable = ASTHelpers_1.ASTHelpers.findVariableInScope(scope, calleeName);
108
+ if (!variable) {
109
+ return null;
110
+ }
111
+ return variable.defs.find((def) => def.type === 'ImportBinding') ?? null;
112
+ }
113
+ /**
114
+ * Checks if the import comes from an allowed Firebase or internal wrapper source.
115
+ */
116
+ function isFromAllowedSource(importDef) {
117
+ if (!importDef || !importDef.parent) {
118
+ return false;
119
+ }
120
+ const importDeclaration = importDef.parent;
121
+ if (importDeclaration.type !== utils_1.AST_NODE_TYPES.ImportDeclaration) {
122
+ return false;
123
+ }
124
+ const source = importDeclaration.source.value;
125
+ return (source.startsWith('firebase-functions') ||
126
+ /(^|\/)v2(\/|$)/.test(source) ||
127
+ /(^|\/)util\/webhook(\/|$)/.test(source));
128
+ }
129
+ /**
130
+ * Helper function to extract name from import specifier.
131
+ */
132
+ function getNameFromImportSpecifier(node) {
133
+ const { imported } = node;
134
+ if (imported.type === utils_1.AST_NODE_TYPES.Identifier) {
135
+ return imported.name;
136
+ }
137
+ // Handle Literal imports (string-named imports in ESLint 8+)
138
+ const importedNode = imported;
139
+ if (importedNode.type === utils_1.AST_NODE_TYPES.Literal &&
140
+ typeof importedNode.value === 'string') {
141
+ return importedNode.value;
142
+ }
143
+ return undefined;
144
+ }
145
+ /**
146
+ * Helper function to extract name from module path.
147
+ */
148
+ function getNameFromModulePath(importDeclaration, entryPoints) {
149
+ const modulePath = importDeclaration.source.value;
150
+ // Extract the module name from the path.
151
+ // Example: '../../v2/https/onCall' -> 'onCall'
152
+ const match = modulePath.match(/[/\\]([^/\\]+)$/);
153
+ const segment = (match ? match[1] : modulePath).replace(/\.[jt]sx?$/, '');
154
+ return entryPoints.has(segment) ? segment : undefined;
155
+ }
156
+ /**
157
+ * Resolves the original name of the imported function, handling aliases and default imports.
158
+ */
159
+ function getOriginalName(importDef, calleeName, entryPoints) {
160
+ if (!importDef || !importDef.node) {
161
+ return calleeName;
162
+ }
163
+ // Handle named imports: import { onCall as myCall } from ...
164
+ if (importDef.node.type === utils_1.AST_NODE_TYPES.ImportSpecifier) {
165
+ return getNameFromImportSpecifier(importDef.node) ?? calleeName;
166
+ }
167
+ // Handle default imports: import onCall from '../../v2/https/onCall'
168
+ // or namespace imports: import * as onCall from '../../v2/https/onCall'
169
+ if (importDef.node.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier ||
170
+ importDef.node.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier) {
171
+ return (getNameFromModulePath(importDef.parent, entryPoints) ?? calleeName);
172
+ }
173
+ return calleeName;
174
+ }
175
+ exports.enforceFExtensionForEntryPoints = (0, createRule_1.createRule)({
176
+ name: 'enforce-f-extension-for-entry-points',
177
+ meta: {
178
+ type: 'problem',
179
+ docs: {
180
+ description: 'Enforce .f.ts extension for entry points',
181
+ recommended: 'error',
182
+ },
183
+ schema: [
184
+ {
185
+ type: 'object',
186
+ properties: {
187
+ entryPoints: {
188
+ type: 'array',
189
+ items: { type: 'string' },
190
+ },
191
+ },
192
+ additionalProperties: false,
193
+ },
194
+ ],
195
+ messages: {
196
+ requireFExtension: 'File "{{fileName}}" contains a Firebase Cloud Function entry point "{{entryPoint}}" but lacks the ".f.ts" extension. ' +
197
+ 'Entry points must use ".f.ts" to clearly mark the public function surface to avoid accidental export/name collisions, ' +
198
+ 'unintended exposure of internal implementation during deployment, confusion during imports/tests, and maintenance bugs. ' +
199
+ 'Rename this file to "{{suggestedName}}" to mark it as a public entry point.',
200
+ },
201
+ },
202
+ defaultOptions: [{ entryPoints: DEFAULT_ENTRY_POINTS }],
203
+ create(context, [options]) {
204
+ const filePath = context.filename ??
205
+ context.getFilename();
206
+ const fileName = path_1.default.basename(filePath);
207
+ const entryPoints = new Set(options.entryPoints?.length ? options.entryPoints : DEFAULT_ENTRY_POINTS);
208
+ // Only apply to files under functions/src/
209
+ const normalizedPath = filePath.replace(/\\/g, '/');
210
+ if (!normalizedPath.includes('/functions/src/') &&
211
+ !normalizedPath.startsWith('functions/src/')) {
212
+ return {};
213
+ }
214
+ // Exclude test files and declaration files
215
+ if (/\.(test|spec)\.tsx?$|\.d\.ts$/.test(fileName)) {
216
+ return {};
217
+ }
218
+ // Only apply to .ts and .tsx files, but not files already using .f.ts or .f.tsx
219
+ const isTsOrTsx = /\.tsx?$/.test(fileName);
220
+ const isFExtension = /\.f\.tsx?$/.test(fileName);
221
+ if (!isTsOrTsx || isFExtension) {
222
+ return {};
223
+ }
224
+ let isDefiningEntryPoint = false;
225
+ let reported = false;
226
+ return {
227
+ ExportNamedDeclaration(node) {
228
+ if (isDefiningEntryPoint || !node.declaration) {
229
+ return;
230
+ }
231
+ if (isNodeDefiningEntryPoint(node.declaration, entryPoints)) {
232
+ isDefiningEntryPoint = true;
233
+ }
234
+ },
235
+ ExportDefaultDeclaration(node) {
236
+ if (isDefiningEntryPoint) {
237
+ return;
238
+ }
239
+ if (isNodeDefiningEntryPoint(node.declaration, entryPoints)) {
240
+ isDefiningEntryPoint = true;
241
+ }
242
+ },
243
+ CallExpression(node) {
244
+ if (reported || isDefiningEntryPoint) {
245
+ return;
246
+ }
247
+ // We only care about simple identifier calls (e.g., onCall())
248
+ if (node.callee.type !== utils_1.AST_NODE_TYPES.Identifier) {
249
+ return;
250
+ }
251
+ const calleeName = node.callee.name;
252
+ // Optimization: Early return if it's not a top-level call
253
+ if (!isTopLevel(node)) {
254
+ return;
255
+ }
256
+ // Variable lookup logic and import source validation
257
+ const importDef = getImportDef(context, calleeName, node);
258
+ if (!importDef || !isFromAllowedSource(importDef)) {
259
+ return;
260
+ }
261
+ // Entry point checking
262
+ const originalName = getOriginalName(importDef, calleeName, entryPoints);
263
+ if (!entryPoints.has(originalName)) {
264
+ return;
265
+ }
266
+ reported = true;
267
+ const suggestedName = fileName.replace(/\.tsx?$/, '.f$&');
268
+ context.report({
269
+ node,
270
+ messageId: 'requireFExtension',
271
+ data: {
272
+ fileName,
273
+ entryPoint: calleeName === originalName
274
+ ? calleeName
275
+ : `${calleeName} (${originalName})`,
276
+ suggestedName,
277
+ },
278
+ });
279
+ },
280
+ };
281
+ },
282
+ });
283
+ //# sourceMappingURL=enforce-f-extension-for-entry-points.js.map
@@ -9,14 +9,14 @@ exports.enforceGlobalConstants = (0, createRule_1.createRule)({
9
9
  meta: {
10
10
  type: 'suggestion',
11
11
  docs: {
12
- description: 'Enforce using global static constants instead of useMemo with empty dependency arrays for object literals, and extract inline destructuring defaults in React components/hooks to global constants',
12
+ description: 'Enforce global static constants for React components/hooks',
13
13
  recommended: 'error',
14
14
  },
15
15
  fixable: 'code',
16
16
  schema: [],
17
17
  messages: {
18
- useGlobalConstant: 'Use a global static constant instead of useMemo with an empty dependency array for object literals',
19
- extractDefaultToGlobalConstant: 'Extract inline default value to a module-level constant for stable reference',
18
+ useGlobalConstant: 'Object literal returned from useMemo with empty dependencies creates a new reference every render without providing memoization benefits this wastes memory and misleads readers into thinking the value is computed → move the object to a module-level constant (e.g., const OPTIONS = { ... } as const;).',
19
+ extractDefaultToGlobalConstant: 'Inline default value in destructuring creates a new reference on every render → this causes unnecessary re-renders in child components due to unstable identity → extract the default to a module-level constant (e.g., const DEFAULT_OPTIONS = { ... } as const;).',
20
20
  },
21
21
  },
22
22
  defaultOptions: [],
@@ -19,7 +19,7 @@ exports.enforceIdCapitalization = (0, createRule_1.createRule)({
19
19
  fixable: 'code',
20
20
  schema: [],
21
21
  messages: {
22
- enforceIdCapitalization: 'Use "ID" instead of "id" in user-facing text for better readability',
22
+ enforceIdCapitalization: 'Found lowercase "id" in user-facing text → "ID" (capitalized) is the standard abbreviation for "identifier" and improves readability in UI labels and messages → replace lowercase "id" with uppercase "ID" in displayed labels and messages.',
23
23
  },
24
24
  },
25
25
  defaultOptions: [],
@@ -3,7 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.enforceMemoizeAsync = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
5
  const createRule_1 = require("../utils/createRule");
6
- const MEMOIZE_MODULE = 'typescript-memoize';
6
+ const MEMOIZE_MODULE = '@blumintinc/typescript-memoize';
7
+ const ALLOWED_MEMOIZE_MODULES = new Set([
8
+ MEMOIZE_MODULE,
9
+ 'typescript-memoize',
10
+ ]);
7
11
  /**
8
12
  * Matches a memoize decorator in supported syntaxes:
9
13
  * - @Alias()
@@ -38,33 +42,34 @@ exports.enforceMemoizeAsync = (0, createRule_1.createRule)({
38
42
  meta: {
39
43
  type: 'suggestion',
40
44
  docs: {
41
- description: 'Enforce @Memoize() decorator on async methods with 0-1 parameters to cache results and prevent redundant API calls or expensive computations. This improves performance by reusing previous results when the same parameters are provided, particularly useful for data fetching methods.',
45
+ description: 'Enforce @Memoize() decorator on async methods with 0-1 parameters to cache results and prevent redundant API calls or expensive computations. Without memoization, repeated calls trigger redundant requests or expensive computations, increasing latency. @Memoize() caches results by parameter, ensuring subsequent calls with identical inputs return immediately.',
42
46
  recommended: 'error',
43
47
  },
44
48
  fixable: 'code',
45
49
  schema: [],
46
50
  messages: {
47
- requireMemoize: 'Async methods with 0-1 parameters should be decorated with @Memoize() to cache results and improve performance. Instead of `async getData(id?: string)`, use `@Memoize()\nasync getData(id?: string)`. Import Memoize from "typescript-memoize".',
51
+ requireMemoize: 'Async methods with 0-1 parameters should be decorated with @Memoize(). Without memoization, repeated calls trigger redundant network requests or expensive computations, increasing latency and risk of rate-limiting. @Memoize() caches results by parameter, ensuring subsequent calls with identical inputs return immediately. Fix: add @Memoize() above the method and import Memoize from "@blumintinc/typescript-memoize".',
48
52
  },
49
53
  },
50
54
  defaultOptions: [],
51
55
  create(context) {
52
56
  let hasMemoizeImport = false;
53
- let memoizeAlias = 'Memoize';
54
- let memoizeNamespace = null;
57
+ const memoizeAliases = new Map(); // alias -> source module
58
+ const memoizeNamespaces = new Map(); // namespace -> source module
55
59
  let scheduledImportFix = false;
56
60
  return {
57
61
  ImportDeclaration(node) {
58
- if (node.source.value === MEMOIZE_MODULE) {
62
+ if (ALLOWED_MEMOIZE_MODULES.has(String(node.source.value))) {
59
63
  node.specifiers.forEach((spec) => {
60
64
  if (spec.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
65
+ spec.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
61
66
  spec.imported.name === 'Memoize') {
62
67
  hasMemoizeImport = true;
63
- memoizeAlias = spec.local?.name ?? memoizeAlias;
68
+ memoizeAliases.set(spec.local.name, String(node.source.value));
64
69
  }
65
70
  else if (spec.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier) {
66
71
  hasMemoizeImport = true;
67
- memoizeNamespace = spec.local.name;
72
+ memoizeNamespaces.set(spec.local.name, String(node.source.value));
68
73
  }
69
74
  });
70
75
  }
@@ -81,7 +86,39 @@ exports.enforceMemoizeAsync = (0, createRule_1.createRule)({
81
86
  return;
82
87
  }
83
88
  // Check if method already has @Memoize or @Memoize() decorator
84
- const hasDecorator = node.decorators?.some((decorator) => isMemoizeDecorator(decorator, memoizeAlias));
89
+ const hasDecorator = node.decorators?.some((decorator) => {
90
+ // If no named imports were found, we assume 'Memoize' is the intended name (for legacy/global support)
91
+ const aliasesToCheck = memoizeAliases.size === 0
92
+ ? ['Memoize']
93
+ : Array.from(memoizeAliases.keys());
94
+ // Check against all known aliases
95
+ for (const alias of aliasesToCheck) {
96
+ if (isMemoizeDecorator(decorator, alias)) {
97
+ return true;
98
+ }
99
+ }
100
+ // Also check against namespaces
101
+ const expression = decorator.expression;
102
+ if (expression.type === utils_1.AST_NODE_TYPES.MemberExpression &&
103
+ !expression.computed &&
104
+ expression.property.type === utils_1.AST_NODE_TYPES.Identifier &&
105
+ expression.property.name === 'Memoize' &&
106
+ expression.object.type === utils_1.AST_NODE_TYPES.Identifier &&
107
+ memoizeNamespaces.has(expression.object.name)) {
108
+ return true;
109
+ }
110
+ // Handle namespace call: @ns.Memoize()
111
+ if (expression.type === utils_1.AST_NODE_TYPES.CallExpression &&
112
+ expression.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
113
+ !expression.callee.computed &&
114
+ expression.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
115
+ expression.callee.property.name === 'Memoize' &&
116
+ expression.callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
117
+ memoizeNamespaces.has(expression.callee.object.name)) {
118
+ return true;
119
+ }
120
+ return false;
121
+ });
85
122
  if (hasDecorator) {
86
123
  return;
87
124
  }
@@ -91,15 +128,32 @@ exports.enforceMemoizeAsync = (0, createRule_1.createRule)({
91
128
  fix(fixer) {
92
129
  const fixes = [];
93
130
  const sourceCode = context.sourceCode;
94
- const decoratorIdent = memoizeNamespace
95
- ? `${memoizeNamespace}.Memoize`
96
- : memoizeAlias;
131
+ // Determine which identifier to use for the decorator
132
+ let decoratorIdent = 'Memoize';
133
+ if (hasMemoizeImport) {
134
+ // Prefer 'Memoize' from the new package if available
135
+ if (memoizeAliases.has('Memoize') &&
136
+ memoizeAliases.get('Memoize') === MEMOIZE_MODULE) {
137
+ decoratorIdent = 'Memoize';
138
+ }
139
+ else if (memoizeAliases.size > 0) {
140
+ // Find first alias from new package, fallback to any alias
141
+ const newPackageAlias = Array.from(memoizeAliases.entries()).find(([_, pkg]) => pkg === MEMOIZE_MODULE)?.[0];
142
+ decoratorIdent =
143
+ newPackageAlias || Array.from(memoizeAliases.keys())[0];
144
+ }
145
+ else if (memoizeNamespaces.size > 0) {
146
+ // Prefer namespace from the new package if available
147
+ const newPackageNs = Array.from(memoizeNamespaces.entries()).find(([_, pkg]) => pkg === MEMOIZE_MODULE)?.[0];
148
+ const selectedNs = newPackageNs || Array.from(memoizeNamespaces.keys())[0];
149
+ decoratorIdent = `${selectedNs}.Memoize`;
150
+ }
151
+ }
97
152
  const importStatement = `import { Memoize } from '${MEMOIZE_MODULE}';`;
98
153
  // Add import if it's not already present; ensure we only add once per file
99
154
  if (!hasMemoizeImport &&
100
- !memoizeNamespace &&
101
- !scheduledImportFix &&
102
- !sourceCode.text.includes(importStatement)) {
155
+ memoizeNamespaces.size === 0 &&
156
+ !scheduledImportFix) {
103
157
  const programBody = sourceCode.ast.body;
104
158
  const firstImport = programBody.find((n) => n.type === utils_1.AST_NODE_TYPES.ImportDeclaration);
105
159
  const anchorNode = (firstImport ?? programBody[0]);
@@ -34,14 +34,48 @@ exports.enforcePropsArgumentName = (0, createRule_1.createRule)({
34
34
  function endsWithProps(typeName) {
35
35
  return typeName.endsWith('Props');
36
36
  }
37
+ // Get all parameters that have a "Props" type
38
+ function getPropsParams(params) {
39
+ return params
40
+ .map((param) => {
41
+ const id = getIdFromParam(param);
42
+ if (id && id.typeAnnotation && id.typeAnnotation.typeAnnotation) {
43
+ const paramTypeName = getTypeName(id.typeAnnotation.typeAnnotation);
44
+ if (paramTypeName && endsWithProps(paramTypeName)) {
45
+ return { id, typeName: paramTypeName };
46
+ }
47
+ }
48
+ return null;
49
+ })
50
+ .filter((p) => !!p);
51
+ }
37
52
  // Generate suggested parameter name for Props types
38
- function getSuggestedParameterName(typeName, allParams, currentParam) {
53
+ function getSuggestedParameterName(typeName, allParams, currentParam, propsParams) {
39
54
  const currentId = getIdFromParam(currentParam);
40
55
  const currentName = currentId?.name;
41
56
  const existingNames = new Set(allParams
42
57
  .map((p) => getIdFromParam(p))
43
58
  .filter((id) => !!id && id.name !== currentName)
44
59
  .map((id) => id.name));
60
+ // If multiple parameters have the exact same Props type, allow the current names.
61
+ // This handles cases like (prevProps: TProps, nextProps: TProps) where
62
+ // forcing a single name based on the type would cause a collision or
63
+ // break descriptive naming.
64
+ const sameTypeParams = propsParams.filter((p) => p.typeName === typeName);
65
+ if (sameTypeParams.length > 1) {
66
+ // Keep the current name when multiple params share the same Props type.
67
+ // This preserves descriptive names like prevProps/nextProps.
68
+ if (!currentName) {
69
+ // Fallback should not occur since destructured params are filtered upstream.
70
+ let candidate = 'props';
71
+ let suffix = 2;
72
+ while (existingNames.has(candidate)) {
73
+ candidate = `props${suffix++}`;
74
+ }
75
+ return candidate;
76
+ }
77
+ return currentName;
78
+ }
45
79
  if (typeName === 'Props') {
46
80
  let candidate = 'props';
47
81
  let suffix = 2;
@@ -50,17 +84,7 @@ exports.enforcePropsArgumentName = (0, createRule_1.createRule)({
50
84
  }
51
85
  return candidate;
52
86
  }
53
- // Count how many parameters have Props types
54
- const propsParams = allParams.filter((param) => {
55
- if (param.type === utils_1.AST_NODE_TYPES.Identifier &&
56
- param.typeAnnotation &&
57
- param.typeAnnotation.typeAnnotation) {
58
- const paramTypeName = getTypeName(param.typeAnnotation.typeAnnotation);
59
- return paramTypeName && endsWithProps(paramTypeName);
60
- }
61
- return false;
62
- });
63
- // If there's only one Props parameter, suggest "props"
87
+ // If there's only one Props parameter (regardless of type name), suggest "props"
64
88
  if (propsParams.length <= 1) {
65
89
  let candidate = 'props';
66
90
  let suffix = 2;
@@ -69,8 +93,8 @@ exports.enforcePropsArgumentName = (0, createRule_1.createRule)({
69
93
  }
70
94
  return candidate;
71
95
  }
72
- // If there are multiple Props parameters, use prefixed names
73
- // For types like "UserProps", suggest "userProps"
96
+ // If there are multiple Props parameters with different types, use prefixed names.
97
+ // For types like "UserProps", suggest "userProps".
74
98
  const baseName = typeName.slice(0, -5); // Remove "Props"
75
99
  const camelCaseBase = baseName.charAt(0).toLowerCase() + baseName.slice(1);
76
100
  let candidate = `${camelCaseBase}Props`;
@@ -138,6 +162,7 @@ exports.enforcePropsArgumentName = (0, createRule_1.createRule)({
138
162
  return;
139
163
  }
140
164
  const sourceCode = context.sourceCode;
165
+ const propsParams = getPropsParams(node.params);
141
166
  node.params.forEach((param) => {
142
167
  if (isDestructuredParameter(param)) {
143
168
  return;
@@ -146,7 +171,7 @@ exports.enforcePropsArgumentName = (0, createRule_1.createRule)({
146
171
  if (id && id.typeAnnotation && id.typeAnnotation.typeAnnotation) {
147
172
  const typeName = getTypeName(id.typeAnnotation.typeAnnotation);
148
173
  if (typeName && endsWithProps(typeName)) {
149
- const suggestedName = getSuggestedParameterName(typeName, node.params, param);
174
+ const suggestedName = getSuggestedParameterName(typeName, node.params, param, propsParams);
150
175
  if (id.name !== suggestedName) {
151
176
  const messageId = suggestedName === 'props'
152
177
  ? 'usePropsParameterName'
@@ -174,6 +199,7 @@ exports.enforcePropsArgumentName = (0, createRule_1.createRule)({
174
199
  function checkClassMethod(node) {
175
200
  const method = node.value;
176
201
  const sourceCode = context.sourceCode;
202
+ const propsParams = getPropsParams(method.params);
177
203
  method.params.forEach((param) => {
178
204
  if (isDestructuredParameter(param)) {
179
205
  return;
@@ -182,7 +208,7 @@ exports.enforcePropsArgumentName = (0, createRule_1.createRule)({
182
208
  if (id && id.typeAnnotation && id.typeAnnotation.typeAnnotation) {
183
209
  const typeName = getTypeName(id.typeAnnotation.typeAnnotation);
184
210
  if (typeName && endsWithProps(typeName)) {
185
- const suggestedName = getSuggestedParameterName(typeName, method.params, param);
211
+ const suggestedName = getSuggestedParameterName(typeName, method.params, param, propsParams);
186
212
  if (id.name !== suggestedName) {
187
213
  const messageId = suggestedName === 'props'
188
214
  ? 'usePropsParameterName'
@@ -31,12 +31,12 @@ function getFunctionName(node) {
31
31
  }
32
32
  return null;
33
33
  }
34
- function isProbablyComponent(node) {
34
+ function isProbablyComponent(node, context) {
35
35
  const name = getFunctionName(node);
36
36
  if (name && /^[A-Z]/.test(name)) {
37
37
  return true;
38
38
  }
39
- if (ASTHelpers_1.ASTHelpers.returnsJSX(node.body)) {
39
+ if (ASTHelpers_1.ASTHelpers.returnsJSX(node.body, context)) {
40
40
  return true;
41
41
  }
42
42
  return false;
@@ -236,7 +236,7 @@ exports.enforceStableHashSpreadProps = (0, createRule_1.createRule)({
236
236
  }
237
237
  functionStack.push({
238
238
  node,
239
- isComponent: isProbablyComponent(node),
239
+ isComponent: isProbablyComponent(node, context),
240
240
  restNames,
241
241
  propsIdentifiers,
242
242
  });
@@ -8,7 +8,7 @@ exports.enforceTransformMemoization = (0, createRule_1.createRule)({
8
8
  meta: {
9
9
  type: 'problem',
10
10
  docs: {
11
- description: 'Enforce memoization of adaptValue transformValue/transformOnChange so the adapted component receives stable handlers and avoids unnecessary re-renders.',
11
+ description: 'Enforce memoization of transformValue and transformOnChange in adaptValue',
12
12
  recommended: 'error',
13
13
  },
14
14
  schema: [],