@blumintinc/eslint-plugin-blumint 1.20.31 → 1.20.32

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