@blumintinc/eslint-plugin-blumint 1.20.31 → 1.20.32
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/lib/index.js +1 -1
- package/lib/rules/enforce-microdiff.d.ts +2 -1
- package/lib/rules/enforce-microdiff.js +147 -23
- package/lib/rules/enforce-stable-hash-spread-props.js +40 -1
- package/lib/rules/fast-deep-equal-over-microdiff.js +37 -0
- package/lib/rules/prefer-global-router-state-key.js +57 -0
- package/lib/rules/prefer-next-dynamic.js +37 -2
- package/lib/rules/prefer-use-deep-compare-memo.js +62 -6
- package/lib/rules/require-dynamic-firebase-imports.d.ts +2 -2
- package/lib/rules/require-dynamic-firebase-imports.js +95 -7
- package/lib/rules/require-memoize-jsx-returners.js +87 -9
- package/package.json +1 -1
- package/release-manifest.json +70 -0
package/lib/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
1
2
|
type MessageIds = 'enforceMicrodiff' | 'enforceMicrodiffImport';
|
|
2
|
-
export declare const enforceMicrodiff:
|
|
3
|
+
export declare const enforceMicrodiff: TSESLint.RuleModule<MessageIds, [], TSESLint.RuleListener>;
|
|
3
4
|
export {};
|
|
@@ -3,6 +3,94 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.enforceMicrodiff = 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");
|
|
7
|
+
const DIFF_NAME = 'diff';
|
|
8
|
+
const MICRODIFF_MODULE = 'microdiff';
|
|
9
|
+
/**
|
|
10
|
+
* The libraries this rule replaces. Every fix retires the whole import
|
|
11
|
+
* declaration of one of these, which is what makes the names it binds available
|
|
12
|
+
* to the microdiff import that takes its place.
|
|
13
|
+
*/
|
|
14
|
+
const COMPETING_DIFF_MODULES = new Set([
|
|
15
|
+
'deep-diff',
|
|
16
|
+
'fast-diff',
|
|
17
|
+
'diff',
|
|
18
|
+
'deep-object-diff',
|
|
19
|
+
// 'fast-deep-equal' and 'fast-deep-equal/es6' stay out of this set: they are
|
|
20
|
+
// allowed alternatives to microdiff, so their imports survive the fix.
|
|
21
|
+
]);
|
|
22
|
+
/**
|
|
23
|
+
* A specifier that makes a bare `diff` resolve to microdiff's diff function:
|
|
24
|
+
* its default export, or its named `diff` export, bound under the name the fix
|
|
25
|
+
* emits.
|
|
26
|
+
*/
|
|
27
|
+
function bindsMicrodiffDiff(specifier) {
|
|
28
|
+
if (specifier.local.name !== DIFF_NAME) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
if (specifier.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
return (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
35
|
+
specifier.importKind !== 'type' &&
|
|
36
|
+
specifier.imported.name === DIFF_NAME);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* microdiff's import read off `Program.body` rather than off a flag raised by
|
|
40
|
+
* the ImportDeclaration visitor. A competing import that precedes the microdiff
|
|
41
|
+
* one is fixed before the visitor reaches microdiff, so a flag still unset at
|
|
42
|
+
* that point makes the fix emit a second `import { diff } from 'microdiff'` and
|
|
43
|
+
* duplicate the binding (TS2300). Demanding a specifier that binds `diff` also
|
|
44
|
+
* rejects the shapes a source-only test mistakes for a usable binding: a
|
|
45
|
+
* namespace import, a type-only import, and an alias of some other export.
|
|
46
|
+
*/
|
|
47
|
+
function findMicrodiffImport(program) {
|
|
48
|
+
return program.body.find((statement) => statement.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
|
|
49
|
+
statement.source.value === MICRODIFF_MODULE &&
|
|
50
|
+
statement.importKind !== 'type' &&
|
|
51
|
+
statement.specifiers.some(bindsMicrodiffDiff));
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The declarations of `diff` that a fix may write over: microdiff's own
|
|
55
|
+
* specifier, which an emitted `diff` is meant to resolve to, and the specifiers
|
|
56
|
+
* of a competing library's import, which the fix replaces or removes outright.
|
|
57
|
+
* Any other declaration of the name belongs to the file's own code.
|
|
58
|
+
*/
|
|
59
|
+
function collectClaimableSpecifiers(program) {
|
|
60
|
+
const claimable = new Set();
|
|
61
|
+
program.body.forEach((statement) => {
|
|
62
|
+
if (statement.type !== utils_1.AST_NODE_TYPES.ImportDeclaration) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const source = statement.source.value;
|
|
66
|
+
if (source === MICRODIFF_MODULE) {
|
|
67
|
+
statement.specifiers
|
|
68
|
+
.filter(bindsMicrodiffDiff)
|
|
69
|
+
.forEach((specifier) => claimable.add(specifier));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (COMPETING_DIFF_MODULES.has(source)) {
|
|
73
|
+
statement.specifiers.forEach((specifier) => claimable.add(specifier));
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
return claimable;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Whether a bare `diff` written at `scope` reaches microdiff's function.
|
|
80
|
+
* Resolving through the scope chain catches both failure modes: a module-scope
|
|
81
|
+
* binding that the inserted import redeclares (TS2440, or TS2300 against
|
|
82
|
+
* another import), and a narrower shadow that captures the emitted reference
|
|
83
|
+
* with no diagnostic at all. A binding with no declaration — a global supplied
|
|
84
|
+
* by the environment — is left alone rather than written over.
|
|
85
|
+
*/
|
|
86
|
+
function canEmitDiff(scope, claimable) {
|
|
87
|
+
const existing = ASTHelpers_1.ASTHelpers.findVariableInScope(scope, DIFF_NAME);
|
|
88
|
+
if (!existing) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
return (existing.defs.length > 0 &&
|
|
92
|
+
existing.defs.every((def) => claimable.has(def.node)));
|
|
93
|
+
}
|
|
6
94
|
exports.enforceMicrodiff = (0, createRule_1.createRule)({
|
|
7
95
|
name: 'enforce-microdiff',
|
|
8
96
|
meta: {
|
|
@@ -23,8 +111,31 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
|
|
|
23
111
|
const sourceCode = context.sourceCode;
|
|
24
112
|
const importedDiffLibraries = new Map();
|
|
25
113
|
const importedFunctions = new Map(); // Map of imported function names to their sources
|
|
26
|
-
let hasMicrodiffImport = false;
|
|
27
114
|
const reportedNodes = new Set();
|
|
115
|
+
/**
|
|
116
|
+
* Whether the fix may emit a bare `diff` at `node`. The AST is read at fix
|
|
117
|
+
* time so the answer stays correct under multi-pass `--fix`, where an
|
|
118
|
+
* earlier pass may already have added the microdiff import.
|
|
119
|
+
*/
|
|
120
|
+
function canEmitDiffAt(node) {
|
|
121
|
+
return canEmitDiff(ASTHelpers_1.ASTHelpers.getScope(context, node), collectClaimableSpecifiers(sourceCode.ast));
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Whether retiring `declaration` leaves every reference it binds able to
|
|
125
|
+
* take the name `diff`. The import rewrite lands at module scope while the
|
|
126
|
+
* references it serves sit in nested scopes: one standing where `diff` is
|
|
127
|
+
* shadowed keeps the old name, because its own fix declines, so rewriting
|
|
128
|
+
* the import would strand it without a binding (TS2304).
|
|
129
|
+
*/
|
|
130
|
+
function canRenameReferencesOf(declaration) {
|
|
131
|
+
const claimable = collectClaimableSpecifiers(sourceCode.ast);
|
|
132
|
+
const declarationScope = ASTHelpers_1.ASTHelpers.getScope(context, declaration);
|
|
133
|
+
return declaration.specifiers.every((specifier) => {
|
|
134
|
+
const variable = ASTHelpers_1.ASTHelpers.findVariableInScope(declarationScope, specifier.local.name);
|
|
135
|
+
return (!variable ||
|
|
136
|
+
variable.references.every((reference) => canEmitDiff(reference.from, claimable)));
|
|
137
|
+
});
|
|
138
|
+
}
|
|
28
139
|
// Add a specific set to track which import names are used
|
|
29
140
|
const usedImportNames = new Set();
|
|
30
141
|
// Check if a node is an object or array type
|
|
@@ -61,19 +172,11 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
|
|
|
61
172
|
ImportDeclaration(node) {
|
|
62
173
|
const importSource = node.source.value;
|
|
63
174
|
// Check for microdiff import
|
|
64
|
-
if (importSource ===
|
|
65
|
-
hasMicrodiffImport = true;
|
|
175
|
+
if (importSource === MICRODIFF_MODULE) {
|
|
66
176
|
return;
|
|
67
177
|
}
|
|
68
178
|
// Track other diffing libraries
|
|
69
|
-
if (
|
|
70
|
-
'deep-diff',
|
|
71
|
-
'fast-diff',
|
|
72
|
-
'diff',
|
|
73
|
-
'deep-object-diff',
|
|
74
|
-
// Removed 'fast-deep-equal' and 'fast-deep-equal/es6' from this list
|
|
75
|
-
// as they are allowed alternatives to microdiff
|
|
76
|
-
].includes(importSource)) {
|
|
179
|
+
if (COMPETING_DIFF_MODULES.has(importSource)) {
|
|
77
180
|
// Track imported function names and their sources
|
|
78
181
|
node.specifiers.forEach((specifier) => {
|
|
79
182
|
if (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
@@ -94,12 +197,18 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
|
|
|
94
197
|
importSource,
|
|
95
198
|
},
|
|
96
199
|
fix(fixer) {
|
|
200
|
+
// Decline rather than duplicate or shadow a `diff` this file
|
|
201
|
+
// already binds to something else. The report stands so the
|
|
202
|
+
// author resolves the name clash deliberately.
|
|
203
|
+
if (!canEmitDiffAt(node) || !canRenameReferencesOf(node)) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
97
206
|
// If we already have a microdiff import, just remove this import
|
|
98
|
-
if (
|
|
207
|
+
if (findMicrodiffImport(sourceCode.ast)) {
|
|
99
208
|
return fixer.remove(node);
|
|
100
209
|
}
|
|
101
210
|
// Otherwise, replace with microdiff import
|
|
102
|
-
return fixer.replaceText(node, `import {
|
|
211
|
+
return fixer.replaceText(node, `import { ${DIFF_NAME} } from '${MICRODIFF_MODULE}';`);
|
|
103
212
|
},
|
|
104
213
|
});
|
|
105
214
|
// Check if importing a diff function or a known equality library
|
|
@@ -154,7 +263,10 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
|
|
|
154
263
|
node,
|
|
155
264
|
messageId: 'enforceMicrodiff',
|
|
156
265
|
fix(fixer) {
|
|
157
|
-
|
|
266
|
+
if (!canEmitDiffAt(node)) {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
return fixer.replaceText(callee, DIFF_NAME);
|
|
158
270
|
},
|
|
159
271
|
});
|
|
160
272
|
return;
|
|
@@ -179,8 +291,11 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
|
|
|
179
291
|
node,
|
|
180
292
|
messageId: 'enforceMicrodiff',
|
|
181
293
|
fix(fixer) {
|
|
294
|
+
if (!canEmitDiffAt(node)) {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
182
297
|
// When handling fast-diff and similar libraries, need to ensure the function name is replaced
|
|
183
|
-
return fixer.replaceText(callee,
|
|
298
|
+
return fixer.replaceText(callee, DIFF_NAME);
|
|
184
299
|
},
|
|
185
300
|
});
|
|
186
301
|
}
|
|
@@ -198,8 +313,11 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
|
|
|
198
313
|
node,
|
|
199
314
|
messageId: 'enforceMicrodiff',
|
|
200
315
|
fix(fixer) {
|
|
316
|
+
if (!canEmitDiffAt(node)) {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
201
319
|
// Replace with microdiff
|
|
202
|
-
return fixer.replaceText(node,
|
|
320
|
+
return fixer.replaceText(node, `${DIFF_NAME}(${node.arguments
|
|
203
321
|
.map((arg) => sourceCode.getText(arg))
|
|
204
322
|
.join(', ')})`);
|
|
205
323
|
},
|
|
@@ -245,6 +363,9 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
|
|
|
245
363
|
node,
|
|
246
364
|
messageId: 'enforceMicrodiff',
|
|
247
365
|
fix(fixer) {
|
|
366
|
+
if (!canEmitDiffAt(node)) {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
248
369
|
// Find the containing function to add the import
|
|
249
370
|
let functionNode = node;
|
|
250
371
|
while (functionNode &&
|
|
@@ -258,15 +379,15 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
|
|
|
258
379
|
// we'll need to add the import manually
|
|
259
380
|
if (functionNode &&
|
|
260
381
|
functionNode.type === utils_1.AST_NODE_TYPES.Program &&
|
|
261
|
-
!
|
|
382
|
+
!findMicrodiffImport(sourceCode.ast)) {
|
|
262
383
|
// Need to add an import
|
|
263
|
-
const importFix = fixer.insertTextBeforeRange([0, 0],
|
|
384
|
+
const importFix = fixer.insertTextBeforeRange([0, 0], `import { ${DIFF_NAME} } from '${MICRODIFF_MODULE}';\n\n`);
|
|
264
385
|
// Replace JSON.stringify comparison
|
|
265
|
-
const compareFix = fixer.replaceText(node,
|
|
386
|
+
const compareFix = fixer.replaceText(node, `${DIFF_NAME}(${sourceCode.getText(leftArg)}, ${sourceCode.getText(rightArg)})${isEqual ? '.length === 0' : '.length > 0'}`);
|
|
266
387
|
return [importFix, compareFix];
|
|
267
388
|
}
|
|
268
389
|
// Otherwise just replace the comparison
|
|
269
|
-
return fixer.replaceText(node,
|
|
390
|
+
return fixer.replaceText(node, `${DIFF_NAME}(${sourceCode.getText(leftArg)}, ${sourceCode.getText(rightArg)})${isEqual ? '.length === 0' : '.length > 0'}`);
|
|
270
391
|
},
|
|
271
392
|
});
|
|
272
393
|
}
|
|
@@ -308,13 +429,16 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
|
|
|
308
429
|
node,
|
|
309
430
|
messageId: 'enforceMicrodiff',
|
|
310
431
|
fix(fixer) {
|
|
432
|
+
if (!canEmitDiffAt(node)) {
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
311
435
|
// Create a new version of the function with microdiff
|
|
312
436
|
const newFunctionBody = `{
|
|
313
|
-
return
|
|
437
|
+
return ${DIFF_NAME}(${param1}, ${param2}).length > 0;
|
|
314
438
|
}`;
|
|
315
|
-
if (!
|
|
439
|
+
if (!findMicrodiffImport(sourceCode.ast)) {
|
|
316
440
|
// Create a new import statement
|
|
317
|
-
return fixer.replaceText(node, `import {
|
|
441
|
+
return fixer.replaceText(node, `import { ${DIFF_NAME} } from '${MICRODIFF_MODULE}';\n\nfunction ${node.id?.name}(${param1}, ${param2}) ${newFunctionBody}`);
|
|
318
442
|
}
|
|
319
443
|
else {
|
|
320
444
|
// Just replace the function body
|
|
@@ -146,6 +146,25 @@ function getStableHashLocalNames(sourceCode, hashImport) {
|
|
|
146
146
|
function isStableHashImported(sourceCode, hashImport) {
|
|
147
147
|
return getStableHashLocalNames(sourceCode, hashImport).length > 0;
|
|
148
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Whether every declaration of a resolved binding is the configured hash import
|
|
151
|
+
* itself. A namespace import, an import of another name or module, a parameter,
|
|
152
|
+
* or a local declaration all mean the emitted call would resolve somewhere other
|
|
153
|
+
* than the intended hash function.
|
|
154
|
+
*/
|
|
155
|
+
function bindsHashImport(variable, hashImport) {
|
|
156
|
+
return (variable.defs.length > 0 &&
|
|
157
|
+
variable.defs.every((def) => {
|
|
158
|
+
const specifier = def.node;
|
|
159
|
+
if (specifier.type !== utils_1.AST_NODE_TYPES.ImportSpecifier ||
|
|
160
|
+
specifier.imported.name !== hashImport.importName) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
const declaration = specifier.parent;
|
|
164
|
+
return (declaration?.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
|
|
165
|
+
declaration.source.value === hashImport.source);
|
|
166
|
+
}));
|
|
167
|
+
}
|
|
149
168
|
function getIndentBeforeNode(sourceCode, node) {
|
|
150
169
|
const lineText = sourceCode.lines[node.loc.start.line - 1] ?? '';
|
|
151
170
|
const match = lineText.match(/^[ \t]*/);
|
|
@@ -222,7 +241,6 @@ exports.enforceStableHashSpreadProps = (0, createRule_1.createRule)({
|
|
|
222
241
|
// emit `stableHash(...)`, leaving calls to an unbound identifier.
|
|
223
242
|
let importPlanned = false;
|
|
224
243
|
const isReportSuppressed = (0, disableDirectives_1.createSuppressionChecker)(context);
|
|
225
|
-
const hashIdentifier = existingHashLocalNames[0] ?? hashImport.importName;
|
|
226
244
|
const functionStack = [];
|
|
227
245
|
function getCurrentComponentContext() {
|
|
228
246
|
for (let i = functionStack.length - 1; i >= 0; i -= 1) {
|
|
@@ -315,6 +333,27 @@ exports.enforceStableHashSpreadProps = (0, createRule_1.createRule)({
|
|
|
315
333
|
if (isReportSuppressed(depsArg)) {
|
|
316
334
|
return null;
|
|
317
335
|
}
|
|
336
|
+
// Derive the emitted name from the file's imports rather than from
|
|
337
|
+
// traversal state: `--fix` re-lints between passes, so an import a
|
|
338
|
+
// previous pass landed must be reused, and an alias it introduced
|
|
339
|
+
// must be honoured.
|
|
340
|
+
const hashIdentifier = getStableHashLocalNames(sourceCode, hashImport)[0] ??
|
|
341
|
+
hashImport.importName;
|
|
342
|
+
// The fix writes a bare `hashIdentifier` call into the dependency
|
|
343
|
+
// array and may add a top-level import for it. Another binding of
|
|
344
|
+
// that name, visible from the array, makes both halves wrong: a
|
|
345
|
+
// module-scope binding collides with the inserted import
|
|
346
|
+
// (TS2440/TS2300), and a narrower shadow silently resolves the
|
|
347
|
+
// emitted call to the wrong value with no TypeScript diagnostic at
|
|
348
|
+
// all. Resolving through the scope chain of the reported node — the
|
|
349
|
+
// exact position the call lands in — covers both, while a binding
|
|
350
|
+
// that already is the desired import is the reuse path. Declining
|
|
351
|
+
// leaves the report for the author to resolve deliberately.
|
|
352
|
+
const existingBinding = ASTHelpers_1.ASTHelpers.findVariableInScope(ASTHelpers_1.ASTHelpers.getScope(context, depsArg), hashIdentifier);
|
|
353
|
+
if (existingBinding &&
|
|
354
|
+
!bindsHashImport(existingBinding, hashImport)) {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
318
357
|
const fixes = [];
|
|
319
358
|
const seen = new Set();
|
|
320
359
|
for (const { node: targetNode } of offendingElements) {
|
|
@@ -3,7 +3,32 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.fastDeepEqualOverMicrodiff = 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 disableDirectives_1 = require("../utils/disableDirectives");
|
|
8
|
+
const FAST_DEEP_EQUAL_MODULES = new Set([
|
|
9
|
+
'fast-deep-equal',
|
|
10
|
+
'fast-deep-equal/es6',
|
|
11
|
+
]);
|
|
12
|
+
/**
|
|
13
|
+
* Whether every declaration of a visible binding is a fast-deep-equal import,
|
|
14
|
+
* i.e. the name already means the comparison function the fix wants to call.
|
|
15
|
+
* The verdict comes off the AST (each definition's specifier and its import
|
|
16
|
+
* declaration) rather than a traversal flag, so it holds on every pass of a
|
|
17
|
+
* multi-pass `--fix`, including the passes that follow an inserted import.
|
|
18
|
+
*/
|
|
19
|
+
function bindsFastDeepEqual(variable) {
|
|
20
|
+
return (variable.defs.length > 0 &&
|
|
21
|
+
variable.defs.every((def) => {
|
|
22
|
+
const specifier = def.node;
|
|
23
|
+
if (specifier.type !== utils_1.AST_NODE_TYPES.ImportDefaultSpecifier &&
|
|
24
|
+
specifier.type !== utils_1.AST_NODE_TYPES.ImportSpecifier) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
const declaration = specifier.parent;
|
|
28
|
+
return (declaration?.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
|
|
29
|
+
FAST_DEEP_EQUAL_MODULES.has(declaration.source.value));
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
7
32
|
exports.fastDeepEqualOverMicrodiff = (0, createRule_1.createRule)({
|
|
8
33
|
name: 'fast-deep-equal-over-microdiff',
|
|
9
34
|
meta: {
|
|
@@ -306,6 +331,18 @@ exports.fastDeepEqualOverMicrodiff = (0, createRule_1.createRule)({
|
|
|
306
331
|
if (isReportSuppressed(node)) {
|
|
307
332
|
return null;
|
|
308
333
|
}
|
|
334
|
+
// A binding for the target name that is not the fast-deep-equal import
|
|
335
|
+
// makes both halves of the edit wrong: an inserted import collides with a
|
|
336
|
+
// same-scope declaration (TS2440/TS2300), and a narrower-scope shadow
|
|
337
|
+
// rebinds the emitted call to the local value with no diagnostic at all.
|
|
338
|
+
// Resolving from the fixed node's scope chain catches both. Declining
|
|
339
|
+
// before the import is scheduled leaves the carrier slot to a violation
|
|
340
|
+
// whose scope is safe, and drops the whole edit — including the removal
|
|
341
|
+
// of a redundant `const changes = diff(...)` — rather than half of it.
|
|
342
|
+
const existingBinding = ASTHelpers_1.ASTHelpers.findVariableInScope(ASTHelpers_1.ASTHelpers.getScope(context, node), fastDeepEqualImportName);
|
|
343
|
+
if (existingBinding && !bindsFastDeepEqual(existingBinding)) {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
309
346
|
const args = diffCall.arguments;
|
|
310
347
|
if (args.length !== 2) {
|
|
311
348
|
return null; // Can't fix if not exactly 2 arguments
|
|
@@ -7,6 +7,7 @@ exports.preferGlobalRouterStateKey = void 0;
|
|
|
7
7
|
const path_1 = __importDefault(require("path"));
|
|
8
8
|
const utils_1 = require("@typescript-eslint/utils");
|
|
9
9
|
const createRule_1 = require("../utils/createRule");
|
|
10
|
+
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
10
11
|
// The module's path below the project root doubles as the bare specifier,
|
|
11
12
|
// which is precisely why the root tsconfig `paths` and the Jest mapper resolve
|
|
12
13
|
// it.
|
|
@@ -121,6 +122,35 @@ exports.preferGlobalRouterStateKey = (0, createRule_1.createRule)({
|
|
|
121
122
|
function isValidQueryKeyConstant(name) {
|
|
122
123
|
return name.startsWith('QUERY_KEY_');
|
|
123
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Whether every declaration of a visible binding is the very import the fix
|
|
127
|
+
* wants to reference: a value `ImportSpecifier` of `constant` taken from
|
|
128
|
+
* queryKeys.ts, under any local name. Read off the specifier nodes the scope
|
|
129
|
+
* points at rather than off the traversal maps, so an import written below
|
|
130
|
+
* the call site — which the `ImportDeclaration` visitor has not recorded by
|
|
131
|
+
* the time the fix runs — is recognized instead of duplicated.
|
|
132
|
+
*
|
|
133
|
+
* Any other binding (a local declaration, a parameter, an alias of a
|
|
134
|
+
* different export, a type-only specifier) means a bare reference to the
|
|
135
|
+
* name would resolve somewhere other than the constant.
|
|
136
|
+
*/
|
|
137
|
+
function bindsQueryKeyConstant(variable, constant) {
|
|
138
|
+
return (variable.defs.length > 0 &&
|
|
139
|
+
variable.defs.every((def) => {
|
|
140
|
+
const specifier = def.node;
|
|
141
|
+
if (specifier.type !== utils_1.AST_NODE_TYPES.ImportSpecifier ||
|
|
142
|
+
specifier.importKind === 'type' ||
|
|
143
|
+
specifier.imported.name !== constant) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
const declaration = specifier.parent;
|
|
147
|
+
return (declaration?.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
|
|
148
|
+
declaration.importKind !== 'type' &&
|
|
149
|
+
declaration.source.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
150
|
+
typeof declaration.source.value === 'string' &&
|
|
151
|
+
isQueryKeysSource(declaration.source.value));
|
|
152
|
+
}));
|
|
153
|
+
}
|
|
124
154
|
/**
|
|
125
155
|
* `SourceCode#getScope` supersedes the deprecated `context.getScope`; the
|
|
126
156
|
* fallback keeps the rule working on ESLint versions that predate it.
|
|
@@ -361,12 +391,39 @@ exports.preferGlobalRouterStateKey = (0, createRule_1.createRule)({
|
|
|
361
391
|
const replacementText = localName
|
|
362
392
|
? localName
|
|
363
393
|
: formatConstantReference(importAlias, suggestedConstant);
|
|
394
|
+
// A binding that already owns the emitted name
|
|
395
|
+
// makes both halves of the edit wrong: the inserted
|
|
396
|
+
// import becomes a second declaration of it
|
|
397
|
+
// (TS2440/TS2300), and a shadowing local or
|
|
398
|
+
// parameter captures the bare reference with no
|
|
399
|
+
// diagnostic at all. Resolving through the scope
|
|
400
|
+
// chain at the literal rather than the module scope
|
|
401
|
+
// is what exposes such a shadow. Declining leaves
|
|
402
|
+
// the report in place for the author to resolve.
|
|
403
|
+
// The qualified `alias.CONSTANT` form reaches the
|
|
404
|
+
// constant through the alias and claims no name of
|
|
405
|
+
// its own.
|
|
406
|
+
const visibleBinding = importAlias
|
|
407
|
+
? null
|
|
408
|
+
: ASTHelpers_1.ASTHelpers.findVariableInScope(ASTHelpers_1.ASTHelpers.getScope(context, keyValue), replacementText);
|
|
409
|
+
const bindingIsQueryKeyImport = visibleBinding !== null &&
|
|
410
|
+
bindsQueryKeyConstant(visibleBinding, suggestedConstant);
|
|
411
|
+
if (visibleBinding && !bindingIsQueryKeyImport) {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
364
414
|
// 1) Replace the literal with the constant (qualify if alias exists)
|
|
365
415
|
fixes.push(fixer.replaceText(keyValue, replacementText));
|
|
366
416
|
// 2) Ensure an import exists for the suggested constant
|
|
367
417
|
const hasNamespaceOrDefault = Boolean(importAlias);
|
|
368
418
|
if (!existingNamedImport &&
|
|
369
419
|
!hasNamespaceOrDefault) {
|
|
420
|
+
// The name resolves to the very import this fix
|
|
421
|
+
// would write — one declared below this call site,
|
|
422
|
+
// which the traversal maps miss — so the
|
|
423
|
+
// replacement alone is the complete edit.
|
|
424
|
+
if (bindingIsQueryKeyImport) {
|
|
425
|
+
return fixes;
|
|
426
|
+
}
|
|
370
427
|
if (scheduledQueryKeyNamedImports.has(suggestedConstant)) {
|
|
371
428
|
return fixes;
|
|
372
429
|
}
|
|
@@ -3,6 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.preferNextDynamic = 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");
|
|
7
|
+
const NEXT_DYNAMIC_MODULE = 'next/dynamic';
|
|
8
|
+
const DEFAULT_DYNAMIC_NAME = 'dynamic';
|
|
6
9
|
const DEFAULT_USE_DYNAMIC_SOURCES = [
|
|
7
10
|
'useDynamic',
|
|
8
11
|
'./useDynamic',
|
|
@@ -99,7 +102,7 @@ function findUseDynamicImport(program, allowedSources) {
|
|
|
99
102
|
}
|
|
100
103
|
function getNextDynamicLocalName(program) {
|
|
101
104
|
for (const imp of getImportDeclarations(program)) {
|
|
102
|
-
if (imp.source.value ===
|
|
105
|
+
if (imp.source.value === NEXT_DYNAMIC_MODULE) {
|
|
103
106
|
const def = imp.specifiers.find((s) => s.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier);
|
|
104
107
|
if (def)
|
|
105
108
|
return def.local.name;
|
|
@@ -107,6 +110,24 @@ function getNextDynamicLocalName(program) {
|
|
|
107
110
|
}
|
|
108
111
|
return null;
|
|
109
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Whether every declaration of a visible binding is the `next/dynamic` default
|
|
115
|
+
* import itself. A local variable, a function declaration, a parameter, or an
|
|
116
|
+
* import from any other module all mean the emitted call would resolve
|
|
117
|
+
* somewhere other than Next.js's `dynamic`.
|
|
118
|
+
*/
|
|
119
|
+
function bindsNextDynamicDefault(variable) {
|
|
120
|
+
return (variable.defs.length > 0 &&
|
|
121
|
+
variable.defs.every((def) => {
|
|
122
|
+
const specifier = def.node;
|
|
123
|
+
if (specifier.type !== utils_1.AST_NODE_TYPES.ImportDefaultSpecifier) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
const declaration = specifier.parent;
|
|
127
|
+
return (declaration?.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
|
|
128
|
+
declaration.source.value === NEXT_DYNAMIC_MODULE);
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
110
131
|
function buildDynamicReplacement(call, variableKind, variableIdText, namedExportKey, sourceCode, dynamicIdent) {
|
|
111
132
|
const expr = buildDynamicExpression(call, namedExportKey, sourceCode, dynamicIdent);
|
|
112
133
|
return `${variableKind} ${variableIdText} = ${expr};`;
|
|
@@ -235,10 +256,24 @@ exports.preferNextDynamic = (0, createRule_1.createRule)({
|
|
|
235
256
|
data: { componentName: identifierName },
|
|
236
257
|
fix(fixer) {
|
|
237
258
|
const fixes = [];
|
|
238
|
-
//
|
|
259
|
+
// Read the import off Program.body rather than a traversal flag so
|
|
260
|
+
// the decision stays correct across the re-lints of a multi-pass
|
|
261
|
+
// `--fix`, where an earlier pass may already have inserted it.
|
|
239
262
|
const programNode = program;
|
|
240
263
|
let dynamicLocal = getNextDynamicLocalName(programNode);
|
|
241
264
|
const hasDynamic = !!dynamicLocal;
|
|
265
|
+
// Resolve the identifier the replacement will emit through the
|
|
266
|
+
// scope chain at the fix site. Any binding that is not the
|
|
267
|
+
// `next/dynamic` default import makes the edit wrong: an inserted
|
|
268
|
+
// import collides with a same-named declaration (TS2440/TS2300),
|
|
269
|
+
// and a narrower-scope shadow silently binds the emitted call to
|
|
270
|
+
// the shadow with no TypeScript diagnostic at all. Declining leaves
|
|
271
|
+
// the report for the author to resolve deliberately.
|
|
272
|
+
const emittedName = dynamicLocal ?? DEFAULT_DYNAMIC_NAME;
|
|
273
|
+
const existing = ASTHelpers_1.ASTHelpers.findVariableInScope(ASTHelpers_1.ASTHelpers.getScope(context, init), emittedName);
|
|
274
|
+
if (existing && !bindsNextDynamicDefault(existing)) {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
242
277
|
if (!hasDynamic) {
|
|
243
278
|
// Insert after directive prologue (e.g., "use client")
|
|
244
279
|
const insertionIndex = programNode.body.findIndex((stmt) => {
|
|
@@ -3,6 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.preferUseDeepCompareMemo = 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");
|
|
7
|
+
const DEEP_COMPARE_MODULE = '@blumintinc/use-deep-compare';
|
|
8
|
+
const DEEP_COMPARE_HOOK = 'useDeepCompareMemo';
|
|
6
9
|
// Consider these as memoizing hooks producing stable references
|
|
7
10
|
const MEMOIZING_HOOKS = new Set([
|
|
8
11
|
'useMemo',
|
|
@@ -154,17 +157,53 @@ function collectMemoizedIdentifiers(context) {
|
|
|
154
157
|
visit(program);
|
|
155
158
|
return memoized;
|
|
156
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* A specifier that binds the hook as a callable value under the exact name the
|
|
162
|
+
* rewritten call spells. An alias binds the hook to some other name, leaving
|
|
163
|
+
* `useDeepCompareMemo` unresolvable, and a type-only specifier erases at
|
|
164
|
+
* compile time, so neither can carry the call.
|
|
165
|
+
*/
|
|
166
|
+
function bindsDeepCompareMemo(specifier) {
|
|
167
|
+
return (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
168
|
+
specifier.importKind !== 'type' &&
|
|
169
|
+
specifier.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
170
|
+
specifier.imported.name === DEEP_COMPARE_HOOK &&
|
|
171
|
+
specifier.local.name === DEEP_COMPARE_HOOK);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* The hook's import read off `Program.body` rather than off a flag set by an
|
|
175
|
+
* ImportDeclaration visitor, so the answer holds under multi-pass `--fix`
|
|
176
|
+
* wherever the import sits relative to the fix site.
|
|
177
|
+
*/
|
|
178
|
+
function findDeepCompareMemoImport(program) {
|
|
179
|
+
for (const statement of program.body) {
|
|
180
|
+
if (statement.type !== utils_1.AST_NODE_TYPES.ImportDeclaration ||
|
|
181
|
+
statement.source.value !== DEEP_COMPARE_MODULE ||
|
|
182
|
+
(statement.importKind && statement.importKind !== 'value')) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const specifier = statement.specifiers.find(bindsDeepCompareMemo);
|
|
186
|
+
if (specifier)
|
|
187
|
+
return specifier;
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Whether the visible `useDeepCompareMemo` binding is the very import this fix
|
|
193
|
+
* would otherwise insert. Reusing that binding is the intended path; any other
|
|
194
|
+
* binding of the name belongs to the file's author.
|
|
195
|
+
*/
|
|
196
|
+
function bindsHookImport(variable, hookImport) {
|
|
197
|
+
return (hookImport !== null &&
|
|
198
|
+
variable.defs.length > 0 &&
|
|
199
|
+
variable.defs.every((def) => def.node === hookImport));
|
|
200
|
+
}
|
|
157
201
|
function ensureDeepCompareImportFixes(context, fixer) {
|
|
158
202
|
const fixes = [];
|
|
159
203
|
const sourceCode = context.sourceCode;
|
|
160
204
|
const program = sourceCode.ast;
|
|
161
205
|
// If already imported anywhere, skip adding
|
|
162
|
-
|
|
163
|
-
n.source.value === '@blumintinc/use-deep-compare' &&
|
|
164
|
-
n.specifiers.some((s) => s.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
165
|
-
s.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
166
|
-
s.imported.name === 'useDeepCompareMemo'));
|
|
167
|
-
if (hasImport)
|
|
206
|
+
if (findDeepCompareMemoImport(program))
|
|
168
207
|
return fixes;
|
|
169
208
|
// Determine insertion point and indentation
|
|
170
209
|
const importDecls = program.body.filter((n) => n.type === utils_1.AST_NODE_TYPES.ImportDeclaration);
|
|
@@ -377,6 +416,10 @@ exports.preferUseDeepCompareMemo = (0, createRule_1.createRule)({
|
|
|
377
416
|
}
|
|
378
417
|
if (!hasUnmemoizedNonPrimitive)
|
|
379
418
|
return;
|
|
419
|
+
// Captured during traversal because the fix runs afterwards, when an
|
|
420
|
+
// ESLint version lacking sourceCode.getScope can only report the
|
|
421
|
+
// global scope and would miss a narrower shadow.
|
|
422
|
+
const scope = ASTHelpers_1.ASTHelpers.getScope(context, node);
|
|
380
423
|
context.report({
|
|
381
424
|
node,
|
|
382
425
|
messageId: 'preferUseDeepCompareMemo',
|
|
@@ -384,6 +427,19 @@ exports.preferUseDeepCompareMemo = (0, createRule_1.createRule)({
|
|
|
384
427
|
hook: 'useMemo',
|
|
385
428
|
},
|
|
386
429
|
fix(fixer) {
|
|
430
|
+
// Resolve the emitted name through the scope chain at the call
|
|
431
|
+
// site. A binding that is not this fix's own import makes the edit
|
|
432
|
+
// wrong twice over: the inserted import declares the name a second
|
|
433
|
+
// time (TS2440/TS2300), and a shadowing parameter or local silently
|
|
434
|
+
// routes the call to the wrong value with no diagnostic at all.
|
|
435
|
+
// Declining leaves the report so the author migrates deliberately —
|
|
436
|
+
// including the useMemo specifier removal below, which would
|
|
437
|
+
// otherwise strip an import the untouched call site still needs.
|
|
438
|
+
const existing = ASTHelpers_1.ASTHelpers.findVariableInScope(scope, DEEP_COMPARE_HOOK);
|
|
439
|
+
if (existing &&
|
|
440
|
+
!bindsHookImport(existing, findDeepCompareMemoImport(context.sourceCode.ast))) {
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
387
443
|
const fixes = [];
|
|
388
444
|
// Replace callee
|
|
389
445
|
if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { TSESTree } from '@typescript-eslint/utils';
|
|
1
|
+
import { TSESLint, TSESTree } from '@typescript-eslint/utils';
|
|
2
2
|
export declare const RULE_NAME = "require-dynamic-firebase-imports";
|
|
3
|
-
declare const _default:
|
|
3
|
+
declare const _default: TSESLint.RuleModule<"requireDynamicImport", never[], {
|
|
4
4
|
ImportDeclaration(node: TSESTree.ImportDeclaration): void;
|
|
5
5
|
}>;
|
|
6
6
|
export default _default;
|
|
@@ -3,7 +3,79 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.RULE_NAME = 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
|
exports.RULE_NAME = 'require-dynamic-firebase-imports';
|
|
8
|
+
/**
|
|
9
|
+
* A specifier is erased at compile time when either the whole statement is an
|
|
10
|
+
* `import type` or the specifier carries an inline `type` marker. Only such a
|
|
11
|
+
* specifier is interchangeable with the `import type` the fixer hoists.
|
|
12
|
+
*/
|
|
13
|
+
const isErasableTypeSpecifier = (declaration, specifier) => declaration.importKind === 'type' || specifier.importKind === 'type';
|
|
14
|
+
/**
|
|
15
|
+
* Whether `specifier` binds the same local name to the same export of
|
|
16
|
+
* `importSource` in type position as `target` — i.e. whether it is exactly the
|
|
17
|
+
* binding the hoisted `import type` would introduce.
|
|
18
|
+
*/
|
|
19
|
+
const bindsSameTypeImport = (specifier, target, importSource) => {
|
|
20
|
+
if (specifier.type !== utils_1.AST_NODE_TYPES.ImportSpecifier ||
|
|
21
|
+
specifier.local.name !== target.local.name ||
|
|
22
|
+
specifier.imported.name !== target.imported.name) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
const declaration = specifier.parent;
|
|
26
|
+
return (declaration?.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
|
|
27
|
+
declaration.source.value === importSource &&
|
|
28
|
+
isErasableTypeSpecifier(declaration, specifier));
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Read the module-scope import state off `Program.body` rather than a traversal
|
|
32
|
+
* flag: `--fix` runs multiple passes, and a sibling report in an earlier pass may
|
|
33
|
+
* already have hoisted the same `import type`. A flag set by the
|
|
34
|
+
* `ImportDeclaration` visitor also depends on source order, so it would be stale
|
|
35
|
+
* for any import that precedes the one being rewritten.
|
|
36
|
+
*/
|
|
37
|
+
const findHoistedTypeImport = (program, target, importSource) => {
|
|
38
|
+
for (const statement of program.body) {
|
|
39
|
+
if (statement.type !== utils_1.AST_NODE_TYPES.ImportDeclaration) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const match = statement.specifiers.find((specifier) => bindsSameTypeImport(specifier, target, importSource));
|
|
43
|
+
if (match) {
|
|
44
|
+
return match;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* The binding that makes hoisting `target` unsafe, or `null` when the name is
|
|
51
|
+
* free (or already bound to the very import being hoisted).
|
|
52
|
+
*
|
|
53
|
+
* Resolution walks the scope chain of the rewritten import instead of stopping
|
|
54
|
+
* at module scope, because a narrower shadow raises no TypeScript diagnostic yet
|
|
55
|
+
* silently binds the hoisted type to the wrong declaration. Each hop resolves a
|
|
56
|
+
* single step further up: the rewritten statement binds its own inline `type`
|
|
57
|
+
* specifier in the enclosing function scope, which would otherwise mask the
|
|
58
|
+
* colliding declaration above it. Bindings without a definition are ambient
|
|
59
|
+
* (TypeScript lib types, configured globals); a module-scope import legally
|
|
60
|
+
* shadows those, so they are not collisions.
|
|
61
|
+
*/
|
|
62
|
+
const findCollidingBinding = (scope, target, importSource) => {
|
|
63
|
+
let current = scope;
|
|
64
|
+
while (current) {
|
|
65
|
+
const existing = ASTHelpers_1.ASTHelpers.findVariableInScope(current, target.local.name);
|
|
66
|
+
if (!existing) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
if (existing.defs.length > 0) {
|
|
70
|
+
const bindsTheDesiredImport = existing.defs.every((def) => bindsSameTypeImport(def.node, target, importSource));
|
|
71
|
+
if (!bindsTheDesiredImport) {
|
|
72
|
+
return existing;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
current = existing.scope.upper;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
};
|
|
7
79
|
exports.default = (0, createRule_1.createRule)({
|
|
8
80
|
name: exports.RULE_NAME,
|
|
9
81
|
meta: {
|
|
@@ -109,16 +181,32 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
109
181
|
if (!isInsideAsyncFunction(node)) {
|
|
110
182
|
return null;
|
|
111
183
|
}
|
|
184
|
+
// Type specifiers must not travel into the runtime destructuring:
|
|
185
|
+
// they have no runtime value, and dropping the `type` marker turns
|
|
186
|
+
// type references into dangling value bindings. They hoist into a
|
|
187
|
+
// static `import type` at module scope, which is erased at compile
|
|
188
|
+
// time.
|
|
189
|
+
const scope = ASTHelpers_1.ASTHelpers.getScope(context, node);
|
|
190
|
+
const program = context.getSourceCode().ast;
|
|
191
|
+
const specifiersToHoist = [];
|
|
192
|
+
for (const specifier of typeSpecifiers) {
|
|
193
|
+
if (findCollidingBinding(scope, specifier, importSource)) {
|
|
194
|
+
// Declining the whole fix keeps the file compiling: applying
|
|
195
|
+
// the dynamic import while skipping a colliding hoist would
|
|
196
|
+
// strand the type reference (TS2749) instead of duplicating an
|
|
197
|
+
// identifier (TS2440/TS2300). The report stands so the author
|
|
198
|
+
// resolves the name clash deliberately.
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
if (!findHoistedTypeImport(program, specifier, importSource)) {
|
|
202
|
+
specifiersToHoist.push(specifier);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
112
205
|
const fixes = [
|
|
113
206
|
fixer.replaceText(node, buildDynamicImport(importSource, valueSpecifiers)),
|
|
114
207
|
];
|
|
115
|
-
if (
|
|
116
|
-
|
|
117
|
-
// destructuring: they have no runtime value, and dropping the
|
|
118
|
-
// `type` marker turns type references into dangling value
|
|
119
|
-
// bindings. Hoist them into a static `import type` at module
|
|
120
|
-
// scope, which is erased at compile time.
|
|
121
|
-
fixes.push(fixer.insertTextBeforeRange([0, 0], buildStaticTypeImport(importSource, typeSpecifiers)));
|
|
208
|
+
if (specifiersToHoist.length > 0) {
|
|
209
|
+
fixes.push(fixer.insertTextBeforeRange([0, 0], buildStaticTypeImport(importSource, specifiersToHoist)));
|
|
122
210
|
}
|
|
123
211
|
return fixes;
|
|
124
212
|
},
|
|
@@ -3,12 +3,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.requireMemoizeJsxReturners = 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 disableDirectives_1 = require("../utils/disableDirectives");
|
|
7
8
|
const MEMOIZE_PREFERRED_MODULE = '@blumintinc/typescript-memoize';
|
|
8
9
|
const MEMOIZE_MODULES = new Set([
|
|
9
10
|
MEMOIZE_PREFERRED_MODULE,
|
|
10
11
|
'typescript-memoize',
|
|
11
12
|
]);
|
|
13
|
+
const MEMOIZE_EXPORT_NAME = 'Memoize';
|
|
12
14
|
function isMemoizeDecorator(decorator, alias, namespaceAlias) {
|
|
13
15
|
const expression = decorator.expression;
|
|
14
16
|
const matchesAliasIdentifier = (node) => !!node && node.type === utils_1.AST_NODE_TYPES.Identifier && node.name === alias;
|
|
@@ -325,6 +327,66 @@ function functionReturnsJSX(fn, knownFunctions, cache, factoryContext) {
|
|
|
325
327
|
cache.set(fn, returnsJSX);
|
|
326
328
|
return returnsJSX;
|
|
327
329
|
}
|
|
330
|
+
/**
|
|
331
|
+
* A specifier that binds the memoize module's `Memoize` export — either by name
|
|
332
|
+
* (under any local alias) or through the module namespace.
|
|
333
|
+
*/
|
|
334
|
+
function isMemoizeSpecifier(specifier) {
|
|
335
|
+
if (specifier.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier) {
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
return (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
339
|
+
specifier.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
340
|
+
specifier.imported.name === MEMOIZE_EXPORT_NAME);
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* The binding through which the emitted decorator reaches `Memoize`, read off
|
|
344
|
+
* the program body rather than a traversal flag. `eslint --fix` re-lints between
|
|
345
|
+
* passes and inserts the import above code an earlier pass already visited, so a
|
|
346
|
+
* flag set by the `ImportDeclaration` visitor is not yet accurate for a class
|
|
347
|
+
* that precedes the import in source order.
|
|
348
|
+
*/
|
|
349
|
+
function findMemoizeImportBinding(program) {
|
|
350
|
+
let namedAlias = null;
|
|
351
|
+
let namespaceAlias = null;
|
|
352
|
+
for (const statement of program.body) {
|
|
353
|
+
if (statement.type !== utils_1.AST_NODE_TYPES.ImportDeclaration ||
|
|
354
|
+
!MEMOIZE_MODULES.has(String(statement.source.value))) {
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
for (const specifier of statement.specifiers) {
|
|
358
|
+
if (specifier.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier) {
|
|
359
|
+
namespaceAlias = specifier.local.name;
|
|
360
|
+
}
|
|
361
|
+
else if (isMemoizeSpecifier(specifier)) {
|
|
362
|
+
namedAlias = specifier.local?.name ?? namedAlias;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
/** `namespace.Memoize` stays valid whatever the named specifiers are. */
|
|
367
|
+
if (namespaceAlias) {
|
|
368
|
+
return { localName: namespaceAlias, isNamespace: true };
|
|
369
|
+
}
|
|
370
|
+
return namedAlias ? { localName: namedAlias, isNamespace: false } : null;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Whether every declaration of a visible binding is the memoize import itself.
|
|
374
|
+
* Anything else — a local declaration, a parameter, a class, a default
|
|
375
|
+
* specifier, an import of the same name from another module — means the fix
|
|
376
|
+
* cannot proceed under that name.
|
|
377
|
+
*/
|
|
378
|
+
function bindsMemoizeImport(variable) {
|
|
379
|
+
return (variable.defs.length > 0 &&
|
|
380
|
+
variable.defs.every((def) => {
|
|
381
|
+
const specifier = def.node;
|
|
382
|
+
if (!isMemoizeSpecifier(specifier)) {
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
const declaration = specifier.parent;
|
|
386
|
+
return (declaration?.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
|
|
387
|
+
MEMOIZE_MODULES.has(String(declaration.source.value)));
|
|
388
|
+
}));
|
|
389
|
+
}
|
|
328
390
|
function getImportFixes(fixer, sourceCode, hasMemoizeImport, scheduledImportFix) {
|
|
329
391
|
const fixes = [];
|
|
330
392
|
if (hasMemoizeImport || scheduledImportFix) {
|
|
@@ -381,8 +443,12 @@ exports.requireMemoizeJsxReturners = (0, createRule_1.createRule)({
|
|
|
381
443
|
if (!isVirtualFile && !/\.tsx?$/i.test(filename)) {
|
|
382
444
|
return {};
|
|
383
445
|
}
|
|
384
|
-
|
|
385
|
-
|
|
446
|
+
/**
|
|
447
|
+
* Aliases used to recognize an existing `@Memoize()` decorator. The fixer
|
|
448
|
+
* derives its own import state from the program body instead, so these track
|
|
449
|
+
* detection only.
|
|
450
|
+
*/
|
|
451
|
+
let memoizeAlias = MEMOIZE_EXPORT_NAME;
|
|
386
452
|
let memoizeNamespace = null;
|
|
387
453
|
let scheduledImportFix = false;
|
|
388
454
|
/**
|
|
@@ -430,13 +496,11 @@ exports.requireMemoizeJsxReturners = (0, createRule_1.createRule)({
|
|
|
430
496
|
for (const specifier of node.specifiers) {
|
|
431
497
|
if (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier) {
|
|
432
498
|
if (specifier.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
433
|
-
specifier.imported.name ===
|
|
434
|
-
hasMemoizeImport = true;
|
|
499
|
+
specifier.imported.name === MEMOIZE_EXPORT_NAME) {
|
|
435
500
|
memoizeAlias = specifier.local?.name ?? memoizeAlias;
|
|
436
501
|
}
|
|
437
502
|
}
|
|
438
503
|
else if (specifier.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier) {
|
|
439
|
-
hasMemoizeImport = true;
|
|
440
504
|
memoizeNamespace = specifier.local.name;
|
|
441
505
|
}
|
|
442
506
|
}
|
|
@@ -459,9 +523,6 @@ exports.requireMemoizeJsxReturners = (0, createRule_1.createRule)({
|
|
|
459
523
|
if (!functionReturnsJSX(node.value, localFunctions, jsxReturnCache, factoryContext)) {
|
|
460
524
|
return;
|
|
461
525
|
}
|
|
462
|
-
const decoratorIdent = memoizeNamespace
|
|
463
|
-
? `${memoizeNamespace}.Memoize`
|
|
464
|
-
: memoizeAlias;
|
|
465
526
|
// The report is emitted even when suppressed: ESLint discards it, and
|
|
466
527
|
// reporting keeps the user's disable directive "used" so that
|
|
467
528
|
// `--report-unused-disable-directives` does not flag it.
|
|
@@ -477,7 +538,24 @@ exports.requireMemoizeJsxReturners = (0, createRule_1.createRule)({
|
|
|
477
538
|
return null;
|
|
478
539
|
}
|
|
479
540
|
const sourceCode = context.getSourceCode();
|
|
480
|
-
const
|
|
541
|
+
const memoizeImport = findMemoizeImportBinding(sourceCode.ast);
|
|
542
|
+
const decoratorBaseName = memoizeImport?.localName ?? MEMOIZE_EXPORT_NAME;
|
|
543
|
+
// Resolve the decorator's identifier through the scope chain at the
|
|
544
|
+
// member being fixed. A binding that is not the memoize import
|
|
545
|
+
// makes both halves of the edit wrong: the inserted
|
|
546
|
+
// `import { Memoize }` collides with a top-level binding of that
|
|
547
|
+
// name (TS2440/TS2300), and a narrower shadow silently binds
|
|
548
|
+
// `@Memoize()` to the shadow with no TypeScript diagnostic at all.
|
|
549
|
+
// Declining leaves the report in place so the author resolves the
|
|
550
|
+
// conflict deliberately.
|
|
551
|
+
const existing = ASTHelpers_1.ASTHelpers.findVariableInScope(ASTHelpers_1.ASTHelpers.getScope(context, node), decoratorBaseName);
|
|
552
|
+
if (existing && !bindsMemoizeImport(existing)) {
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
const decoratorIdent = memoizeImport?.isNamespace
|
|
556
|
+
? `${memoizeImport.localName}.${MEMOIZE_EXPORT_NAME}`
|
|
557
|
+
: decoratorBaseName;
|
|
558
|
+
const { fixes, scheduledImportFix: newScheduledImportFix } = getImportFixes(fixer, sourceCode, !!memoizeImport, scheduledImportFix);
|
|
481
559
|
scheduledImportFix = newScheduledImportFix;
|
|
482
560
|
const insertionTarget = node.decorators && node.decorators.length > 0
|
|
483
561
|
? node.decorators[0]
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,74 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.32",
|
|
4
|
+
"date": "2026-07-30T12:36:24.525Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "enforce-microdiff",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1429
|
|
11
|
+
],
|
|
12
|
+
"summary": "decline the fix when `diff` is already bound (closes #1429)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "enforce-stable-hash-spread-props",
|
|
16
|
+
"changeType": "fix",
|
|
17
|
+
"issues": [
|
|
18
|
+
1430
|
|
19
|
+
],
|
|
20
|
+
"summary": "decline the fix when `stableHash` is already bound (closes #1430)"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "fast-deep-equal-over-microdiff",
|
|
24
|
+
"changeType": "fix",
|
|
25
|
+
"issues": [
|
|
26
|
+
1435
|
|
27
|
+
],
|
|
28
|
+
"summary": "decline the fix when `isEqual` is already bound (closes #1435)"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "prefer-global-router-state-key",
|
|
32
|
+
"changeType": "fix",
|
|
33
|
+
"issues": [
|
|
34
|
+
1431
|
|
35
|
+
],
|
|
36
|
+
"summary": "decline the fix when the derived query-key constant is already bound (closes #1431)"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"name": "prefer-next-dynamic",
|
|
40
|
+
"changeType": "fix",
|
|
41
|
+
"issues": [
|
|
42
|
+
1432
|
|
43
|
+
],
|
|
44
|
+
"summary": "decline the fix when `dynamic` is already bound (closes #1432)"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"name": "prefer-use-deep-compare-memo",
|
|
48
|
+
"changeType": "fix",
|
|
49
|
+
"issues": [
|
|
50
|
+
1436
|
|
51
|
+
],
|
|
52
|
+
"summary": "decline the fix when `useDeepCompareMemo` is already bound (closes #1436)"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"name": "require-dynamic-firebase-imports",
|
|
56
|
+
"changeType": "fix",
|
|
57
|
+
"issues": [
|
|
58
|
+
1433
|
|
59
|
+
],
|
|
60
|
+
"summary": "decline the fix when a hoisted type name is already bound (closes #1433)"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"name": "require-memoize-jsx-returners",
|
|
64
|
+
"changeType": "fix",
|
|
65
|
+
"issues": [
|
|
66
|
+
1434
|
|
67
|
+
],
|
|
68
|
+
"summary": "decline the fix when `Memoize` is already bound (closes #1434)"
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
},
|
|
2
72
|
{
|
|
3
73
|
"version": "1.20.31",
|
|
4
74
|
"date": "2026-07-30T11:41:49.100Z",
|