@blumintinc/eslint-plugin-blumint 1.20.32 → 1.20.34
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-fieldpath-syntax-in-docsetter.js +93 -26
- package/lib/rules/enforce-firestore-set-merge.d.ts +2 -1
- package/lib/rules/enforce-firestore-set-merge.js +237 -60
- package/lib/rules/enforce-global-constants.js +84 -17
- package/lib/rules/enforce-microdiff.js +162 -67
- package/lib/rules/fast-deep-equal-over-microdiff.js +154 -83
- package/lib/rules/flatten-push-calls.js +163 -39
- package/lib/rules/no-unused-usestate.d.ts +2 -2
- package/lib/rules/no-unused-usestate.js +32 -1
- package/lib/rules/prefer-global-router-state-key.js +54 -9
- package/lib/rules/prefer-spread-over-reassembly.js +178 -28
- package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +143 -35
- package/lib/rules/use-custom-memo.js +131 -1
- package/lib/rules/use-latest-callback.js +94 -17
- package/package.json +1 -1
- package/release-manifest.json +112 -0
|
@@ -6,6 +6,28 @@ const createRule_1 = require("../utils/createRule");
|
|
|
6
6
|
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
7
7
|
const DIFF_NAME = 'diff';
|
|
8
8
|
const MICRODIFF_MODULE = 'microdiff';
|
|
9
|
+
/**
|
|
10
|
+
* The names a competing library's diff export is conventionally bound to. They
|
|
11
|
+
* are candidates for a report, never proof of one: what a call resolves to
|
|
12
|
+
* decides.
|
|
13
|
+
*/
|
|
14
|
+
const DIFF_FUNCTION_NAMES = new Set([
|
|
15
|
+
'deepDiff',
|
|
16
|
+
'fastDiff',
|
|
17
|
+
'diffArrays',
|
|
18
|
+
'detailedDiff',
|
|
19
|
+
// 'fastDeepEqual' and 'isEqual' stay out: they are allowed alternatives.
|
|
20
|
+
]);
|
|
21
|
+
/**
|
|
22
|
+
* The exports of a competing library whose call sites this rule rewrites,
|
|
23
|
+
* whatever local name the import binds them to.
|
|
24
|
+
*/
|
|
25
|
+
const COMPETING_DIFF_EXPORTS = new Set(['diff', 'diffArrays', 'detailedDiff']);
|
|
26
|
+
/** Diff libraries this rule permits, whose calls are tracked but never reported. */
|
|
27
|
+
const ALLOWED_DIFF_MODULES = new Set([
|
|
28
|
+
'fast-deep-equal',
|
|
29
|
+
'fast-deep-equal/es6',
|
|
30
|
+
]);
|
|
9
31
|
/**
|
|
10
32
|
* The libraries this rule replaces. Every fix retires the whole import
|
|
11
33
|
* declaration of one of these, which is what makes the names it binds available
|
|
@@ -75,6 +97,75 @@ function collectClaimableSpecifiers(program) {
|
|
|
75
97
|
});
|
|
76
98
|
return claimable;
|
|
77
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* The competing library's import declaration a name resolves to, or null for
|
|
102
|
+
* every other binding.
|
|
103
|
+
*
|
|
104
|
+
* A callee matched by name alone proves nothing on its own: `detailedDiff` is
|
|
105
|
+
* as likely to be the file's own function, a local variable, or a parameter as
|
|
106
|
+
* it is to be `deep-object-diff`'s export. Reporting those renames a call to
|
|
107
|
+
* `diff` that nothing binds while the local definition it was calling survives,
|
|
108
|
+
* so only a name the import handler is about to retire earns a report. The
|
|
109
|
+
* declaration is the one whose rewrite makes the emitted `diff` resolvable,
|
|
110
|
+
* which is what pairs the call fix with the import fix in the same pass.
|
|
111
|
+
*
|
|
112
|
+
* Resolution keys on the *local* name a specifier binds, so an alias that
|
|
113
|
+
* renames a competing export onto one of these names is covered, while an alias
|
|
114
|
+
* that renames it away (`detailedDiff as dd`) is left to `toImportedDiffSource`,
|
|
115
|
+
* which resolves to the specifier itself under whatever name it is bound.
|
|
116
|
+
*/
|
|
117
|
+
function toCompetingDiffImport(variable) {
|
|
118
|
+
if (!variable) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
for (const def of variable.defs) {
|
|
122
|
+
if (def.type !== 'ImportBinding') {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const declaration = def.parent;
|
|
126
|
+
if (!declaration ||
|
|
127
|
+
declaration.type !== utils_1.AST_NODE_TYPES.ImportDeclaration ||
|
|
128
|
+
declaration.importKind === 'type') {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const specifier = def.node;
|
|
132
|
+
if (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
133
|
+
specifier.importKind === 'type') {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (!COMPETING_DIFF_MODULES.has(String(declaration.source.value))) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
return declaration;
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The module a name is imported from, when the name resolves to one of the
|
|
145
|
+
* specifiers the import handler tracked, and null for every other binding.
|
|
146
|
+
*
|
|
147
|
+
* Tracking keys on the specifier node rather than on the local name because
|
|
148
|
+
* names collide and bindings do not: a parameter, a nested `const`, or a
|
|
149
|
+
* function declaration that shadows the imported name answers its own calls.
|
|
150
|
+
* Rewriting one of those to `diff` swaps in microdiff's structural change list
|
|
151
|
+
* for whatever the shadow computed — a substitution that compiles cleanly and
|
|
152
|
+
* leaves the shadow unused, so nothing downstream flags it.
|
|
153
|
+
*/
|
|
154
|
+
function toImportedDiffSource(variable, importedSpecifiers) {
|
|
155
|
+
if (!variable) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
for (const def of variable.defs) {
|
|
159
|
+
if (def.type !== 'ImportBinding') {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const source = importedSpecifiers.get(def.node);
|
|
163
|
+
if (source !== undefined) {
|
|
164
|
+
return source;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
78
169
|
/**
|
|
79
170
|
* Whether a bare `diff` written at `scope` reaches microdiff's function.
|
|
80
171
|
* Resolving through the scope chain catches both failure modes: a module-scope
|
|
@@ -110,8 +201,17 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
|
|
|
110
201
|
create(context) {
|
|
111
202
|
const sourceCode = context.sourceCode;
|
|
112
203
|
const importedDiffLibraries = new Map();
|
|
113
|
-
|
|
204
|
+
// The diff specifiers this file imports, keyed by node so a call site can
|
|
205
|
+
// ask whether the binding it resolves to is one of them.
|
|
206
|
+
const importedDiffSpecifiers = new Map();
|
|
207
|
+
// The local names those specifiers bind: a cheap gate that keeps calls with
|
|
208
|
+
// no relation to a diff library off the scope chain.
|
|
209
|
+
const importedDiffNames = new Set();
|
|
114
210
|
const reportedNodes = new Set();
|
|
211
|
+
function trackImportedDiff(specifier, importSource) {
|
|
212
|
+
importedDiffSpecifiers.set(specifier, importSource);
|
|
213
|
+
importedDiffNames.add(specifier.local.name);
|
|
214
|
+
}
|
|
115
215
|
/**
|
|
116
216
|
* Whether the fix may emit a bare `diff` at `node`. The AST is read at fix
|
|
117
217
|
* time so the answer stays correct under multi-pass `--fix`, where an
|
|
@@ -180,12 +280,10 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
|
|
|
180
280
|
// Track imported function names and their sources
|
|
181
281
|
node.specifiers.forEach((specifier) => {
|
|
182
282
|
if (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
183
|
-
(specifier.imported.name
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const localName = specifier.local.name;
|
|
188
|
-
importedFunctions.set(localName, importSource);
|
|
283
|
+
COMPETING_DIFF_EXPORTS.has(specifier.imported.name)) {
|
|
284
|
+
// Track the specifier itself, so renaming the export and
|
|
285
|
+
// shadowing the local name both stay accounted for
|
|
286
|
+
trackImportedDiff(specifier, importSource);
|
|
189
287
|
}
|
|
190
288
|
// Removed the fast-deep-equal handling since it's now allowed
|
|
191
289
|
});
|
|
@@ -212,25 +310,18 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
|
|
|
212
310
|
},
|
|
213
311
|
});
|
|
214
312
|
// Check if importing a diff function or a known equality library
|
|
215
|
-
const hasDiffImport = node.specifiers.some((specifier) =>
|
|
216
|
-
|
|
217
|
-
return (specifier.imported.name === 'diff' ||
|
|
218
|
-
specifier.imported.name === 'diffArrays' ||
|
|
219
|
-
specifier.imported.name === 'detailedDiff');
|
|
220
|
-
}
|
|
221
|
-
return false;
|
|
222
|
-
});
|
|
313
|
+
const hasDiffImport = node.specifiers.some((specifier) => specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
314
|
+
COMPETING_DIFF_EXPORTS.has(specifier.imported.name));
|
|
223
315
|
if (hasDiffImport) {
|
|
224
316
|
importedDiffLibraries.set(importSource, node);
|
|
225
317
|
}
|
|
226
318
|
}
|
|
227
319
|
// Special handling for fast-deep-equal: track it but don't report it
|
|
228
|
-
if (importSource
|
|
229
|
-
importSource === 'fast-deep-equal/es6') {
|
|
320
|
+
if (ALLOWED_DIFF_MODULES.has(importSource)) {
|
|
230
321
|
// Track imported function names for later reference
|
|
231
322
|
node.specifiers.forEach((specifier) => {
|
|
232
323
|
if (specifier.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier) {
|
|
233
|
-
|
|
324
|
+
trackImportedDiff(specifier, importSource);
|
|
234
325
|
}
|
|
235
326
|
});
|
|
236
327
|
// Add to importedDiffLibraries for tracking but don't report
|
|
@@ -247,45 +338,21 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
|
|
|
247
338
|
// Check for direct calls to imported diff functions
|
|
248
339
|
if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
249
340
|
const name = callee.name;
|
|
250
|
-
|
|
251
|
-
if (
|
|
252
|
-
|
|
253
|
-
//
|
|
254
|
-
const
|
|
255
|
-
//
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
fix(fixer) {
|
|
266
|
-
if (!canEmitDiffAt(node)) {
|
|
267
|
-
return null;
|
|
268
|
-
}
|
|
269
|
-
return fixer.replaceText(callee, DIFF_NAME);
|
|
270
|
-
},
|
|
271
|
-
});
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
// Check known diff function names
|
|
275
|
-
const isDiffFunction = [
|
|
276
|
-
'deepDiff',
|
|
277
|
-
'fastDiff',
|
|
278
|
-
'diffArrays',
|
|
279
|
-
'detailedDiff',
|
|
280
|
-
// Removed 'fastDeepEqual' and 'isEqual' as they are allowed alternatives
|
|
281
|
-
].includes(name);
|
|
282
|
-
if (isDiffFunction) {
|
|
283
|
-
// Track this import name as used
|
|
284
|
-
usedImportNames.add(name);
|
|
285
|
-
// Check if we have at least 2 arguments that are objects or arrays
|
|
286
|
-
if (node.arguments.length >= 2 &&
|
|
287
|
-
isObjectOrArrayType(node.arguments[0]) &&
|
|
288
|
-
isObjectOrArrayType(node.arguments[1])) {
|
|
341
|
+
const isDiffFunctionName = DIFF_FUNCTION_NAMES.has(name);
|
|
342
|
+
if (importedDiffNames.has(name) || isDiffFunctionName) {
|
|
343
|
+
// Both paths below ask what the callee binds, so the scope chain is
|
|
344
|
+
// walked once for the name they share.
|
|
345
|
+
const calleeVariable = ASTHelpers_1.ASTHelpers.findVariableInScope(ASTHelpers_1.ASTHelpers.getScope(context, node), name);
|
|
346
|
+
// Check if this call resolves to a function we specifically
|
|
347
|
+
// imported from a diff library
|
|
348
|
+
const importSource = toImportedDiffSource(calleeVariable, importedDiffSpecifiers);
|
|
349
|
+
if (importSource) {
|
|
350
|
+
usedImportNames.add(name);
|
|
351
|
+
// Skip reporting if it's from fast-deep-equal
|
|
352
|
+
if (ALLOWED_DIFF_MODULES.has(importSource)) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
// Report it if it's from any other tracked library
|
|
289
356
|
reportedNodes.add(node);
|
|
290
357
|
context.report({
|
|
291
358
|
node,
|
|
@@ -294,10 +361,38 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
|
|
|
294
361
|
if (!canEmitDiffAt(node)) {
|
|
295
362
|
return null;
|
|
296
363
|
}
|
|
297
|
-
// When handling fast-diff and similar libraries, need to ensure the function name is replaced
|
|
298
364
|
return fixer.replaceText(callee, DIFF_NAME);
|
|
299
365
|
},
|
|
300
366
|
});
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (isDiffFunctionName) {
|
|
370
|
+
// The name is only a candidate until the scope chain says what it
|
|
371
|
+
// binds: a local function, variable, parameter, or an import from
|
|
372
|
+
// anywhere but a competing diff library keeps its call untouched.
|
|
373
|
+
const competingImport = toCompetingDiffImport(calleeVariable);
|
|
374
|
+
if (!competingImport) {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
// Track this import name as used
|
|
378
|
+
usedImportNames.add(name);
|
|
379
|
+
// Check if we have at least 2 arguments that are objects or arrays
|
|
380
|
+
if (node.arguments.length >= 2 &&
|
|
381
|
+
isObjectOrArrayType(node.arguments[0]) &&
|
|
382
|
+
isObjectOrArrayType(node.arguments[1])) {
|
|
383
|
+
reportedNodes.add(node);
|
|
384
|
+
context.report({
|
|
385
|
+
node,
|
|
386
|
+
messageId: 'enforceMicrodiff',
|
|
387
|
+
fix(fixer) {
|
|
388
|
+
if (!canEmitDiffAt(node)) {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
// When handling fast-diff and similar libraries, need to ensure the function name is replaced
|
|
392
|
+
return fixer.replaceText(callee, DIFF_NAME);
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
}
|
|
301
396
|
}
|
|
302
397
|
}
|
|
303
398
|
}
|
|
@@ -308,19 +403,19 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
|
|
|
308
403
|
const property = callee.property;
|
|
309
404
|
if (property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
310
405
|
['difference', 'differenceBy', 'differenceWith'].includes(property.name)) {
|
|
406
|
+
// Report only. lodash's difference family returns the elements of
|
|
407
|
+
// its first array that have no match in the second — a subset of
|
|
408
|
+
// the input — while microdiff's `diff` returns a structural change
|
|
409
|
+
// list of `{type, path, value}` records. No mechanical rewrite
|
|
410
|
+
// preserves the meaning of the call, and the extra iteratee or
|
|
411
|
+
// comparator argument has no counterpart in `diff(a, b)`, whose
|
|
412
|
+
// third parameter is an options object. Replacing the call would
|
|
413
|
+
// silently change what the surrounding code receives, so the author
|
|
414
|
+
// converts it.
|
|
311
415
|
reportedNodes.add(node);
|
|
312
416
|
context.report({
|
|
313
417
|
node,
|
|
314
418
|
messageId: 'enforceMicrodiff',
|
|
315
|
-
fix(fixer) {
|
|
316
|
-
if (!canEmitDiffAt(node)) {
|
|
317
|
-
return null;
|
|
318
|
-
}
|
|
319
|
-
// Replace with microdiff
|
|
320
|
-
return fixer.replaceText(node, `${DIFF_NAME}(${node.arguments
|
|
321
|
-
.map((arg) => sourceCode.getText(arg))
|
|
322
|
-
.join(', ')})`);
|
|
323
|
-
},
|
|
324
419
|
});
|
|
325
420
|
}
|
|
326
421
|
}
|
|
@@ -29,6 +29,15 @@ function bindsFastDeepEqual(variable) {
|
|
|
29
29
|
FAST_DEEP_EQUAL_MODULES.has(declaration.source.value));
|
|
30
30
|
}));
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Whether two edits touch the same characters. ESLint sorts the fixes of one
|
|
34
|
+
* report and asserts each starts at or after the end of the previous one, so
|
|
35
|
+
* abutting ranges are fine while overlapping ranges throw and discard every
|
|
36
|
+
* message for the file.
|
|
37
|
+
*/
|
|
38
|
+
function rangesOverlap(a, b) {
|
|
39
|
+
return a[0] < b[1] && b[0] < a[1];
|
|
40
|
+
}
|
|
32
41
|
exports.fastDeepEqualOverMicrodiff = (0, createRule_1.createRule)({
|
|
33
42
|
name: 'fast-deep-equal-over-microdiff',
|
|
34
43
|
meta: {
|
|
@@ -301,25 +310,133 @@ exports.fastDeepEqualOverMicrodiff = (0, createRule_1.createRule)({
|
|
|
301
310
|
return undefined;
|
|
302
311
|
}
|
|
303
312
|
/**
|
|
304
|
-
*
|
|
313
|
+
* The call's own text with nothing but the callee name swapped for the
|
|
314
|
+
* fast-deep-equal name. Slicing the original text keeps the argument list —
|
|
315
|
+
* including its formatting and any comments inside it — byte-identical,
|
|
316
|
+
* which matters when the call has to be re-emitted at another position.
|
|
317
|
+
*/
|
|
318
|
+
function renameCallee(diffCall) {
|
|
319
|
+
const callText = sourceCode.getText(diffCall);
|
|
320
|
+
const start = diffCall.range[0];
|
|
321
|
+
return (callText.slice(0, diffCall.callee.range[0] - start) +
|
|
322
|
+
fastDeepEqualImportName +
|
|
323
|
+
callText.slice(diffCall.callee.range[1] - start));
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* The `const changes = diff(...)` statement behind a `changes.length`
|
|
327
|
+
* comparison, when nothing but the comparison reads it — so inlining the
|
|
328
|
+
* call makes the statement dead. A statement declaring more than one
|
|
329
|
+
* variable stays: dropping it would take the other declarators with it.
|
|
330
|
+
*/
|
|
331
|
+
function findRedundantDeclaration(node) {
|
|
332
|
+
const lengthIdentifier = getLengthIdentifierFromNode(node);
|
|
333
|
+
if (!lengthIdentifier || !isVariableOnlyUsedForLength(lengthIdentifier)) {
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
336
|
+
const variable = findVariableInScopeChain(lengthIdentifier);
|
|
337
|
+
const defNode = variable?.defs?.[0]?.node;
|
|
338
|
+
if (!defNode || defNode.type !== utils_1.AST_NODE_TYPES.VariableDeclarator) {
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
341
|
+
const declaration = defNode.parent;
|
|
342
|
+
if (declaration?.type !== utils_1.AST_NODE_TYPES.VariableDeclaration ||
|
|
343
|
+
declaration.declarations.length !== 1) {
|
|
344
|
+
return undefined;
|
|
345
|
+
}
|
|
346
|
+
return declaration;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* The span to delete for a redundant declaration. A declaration that owns
|
|
350
|
+
* its whole line takes the line with it, leaving no blank line behind.
|
|
351
|
+
* Otherwise only its own node range goes: a line-wide span would swallow
|
|
352
|
+
* whatever shares the line — the comparison this same report rewrites (an
|
|
353
|
+
* overlap ESLint refuses), the `;` separating a `for` header's clauses, or a
|
|
354
|
+
* trailing directive that governs the surviving next line.
|
|
305
355
|
*/
|
|
306
|
-
function
|
|
356
|
+
function redundantDeclarationRange(declaration) {
|
|
307
357
|
const text = sourceCode.text;
|
|
308
|
-
const start =
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
358
|
+
const [start, end] = declaration.range;
|
|
359
|
+
const startOfLine = text.lastIndexOf('\n', start - 1) + 1;
|
|
360
|
+
const nextNewline = text.indexOf('\n', end);
|
|
361
|
+
const lineEnd = nextNewline === -1 ? text.length : nextNewline;
|
|
362
|
+
const isBlank = (slice) => /^[ \t]*$/.test(slice);
|
|
363
|
+
// A comment counts as content, so it keeps its line rather than riding
|
|
364
|
+
// along with the statement being deleted.
|
|
365
|
+
if (isBlank(text.slice(startOfLine, start)) &&
|
|
366
|
+
isBlank(text.slice(end, lineEnd))) {
|
|
367
|
+
return [startOfLine, nextNewline === -1 ? end : nextNewline + 1];
|
|
368
|
+
}
|
|
369
|
+
// The blanks between the declaration and what follows it on the line go
|
|
370
|
+
// too, so the surviving statement keeps its original spacing.
|
|
371
|
+
let removalEnd = end;
|
|
372
|
+
while (removalEnd < lineEnd && isBlank(text[removalEnd])) {
|
|
373
|
+
removalEnd++;
|
|
374
|
+
}
|
|
375
|
+
if (removalEnd < lineEnd) {
|
|
376
|
+
return [start, removalEnd];
|
|
377
|
+
}
|
|
378
|
+
// Nothing but the line break follows, so the blanks in front of the
|
|
379
|
+
// declaration would be left behind as trailing whitespace.
|
|
380
|
+
let removalStart = start;
|
|
381
|
+
while (removalStart > startOfLine && isBlank(text[removalStart - 1])) {
|
|
382
|
+
removalStart--;
|
|
383
|
+
}
|
|
384
|
+
return [removalStart, removalEnd];
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* The edits that turn the comparison itself into a fast-deep-equal call.
|
|
388
|
+
*/
|
|
389
|
+
function createComparisonFixes(fixer, node, diffCall, isEquality) {
|
|
390
|
+
// Comparing a call in place — `diff(a, b).length === 0` — is rewritten by
|
|
391
|
+
// splicing the two ranges that actually change: the callee name and the
|
|
392
|
+
// `.length` comparison tail (plus the `!`/`0 ===` head). Re-emitting the
|
|
393
|
+
// argument list instead destroyed everything between the arguments, and a
|
|
394
|
+
// dropped `eslint-disable` comment silently re-enables the rule it was
|
|
395
|
+
// suppressing.
|
|
396
|
+
const isCallInPlace = diffCall.range[0] >= node.range[0] &&
|
|
397
|
+
diffCall.range[1] <= node.range[1];
|
|
398
|
+
if (!isCallInPlace) {
|
|
399
|
+
// `changes.length === 0` compares a call declared elsewhere, so that
|
|
400
|
+
// call moves to the comparison's position and its text has to be
|
|
401
|
+
// re-emitted — verbatim, so its comments move with it.
|
|
402
|
+
const call = renameCallee(diffCall);
|
|
403
|
+
return [fixer.replaceText(node, isEquality ? call : `!${call}`)];
|
|
404
|
+
}
|
|
405
|
+
const fixes = [];
|
|
406
|
+
const headRange = [node.range[0], diffCall.range[0]];
|
|
407
|
+
if (!isEquality) {
|
|
408
|
+
fixes.push(fixer.replaceTextRange(headRange, '!'));
|
|
409
|
+
}
|
|
410
|
+
else if (headRange[0] !== headRange[1]) {
|
|
411
|
+
fixes.push(fixer.removeRange(headRange));
|
|
412
|
+
}
|
|
413
|
+
fixes.push(fixer.replaceText(diffCall.callee, fastDeepEqualImportName));
|
|
414
|
+
fixes.push(fixer.removeRange([diffCall.range[1], node.range[1]]));
|
|
415
|
+
return fixes;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* The `import ... from 'fast-deep-equal'` edit, scheduled at most once per
|
|
419
|
+
* file. Claiming the carrier slot is a side effect, so this runs last —
|
|
420
|
+
* after every reason to decline the fix has been ruled out — otherwise a
|
|
421
|
+
* declining violation takes the import down with it and leaves the
|
|
422
|
+
* surviving violations' `isEqual(...)` calls unbound.
|
|
423
|
+
*/
|
|
424
|
+
function planFastDeepEqualImport(fixer) {
|
|
425
|
+
if (hasFastDeepEqualImport || plannedFastDeepEqualImport) {
|
|
426
|
+
return [];
|
|
427
|
+
}
|
|
428
|
+
plannedFastDeepEqualImport = true;
|
|
429
|
+
const importDeclarations = sourceCode.ast.body.filter((statement) => statement.type === utils_1.AST_NODE_TYPES.ImportDeclaration);
|
|
430
|
+
const microdiffImport = importDeclarations.find((declaration) => declaration.source.value === 'microdiff');
|
|
431
|
+
const anchor = microdiffImport ?? importDeclarations[importDeclarations.length - 1];
|
|
432
|
+
if (anchor) {
|
|
433
|
+
return [
|
|
434
|
+
fixer.insertTextAfter(anchor, `\nimport ${fastDeepEqualImportName} from 'fast-deep-equal';`),
|
|
435
|
+
];
|
|
321
436
|
}
|
|
322
|
-
return
|
|
437
|
+
return [
|
|
438
|
+
fixer.insertTextBeforeRange([0, 0], `import ${fastDeepEqualImportName} from 'fast-deep-equal';\n`),
|
|
439
|
+
];
|
|
323
440
|
}
|
|
324
441
|
/**
|
|
325
442
|
* Create a fix for replacing microdiff equality check with fast-deep-equal
|
|
@@ -343,77 +460,31 @@ exports.fastDeepEqualOverMicrodiff = (0, createRule_1.createRule)({
|
|
|
343
460
|
if (existingBinding && !bindsFastDeepEqual(existingBinding)) {
|
|
344
461
|
return null;
|
|
345
462
|
}
|
|
346
|
-
|
|
347
|
-
if (args.length !== 2) {
|
|
463
|
+
if (diffCall.arguments.length !== 2) {
|
|
348
464
|
return null; // Can't fix if not exactly 2 arguments
|
|
349
465
|
}
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
const
|
|
353
|
-
//
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
fixes.push(fixer.insertTextAfter(lastImport, `\nimport ${fastDeepEqualImportName} from 'fast-deep-equal';`));
|
|
365
|
-
}
|
|
366
|
-
else {
|
|
367
|
-
fixes.push(fixer.insertTextBeforeRange([0, 0], `import ${fastDeepEqualImportName} from 'fast-deep-equal';\n`));
|
|
368
|
-
}
|
|
369
|
-
plannedFastDeepEqualImport = true;
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
// If the equality was via an identifier like `changes.length`, and that identifier
|
|
373
|
-
// is declared as `const changes = diff(...);` and used ONLY for `.length` checks,
|
|
374
|
-
// remove the redundant variable declaration.
|
|
375
|
-
const maybeIdentifier = getLengthIdentifierFromNode(node);
|
|
376
|
-
if (maybeIdentifier && isVariableOnlyUsedForLength(maybeIdentifier)) {
|
|
377
|
-
const variable = findVariableInScopeChain(maybeIdentifier);
|
|
378
|
-
const defNode = variable?.defs?.[0]?.node;
|
|
379
|
-
if (defNode &&
|
|
380
|
-
defNode.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
381
|
-
defNode.parent &&
|
|
382
|
-
defNode.parent.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
|
|
383
|
-
const declaration = defNode.parent;
|
|
384
|
-
// Only remove if it's the only declaration in the const/let statement
|
|
385
|
-
if (declaration.declarations.length === 1) {
|
|
386
|
-
const text = sourceCode.text;
|
|
387
|
-
const startOfLine = text.lastIndexOf('\n', declaration.range[0] - 1) + 1;
|
|
388
|
-
const nextNewline = text.indexOf('\n', declaration.range[1]);
|
|
389
|
-
const endOfLine = nextNewline === -1 ? declaration.range[1] : nextNewline + 1;
|
|
390
|
-
fixes.push(fixer.removeRange([startOfLine, endOfLine]));
|
|
391
|
-
}
|
|
466
|
+
const comparisonFixes = createComparisonFixes(fixer, node, diffCall, isEquality);
|
|
467
|
+
// If the equality was via an identifier like `changes.length`, and that
|
|
468
|
+
// identifier is declared as `const changes = diff(...);` and used ONLY for
|
|
469
|
+
// `.length` checks, remove the redundant variable declaration.
|
|
470
|
+
const declaration = findRedundantDeclaration(node);
|
|
471
|
+
const declarationFixes = [];
|
|
472
|
+
if (declaration) {
|
|
473
|
+
const removalRange = redundantDeclarationRange(declaration);
|
|
474
|
+
// A comparison nested inside the declaration it would delete — a diff
|
|
475
|
+
// argument that reads `changes.length` — admits no pair of disjoint
|
|
476
|
+
// edits. Reporting without a fix beats an overlap, which aborts the
|
|
477
|
+
// file, and beats a half-applied edit that deletes the surviving code.
|
|
478
|
+
if (comparisonFixes.some(({ range }) => rangesOverlap(range, removalRange))) {
|
|
479
|
+
return null;
|
|
392
480
|
}
|
|
481
|
+
declarationFixes.push(fixer.removeRange(removalRange));
|
|
393
482
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
let replacement;
|
|
400
|
-
if (isMultilineCall) {
|
|
401
|
-
const baseIndent = getLineBaseIndent(node);
|
|
402
|
-
const innerIndentPlus = baseIndent + ' ';
|
|
403
|
-
replacement =
|
|
404
|
-
`${fastDeepEqualImportName}(` +
|
|
405
|
-
`\n${innerIndentPlus}${arg1},` +
|
|
406
|
-
`\n${innerIndentPlus}${arg2},` +
|
|
407
|
-
`\n${baseIndent})`;
|
|
408
|
-
}
|
|
409
|
-
else {
|
|
410
|
-
replacement = `${fastDeepEqualImportName}(${arg1}, ${arg2})`;
|
|
411
|
-
}
|
|
412
|
-
if (!isEquality) {
|
|
413
|
-
replacement = `!${replacement}`;
|
|
414
|
-
}
|
|
415
|
-
fixes.push(fixer.replaceText(node, replacement));
|
|
416
|
-
return fixes;
|
|
483
|
+
return [
|
|
484
|
+
...planFastDeepEqualImport(fixer),
|
|
485
|
+
...declarationFixes,
|
|
486
|
+
...comparisonFixes,
|
|
487
|
+
];
|
|
417
488
|
}
|
|
418
489
|
function reportEqualityCheck(node, result) {
|
|
419
490
|
const { diffCall, isEquality } = result;
|