@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.
- package/README.md +10 -11
- package/lib/index.js +14 -12
- package/lib/rules/avoid-utils-directory.js +0 -4
- package/lib/rules/consistent-callback-naming.js +42 -3
- package/lib/rules/dynamic-https-errors.d.ts +1 -1
- package/lib/rules/dynamic-https-errors.js +132 -41
- package/lib/rules/enforce-boolean-naming-prefixes.js +0 -212
- package/lib/rules/enforce-date-ttime.d.ts +1 -0
- package/lib/rules/enforce-date-ttime.js +156 -0
- package/lib/rules/enforce-dynamic-firebase-imports.d.ts +2 -1
- package/lib/rules/enforce-dynamic-firebase-imports.js +3 -2
- package/lib/rules/enforce-f-extension-for-entry-points.d.ts +8 -0
- package/lib/rules/enforce-f-extension-for-entry-points.js +283 -0
- package/lib/rules/enforce-global-constants.js +3 -3
- package/lib/rules/enforce-id-capitalization.js +1 -1
- package/lib/rules/enforce-memoize-async.js +69 -15
- package/lib/rules/enforce-props-argument-name.js +42 -16
- package/lib/rules/enforce-stable-hash-spread-props.js +3 -3
- package/lib/rules/enforce-transform-memoization.js +1 -1
- package/lib/rules/enforce-verb-noun-naming.js +3814 -4641
- package/lib/rules/global-const-style.js +16 -4
- package/lib/rules/memo-compare-deeply-complex-props.js +16 -3
- package/lib/rules/memo-nested-react-components.js +108 -103
- package/lib/rules/no-async-foreach.js +7 -2
- package/lib/rules/no-circular-references.d.ts +2 -1
- package/lib/rules/no-circular-references.js +13 -15
- package/lib/rules/no-console-error.js +12 -10
- package/lib/rules/no-empty-dependency-use-callbacks.js +1 -1
- package/lib/rules/no-entire-object-hook-deps.js +48 -1
- package/lib/rules/no-excessive-parent-chain.js +3 -0
- package/lib/rules/no-inline-component-prop.js +16 -7
- package/lib/rules/no-passthrough-getters.d.ts +2 -2
- package/lib/rules/no-passthrough-getters.js +83 -1
- package/lib/rules/no-redundant-this-params.js +50 -1
- package/lib/rules/no-unmemoized-memo-without-props.js +1 -1
- package/lib/rules/no-unnecessary-destructuring-rename.js +2 -5
- package/lib/rules/parallelize-async-operations.js +119 -54
- package/lib/rules/prefer-nullish-coalescing-boolean-props.js +109 -4
- package/lib/rules/prefer-params-over-parent-id.js +1 -1
- package/lib/rules/prefer-settings-object.js +27 -10
- package/lib/rules/prefer-type-alias-over-typeof-constant.js +9 -0
- package/lib/rules/prefer-usememo-over-useeffect-usestate.js +1 -1
- package/lib/rules/prevent-children-clobber.js +9 -5
- package/lib/rules/react-memoize-literals.js +131 -12
- package/lib/rules/require-https-error-cause.js +30 -11
- package/lib/rules/require-memo.js +17 -9
- package/lib/utils/ASTHelpers.d.ts +34 -1
- package/lib/utils/ASTHelpers.js +346 -112
- 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
|
|
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: '
|
|
19
|
-
extractDefaultToGlobalConstant: '
|
|
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: '
|
|
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.
|
|
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()
|
|
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
|
-
|
|
54
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
11
|
+
description: 'Enforce memoization of transformValue and transformOnChange in adaptValue',
|
|
12
12
|
recommended: 'error',
|
|
13
13
|
},
|
|
14
14
|
schema: [],
|