@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.
@@ -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 === 'microdiff') {
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 (hasMicrodiffImport) {
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 { diff } from 'microdiff';`);
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
- return fixer.replaceText(callee, 'diff');
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, 'diff');
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, `diff(${node.arguments
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
- !hasMicrodiffImport) {
382
+ !findMicrodiffImport(sourceCode.ast)) {
262
383
  // Need to add an import
263
- const importFix = fixer.insertTextBeforeRange([0, 0], "import { diff } from 'microdiff';\n\n");
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, `diff(${sourceCode.getText(leftArg)}, ${sourceCode.getText(rightArg)})${isEqual ? '.length === 0' : '.length > 0'}`);
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, `diff(${sourceCode.getText(leftArg)}, ${sourceCode.getText(rightArg)})${isEqual ? '.length === 0' : '.length > 0'}`);
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 diff(${param1}, ${param2}).length > 0;
437
+ return ${DIFF_NAME}(${param1}, ${param2}).length > 0;
314
438
  }`;
315
- if (!hasMicrodiffImport) {
439
+ if (!findMicrodiffImport(sourceCode.ast)) {
316
440
  // Create a new import statement
317
- return fixer.replaceText(node, `import { diff } from 'microdiff';\n\nfunction ${node.id?.name}(${param1}, ${param2}) ${newFunctionBody}`);
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.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
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, 'react', 'useMemo')) {
487
- const extension = buildImportExtensionFix(fixer, sourceCode, 'react', 'useMemo');
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 { useMemo } from 'react';`);
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);