@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.
@@ -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
- const importedFunctions = new Map(); // Map of imported function names to their sources
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 === 'diff' ||
184
- specifier.imported.name === 'diffArrays' ||
185
- specifier.imported.name === 'detailedDiff')) {
186
- // Track the local name (which could be different due to renaming)
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
- if (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier) {
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 === 'fast-deep-equal' ||
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
- importedFunctions.set(specifier.local.name, importSource);
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
- // Check if this is a function we specifically imported from a diff library
251
- if (importedFunctions.has(name)) {
252
- usedImportNames.add(name);
253
- // Get the source of the imported function
254
- const importSource = importedFunctions.get(name);
255
- // Skip reporting if it's from fast-deep-equal
256
- if (importSource === 'fast-deep-equal' ||
257
- importSource === 'fast-deep-equal/es6') {
258
- return;
259
- }
260
- // Report it if it's from any other tracked library
261
- reportedNodes.add(node);
262
- context.report({
263
- node,
264
- messageId: 'enforceMicrodiff',
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
- * Get the indentation at the start of the current line for a node
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 getLineBaseIndent(node) {
356
+ function redundantDeclarationRange(declaration) {
307
357
  const text = sourceCode.text;
308
- const start = node.range?.[0] ?? sourceCode.getIndexFromLoc(node.loc.start);
309
- const lineStart = text.lastIndexOf('\n', start - 1) + 1;
310
- let i = lineStart;
311
- let indent = '';
312
- while (i < text.length) {
313
- const ch = text[i];
314
- if (ch === ' ' || ch === '\t') {
315
- indent += ch;
316
- i++;
317
- }
318
- else {
319
- break;
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 indent;
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
- const args = diffCall.arguments;
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 arg1 = sourceCode.getText(args[0]);
351
- const arg2 = sourceCode.getText(args[1]);
352
- const fixes = [];
353
- // Add import if needed (only once across all fixes in this file)
354
- if (!hasFastDeepEqualImport && !plannedFastDeepEqualImport) {
355
- const importDeclarations = sourceCode.ast.body.filter((node) => node.type === utils_1.AST_NODE_TYPES.ImportDeclaration);
356
- const microdiffImport = importDeclarations.find((node) => node.source.value === 'microdiff');
357
- if (microdiffImport) {
358
- fixes.push(fixer.insertTextAfter(microdiffImport, `\nimport ${fastDeepEqualImportName} from 'fast-deep-equal';`));
359
- plannedFastDeepEqualImport = true;
360
- }
361
- else {
362
- const lastImport = importDeclarations[importDeclarations.length - 1];
363
- if (lastImport) {
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
- // Replace the equality expression with isEqual call
395
- const isMultilineCall = diffCall.loc &&
396
- args[0].loc &&
397
- (diffCall.loc.start.line !== args[0].loc.start.line ||
398
- (args[1] && args[0].loc.start.line !== args[1].loc.start.line));
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;