@blumintinc/eslint-plugin-blumint 1.20.30 → 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-assert-safe-object-key.js +112 -25
- package/lib/rules/enforce-memoize-async.js +97 -19
- package/lib/rules/enforce-memoize-getters.js +108 -21
- 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/no-array-length-in-deps.js +65 -9
- package/lib/rules/prefer-fragment-component.js +127 -73
- 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-memo.js +101 -26
- package/lib/rules/require-memoize-jsx-returners.js +87 -9
- package/lib/rules/use-latest-callback.js +86 -39
- package/package.json +1 -1
- package/release-manifest.json +132 -0
|
@@ -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,8 @@ const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
|
7
7
|
const disableDirectives_1 = require("../utils/disableDirectives");
|
|
8
8
|
// React hooks to check
|
|
9
9
|
const HOOK_NAMES = new Set(['useEffect', 'useCallback', 'useMemo']);
|
|
10
|
+
const REACT_MODULE = 'react';
|
|
11
|
+
const MEMO_HOOK_NAME = 'useMemo';
|
|
10
12
|
const DEFAULT_HASH_IMPORT = {
|
|
11
13
|
source: 'functions/src/util/hash/stableHash',
|
|
12
14
|
importName: 'stableHash',
|
|
@@ -232,20 +234,49 @@ function getValueImports(sourceCode, source) {
|
|
|
232
234
|
* specifier (`useMemo as um`) or a type-only specifier does not count — the
|
|
233
235
|
* value binding under the exact local name must exist.
|
|
234
236
|
*/
|
|
237
|
+
function isNamedValueSpecifier(specifier, name) {
|
|
238
|
+
return (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
239
|
+
specifier.importKind !== 'type' &&
|
|
240
|
+
specifier.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
241
|
+
specifier.imported.name === name &&
|
|
242
|
+
specifier.local.name === name);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Import presence is read off the AST at fix time rather than from a traversal
|
|
246
|
+
* flag: a hook call that precedes the import declaration in source order would
|
|
247
|
+
* otherwise be judged against a flag the `ImportDeclaration` visitor has not
|
|
248
|
+
* set yet, duplicating the import.
|
|
249
|
+
*/
|
|
235
250
|
function hasNamedValueImport(sourceCode, source, name) {
|
|
236
251
|
for (const declaration of getValueImports(sourceCode, source)) {
|
|
237
252
|
for (const spec of declaration.specifiers) {
|
|
238
|
-
if (spec
|
|
239
|
-
spec.importKind !== 'type' &&
|
|
240
|
-
spec.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
241
|
-
spec.imported.name === name &&
|
|
242
|
-
spec.local.name === name) {
|
|
253
|
+
if (isNamedValueSpecifier(spec, name)) {
|
|
243
254
|
return true;
|
|
244
255
|
}
|
|
245
256
|
}
|
|
246
257
|
}
|
|
247
258
|
return false;
|
|
248
259
|
}
|
|
260
|
+
/**
|
|
261
|
+
* Whether every declaration of a visible binding is the value import this fix
|
|
262
|
+
* intends to add. A local const/function/class, a parameter, a namespace or
|
|
263
|
+
* default import, an alias, or a named import from any other module all mean
|
|
264
|
+
* the emitted `useMemo(() => stableHash(...))` would resolve somewhere other
|
|
265
|
+
* than the helper — so the binding cannot be reused.
|
|
266
|
+
*/
|
|
267
|
+
function bindsIntendedImport(variable, name, source) {
|
|
268
|
+
return (variable.defs.length > 0 &&
|
|
269
|
+
variable.defs.every((def) => {
|
|
270
|
+
const specifier = def.node;
|
|
271
|
+
if (!isNamedValueSpecifier(specifier, name)) {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
const declaration = specifier.parent;
|
|
275
|
+
return (declaration?.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
|
|
276
|
+
declaration.importKind !== 'type' &&
|
|
277
|
+
declaration.source.value === source);
|
|
278
|
+
}));
|
|
279
|
+
}
|
|
249
280
|
/**
|
|
250
281
|
* Extends an existing import from `source` with `name` instead of prepending
|
|
251
282
|
* a duplicate declaration. Namespace-only imports cannot host a named
|
|
@@ -437,6 +468,30 @@ exports.noArrayLengthInDeps = (0, createRule_1.createRule)({
|
|
|
437
468
|
// All bail checks precede any shared-state mutation so a skipped
|
|
438
469
|
// fix cannot make a later fix believe imports or declarations are
|
|
439
470
|
// already handled.
|
|
471
|
+
// The generated declaration spells both helper names bare and
|
|
472
|
+
// imports both, so a binding of either name breaks the edit two
|
|
473
|
+
// ways: the inserted import collides with a declaration of that
|
|
474
|
+
// name (TS2440, or TS2300 when the binding is itself an import),
|
|
475
|
+
// and a shadowing parameter or block-scoped binding captures the
|
|
476
|
+
// emitted call with no compile error at all. Resolving through the
|
|
477
|
+
// scope chain at the hook call catches both, since the insertion
|
|
478
|
+
// point shares that scope. Because the emitted code needs BOTH
|
|
479
|
+
// names, a clash on either withholds the whole edit — a partial
|
|
480
|
+
// one would still be broken. The report stands so the author
|
|
481
|
+
// resolves the clash deliberately.
|
|
482
|
+
const scope = ASTHelpers_1.ASTHelpers.getScope(context, node);
|
|
483
|
+
const collides = [
|
|
484
|
+
{ name: MEMO_HOOK_NAME, source: REACT_MODULE },
|
|
485
|
+
{
|
|
486
|
+
name: hashImportConfig.importName,
|
|
487
|
+
source: hashImportConfig.source,
|
|
488
|
+
},
|
|
489
|
+
].some(({ name, source }) => {
|
|
490
|
+
const existing = ASTHelpers_1.ASTHelpers.findVariableInScope(scope, name);
|
|
491
|
+
return !!existing && !bindsIntendedImport(existing, name, source);
|
|
492
|
+
});
|
|
493
|
+
if (collides)
|
|
494
|
+
return null;
|
|
440
495
|
const insertion = findInsertionPoint(node);
|
|
441
496
|
if (!insertion)
|
|
442
497
|
return null;
|
|
@@ -483,12 +538,13 @@ exports.noArrayLengthInDeps = (0, createRule_1.createRule)({
|
|
|
483
538
|
}
|
|
484
539
|
if (!importsPlanned) {
|
|
485
540
|
const newImportLines = [];
|
|
486
|
-
if (!hasNamedValueImport(sourceCode,
|
|
487
|
-
const extension = buildImportExtensionFix(fixer, sourceCode,
|
|
541
|
+
if (!hasNamedValueImport(sourceCode, REACT_MODULE, MEMO_HOOK_NAME)) {
|
|
542
|
+
const extension = buildImportExtensionFix(fixer, sourceCode, REACT_MODULE, MEMO_HOOK_NAME);
|
|
488
543
|
if (extension)
|
|
489
544
|
fixes.push(extension);
|
|
490
|
-
else
|
|
491
|
-
newImportLines.push(`import {
|
|
545
|
+
else {
|
|
546
|
+
newImportLines.push(`import { ${MEMO_HOOK_NAME} } from '${REACT_MODULE}';`);
|
|
547
|
+
}
|
|
492
548
|
}
|
|
493
549
|
if (!hasNamedValueImport(sourceCode, hashImportConfig.source, hashImportConfig.importName)) {
|
|
494
550
|
const extension = buildImportExtensionFix(fixer, sourceCode, hashImportConfig.source, hashImportConfig.importName);
|