@blumintinc/eslint-plugin-blumint 1.20.30 → 1.20.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js 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.30',
226
+ version: '1.20.32',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -10,6 +10,26 @@ const createRule_1 = require("../utils/createRule");
10
10
  const ASTHelpers_1 = require("../utils/ASTHelpers");
11
11
  const disableDirectives_1 = require("../utils/disableDirectives");
12
12
  const DEFAULT_IMPORT_PATH = 'functions/src/util/assertSafe';
13
+ const ASSERT_SAFE_NAME = 'assertSafe';
14
+ /**
15
+ * A named specifier that binds `assertSafe` under its own name — the only shape
16
+ * that makes a bare `assertSafe(...)` call resolve to the helper. An alias
17
+ * (`import { assertSafe as ensureSafe }`) leaves the name free for the injected
18
+ * import, and a type-only specifier erases at compile time.
19
+ */
20
+ function isAssertSafeSpecifier(specifier) {
21
+ return (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
22
+ specifier.importKind !== 'type' &&
23
+ specifier.imported.name === ASSERT_SAFE_NAME &&
24
+ specifier.local.name === ASSERT_SAFE_NAME);
25
+ }
26
+ /**
27
+ * Drops the extension and normalizes separators so two spellings of one module
28
+ * compare equal.
29
+ */
30
+ function normalizeModulePath(value) {
31
+ return value.replace(/\\/g, '/').replace(/\.(tsx?|jsx?|mts|cts)$/i, '');
32
+ }
13
33
  exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
14
34
  name: 'enforce-assert-safe-object-key',
15
35
  meta: {
@@ -35,7 +55,14 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
35
55
  defaultOptions: [{}],
36
56
  create(context, [options]) {
37
57
  const importPath = options?.assertSafeImportPath || DEFAULT_IMPORT_PATH;
38
- let hasAssertSafeImport = false;
58
+ // Repo-root-anchored location of the helper, the yardstick every module
59
+ // specifier written in the file is compared against.
60
+ const assertSafeTarget = normalizeModulePath(importPath);
61
+ // Whether an earlier fix in this pass already carries the import. The AST is
62
+ // not re-parsed between the fixes of a single pass, so the import can only
63
+ // be claimed once: a second fix repeating it would span the same insertion
64
+ // point, overlap, and be dropped along with its wrap.
65
+ let importClaimed = false;
39
66
  /**
40
67
  * The `import { assertSafe }` statement rides on a single violation's fix,
41
68
  * making that violation the file's import carrier. A suppressed carrier
@@ -43,6 +70,21 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
43
70
  * emit `assertSafe(...)`, leaving the call unbound.
44
71
  */
45
72
  const isReportSuppressed = (0, disableDirectives_1.createSuppressionChecker)(context);
73
+ /**
74
+ * Directory of the file being fixed, relative to the repo root, or null for
75
+ * virtual/stdin files (RuleTester default 'file.ts', '<input>', '<text>')
76
+ * whose non-absolute name cannot anchor a relative path.
77
+ */
78
+ const fileDirFromRoot = () => {
79
+ const rawFilename = context.getFilename().replace(/\\/g, '/');
80
+ if (!path_1.default.isAbsolute(rawFilename)) {
81
+ return null;
82
+ }
83
+ const fileRelToCwd = path_1.default
84
+ .relative(process.cwd(), rawFilename)
85
+ .replace(/\\/g, '/');
86
+ return path_1.default.posix.dirname(fileRelToCwd);
87
+ };
46
88
  /**
47
89
  * Computes the module specifier for the injected assertSafe import.
48
90
  *
@@ -56,27 +98,69 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
56
98
  * file being fixed so the emitted import resolves from that file's location.
57
99
  */
58
100
  const computeImportSpecifier = () => {
59
- const rawFilename = context.getFilename().replace(/\\/g, '/');
60
- // Virtual/stdin files (RuleTester default 'file.ts', '<input>', '<text>')
61
- // are not absolute; we cannot anchor a relative path, so emit the
62
- // configured path verbatim (preserves the option's literal value for
63
- // non-file lints).
64
- if (!path_1.default.isAbsolute(rawFilename)) {
101
+ const fileDir = fileDirFromRoot();
102
+ // Emitting the configured path verbatim preserves the option's literal
103
+ // value for non-file lints.
104
+ if (fileDir === null) {
65
105
  return importPath;
66
106
  }
67
- const target = importPath
68
- .replace(/\\/g, '/')
69
- .replace(/\.(tsx?|jsx?|mts|cts)$/i, '');
70
- const fileRelToCwd = path_1.default
71
- .relative(process.cwd(), rawFilename)
72
- .replace(/\\/g, '/');
73
- const fileDir = path_1.default.posix.dirname(fileRelToCwd);
74
- let specifier = path_1.default.posix.relative(fileDir, target);
107
+ let specifier = path_1.default.posix.relative(fileDir, assertSafeTarget);
75
108
  if (!specifier.startsWith('.')) {
76
109
  specifier = `./${specifier}`;
77
110
  }
78
111
  return specifier;
79
112
  };
113
+ /**
114
+ * Whether a module specifier written in the file denotes the same helper the
115
+ * fix imports. A relative specifier is resolved against the file's own
116
+ * directory before the comparison, because the configured path is anchored
117
+ * at the repo root: `../../assertSafe` inside functions/src/util/a/b and
118
+ * `functions/src/util/assertSafe` name one module, and treating them as
119
+ * different would withhold the fix from files that import the helper
120
+ * perfectly well.
121
+ */
122
+ const isAssertSafeModule = (source) => {
123
+ const normalized = normalizeModulePath(source);
124
+ if (normalized === assertSafeTarget) {
125
+ return true;
126
+ }
127
+ if (!normalized.startsWith('.')) {
128
+ return false;
129
+ }
130
+ const fileDir = fileDirFromRoot();
131
+ if (fileDir === null) {
132
+ return false;
133
+ }
134
+ return (path_1.default.posix.normalize(path_1.default.posix.join(fileDir, normalized)) ===
135
+ assertSafeTarget);
136
+ };
137
+ /**
138
+ * Read the import off the AST instead of a traversal flag: a violation that
139
+ * precedes the import declaration in source order would otherwise be judged
140
+ * against a flag the `ImportDeclaration` visitor has not set yet, emitting a
141
+ * duplicate import.
142
+ */
143
+ const importsAssertSafe = (program) => program.body.some((statement) => statement.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
144
+ statement.importKind !== 'type' &&
145
+ isAssertSafeModule(statement.source.value) &&
146
+ statement.specifiers.some(isAssertSafeSpecifier));
147
+ /**
148
+ * Whether every declaration of a visible `assertSafe` binding is the helper
149
+ * import itself. A local const/function/class, a parameter, a namespace or
150
+ * default import, or a named import from another module all mean the emitted
151
+ * `assertSafe(...)` call would resolve somewhere other than the helper.
152
+ */
153
+ const bindsAssertSafe = (variable) => variable.defs.length > 0 &&
154
+ variable.defs.every((def) => {
155
+ const specifier = def.node;
156
+ if (!isAssertSafeSpecifier(specifier)) {
157
+ return false;
158
+ }
159
+ const declaration = specifier.parent;
160
+ return (declaration?.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
161
+ declaration.importKind !== 'type' &&
162
+ isAssertSafeModule(declaration.source.value));
163
+ });
80
164
  /**
81
165
  * Helper function to add assertSafe import if needed
82
166
  */
@@ -96,10 +180,9 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
96
180
  */
97
181
  const createFixes = (fixer, node, argText) => {
98
182
  const fixes = [];
99
- // Add import if not present
100
- if (!hasAssertSafeImport) {
183
+ if (!importClaimed && !importsAssertSafe(context.sourceCode.ast)) {
101
184
  fixes.push(addAssertSafeImport(fixer));
102
- hasAssertSafeImport = true;
185
+ importClaimed = true;
103
186
  }
104
187
  // Replace the node with assertSafe(argText)
105
188
  fixes.push(fixer.replaceText(node, `assertSafe(${argText})`));
@@ -119,6 +202,17 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
119
202
  if (isReportSuppressed(node)) {
120
203
  return null;
121
204
  }
205
+ // Resolve `assertSafe` through the scope chain at the fixed node. A
206
+ // binding that is not the helper import breaks the edit two ways: the
207
+ // injected import collides with a module-scope declaration (TS2440,
208
+ // or TS2300 when the binding is itself an import), and a shadowing
209
+ // parameter or block-scoped binding captures the emitted call with no
210
+ // compile error at all. Declining leaves the report standing so the
211
+ // author resolves the clash deliberately.
212
+ const existing = ASTHelpers_1.ASTHelpers.findVariableInScope(ASTHelpers_1.ASTHelpers.getScope(context, node), ASSERT_SAFE_NAME);
213
+ if (existing && !bindsAssertSafe(existing)) {
214
+ return null;
215
+ }
122
216
  return createFixes(fixer, node, expressionText);
123
217
  },
124
218
  });
@@ -146,13 +240,6 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
146
240
  });
147
241
  };
148
242
  return {
149
- ImportDeclaration(node) {
150
- // Check if assertSafe is already imported
151
- if (node.specifiers.some((specifier) => specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
152
- specifier.imported.name === 'assertSafe')) {
153
- hasAssertSafeImport = true;
154
- }
155
- },
156
243
  // Handle computed property in object destructuring
157
244
  Property(node) {
158
245
  if (node.computed && node.key) {
@@ -3,9 +3,45 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.enforceMemoizeAsync = 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_MODULE = '@blumintinc/typescript-memoize';
8
9
  const ALLOWED_MEMOIZE_MODULES = new Set([MEMOIZE_MODULE, 'typescript-memoize']);
10
+ const MEMOIZE_NAME = 'Memoize';
11
+ /**
12
+ * A named specifier that binds `Memoize` under its own name — the only shape
13
+ * that makes a bare `@Memoize()` decorator resolve to the decorator factory. An
14
+ * alias (`import { Memoize as Cache }`) leaves the name free for the injected
15
+ * import, and a type-only specifier erases at compile time, so neither backs a
16
+ * value reference.
17
+ */
18
+ function isMemoizeSpecifier(specifier) {
19
+ return (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
20
+ specifier.importKind !== 'type' &&
21
+ specifier.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
22
+ specifier.imported.name === MEMOIZE_NAME &&
23
+ specifier.local.name === MEMOIZE_NAME);
24
+ }
25
+ /**
26
+ * Whether every declaration of a visible `Memoize` binding is a value import of
27
+ * the decorator itself. A local const/function/class, an enclosing class of the
28
+ * same name, a parameter, a namespace or default import, or a named import from
29
+ * any other module all mean the emitted `@Memoize()` would resolve somewhere
30
+ * other than the decorator factory.
31
+ */
32
+ function bindsMemoize(variable) {
33
+ return (variable.defs.length > 0 &&
34
+ variable.defs.every((def) => {
35
+ const specifier = def.node;
36
+ if (!isMemoizeSpecifier(specifier)) {
37
+ return false;
38
+ }
39
+ const declaration = specifier.parent;
40
+ return (declaration?.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
41
+ declaration.importKind !== 'type' &&
42
+ ALLOWED_MEMOIZE_MODULES.has(String(declaration.source.value)));
43
+ }));
44
+ }
9
45
  /**
10
46
  * Matches a memoize decorator in supported syntaxes:
11
47
  * - @Alias()
@@ -51,9 +87,6 @@ exports.enforceMemoizeAsync = (0, createRule_1.createRule)({
51
87
  },
52
88
  defaultOptions: [],
53
89
  create(context) {
54
- let hasMemoizeImport = false;
55
- const memoizeAliases = new Map(); // alias -> source module
56
- const memoizeNamespaces = new Map(); // namespace -> source module
57
90
  let scheduledImportFix = false;
58
91
  /**
59
92
  * The `import { Memoize }` statement rides on a single violation's fix, so
@@ -62,23 +95,48 @@ exports.enforceMemoizeAsync = (0, createRule_1.createRule)({
62
95
  * `@Memoize()`, leaving a decorator with no import.
63
96
  */
64
97
  const isReportSuppressed = (0, disableDirectives_1.createSuppressionChecker)(context);
65
- return {
66
- ImportDeclaration(node) {
67
- if (ALLOWED_MEMOIZE_MODULES.has(String(node.source.value))) {
68
- node.specifiers.forEach((spec) => {
69
- if (spec.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
70
- spec.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
71
- spec.imported.name === 'Memoize') {
72
- hasMemoizeImport = true;
73
- memoizeAliases.set(spec.local.name, String(node.source.value));
74
- }
75
- else if (spec.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier) {
76
- hasMemoizeImport = true;
77
- memoizeNamespaces.set(spec.local.name, String(node.source.value));
78
- }
79
- });
98
+ /**
99
+ * Memoize bindings the file already imports, keyed by local name: aliases
100
+ * for `import { Memoize as X }` and namespaces for `import * as X`.
101
+ *
102
+ * Read off `Program.body` rather than accumulated by an `ImportDeclaration`
103
+ * visitor, because a class that precedes the import declaration in source
104
+ * order is visited — and fixed — before that visitor runs, and would be
105
+ * judged against state recorded for no import at all. The AST is fixed for
106
+ * the pass, so a single scan serves every violation.
107
+ */
108
+ const readMemoizeImports = () => {
109
+ const aliases = new Map();
110
+ const namespaces = new Map();
111
+ for (const statement of context.sourceCode.ast.body) {
112
+ if (statement.type !== utils_1.AST_NODE_TYPES.ImportDeclaration) {
113
+ continue;
80
114
  }
81
- },
115
+ const source = String(statement.source.value);
116
+ if (!ALLOWED_MEMOIZE_MODULES.has(source)) {
117
+ continue;
118
+ }
119
+ for (const spec of statement.specifiers) {
120
+ if (spec.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
121
+ spec.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
122
+ spec.imported.name === MEMOIZE_NAME) {
123
+ aliases.set(spec.local.name, source);
124
+ }
125
+ else if (spec.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier) {
126
+ namespaces.set(spec.local.name, source);
127
+ }
128
+ }
129
+ }
130
+ return { aliases, namespaces };
131
+ };
132
+ let memoizeImportCache = null;
133
+ const memoizeImports = () => {
134
+ if (!memoizeImportCache) {
135
+ memoizeImportCache = readMemoizeImports();
136
+ }
137
+ return memoizeImportCache;
138
+ };
139
+ return {
82
140
  MethodDefinition(node) {
83
141
  // Only process async instance methods (skip static methods)
84
142
  if (node.value.type !== utils_1.AST_NODE_TYPES.FunctionExpression ||
@@ -90,6 +148,8 @@ exports.enforceMemoizeAsync = (0, createRule_1.createRule)({
90
148
  if (node.value.params.length > 1) {
91
149
  return;
92
150
  }
151
+ const { aliases: memoizeAliases, namespaces: memoizeNamespaces } = memoizeImports();
152
+ const hasMemoizeImport = memoizeAliases.size > 0 || memoizeNamespaces.size > 0;
93
153
  // Check if method already has @Memoize or @Memoize() decorator
94
154
  const hasDecorator = node.decorators?.some((decorator) => {
95
155
  // If no named imports were found, we assume 'Memoize' is the intended name (for legacy/global support)
@@ -164,6 +224,24 @@ exports.enforceMemoizeAsync = (0, createRule_1.createRule)({
164
224
  }
165
225
  }
166
226
  const importStatement = `import { Memoize } from '${MEMOIZE_MODULE}';`;
227
+ // Resolve `Memoize` through the scope chain at the fixed node
228
+ // whenever the edit spells the decorator bare. A binding that is
229
+ // not a memoize import breaks the edit two ways: the injected
230
+ // import collides with a module-scope declaration (TS2440, or
231
+ // TS2300 when the binding is itself an import), and a shadowing
232
+ // parameter or block-scoped binding captures the emitted decorator
233
+ // with no compile error at all. Declining leaves the report
234
+ // standing so the author resolves the clash deliberately.
235
+ //
236
+ // An alias or namespace decorator (`@Cache()`, `@ns.Memoize()`)
237
+ // neither references the bare name nor injects the import, so it is
238
+ // unaffected by a `Memoize` binding and must not be declined.
239
+ if (decoratorIdent === MEMOIZE_NAME) {
240
+ const existing = ASTHelpers_1.ASTHelpers.findVariableInScope(ASTHelpers_1.ASTHelpers.getScope(context, node), MEMOIZE_NAME);
241
+ if (existing && !bindsMemoize(existing)) {
242
+ return null;
243
+ }
244
+ }
167
245
  // Add import if it's not already present; ensure we only add once per file
168
246
  if (!hasMemoizeImport &&
169
247
  memoizeNamespaces.size === 0 &&
@@ -3,12 +3,48 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.enforceMemoizeGetters = 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_NAME = 'Memoize';
14
+ /**
15
+ * A named specifier that binds `Memoize` under its own name — the only shape
16
+ * that makes a bare `@Memoize()` decorator resolve to the decorator factory. An
17
+ * alias (`import { Memoize as Cache }`) leaves the name free for the injected
18
+ * import, and a type-only specifier erases at compile time, so neither backs a
19
+ * value reference.
20
+ */
21
+ function isMemoizeSpecifier(specifier) {
22
+ return (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
23
+ specifier.importKind !== 'type' &&
24
+ specifier.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
25
+ specifier.imported.name === MEMOIZE_NAME &&
26
+ specifier.local.name === MEMOIZE_NAME);
27
+ }
28
+ /**
29
+ * Whether every declaration of a visible `Memoize` binding is a value import of
30
+ * the decorator itself. A local const/function/class, an enclosing class of the
31
+ * same name, a parameter, a namespace or default import, or a named import from
32
+ * any other module all mean the emitted `@Memoize()` would resolve somewhere
33
+ * other than the decorator factory.
34
+ */
35
+ function bindsMemoize(variable) {
36
+ return (variable.defs.length > 0 &&
37
+ variable.defs.every((def) => {
38
+ const specifier = def.node;
39
+ if (!isMemoizeSpecifier(specifier)) {
40
+ return false;
41
+ }
42
+ const declaration = specifier.parent;
43
+ return (declaration?.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
44
+ declaration.importKind !== 'type' &&
45
+ MEMOIZE_MODULES.has(String(declaration.source.value)));
46
+ }));
47
+ }
12
48
  function isMemoizeDecorator(decorator, alias) {
13
49
  const expression = decorator.expression;
14
50
  // @Alias()
@@ -54,10 +90,6 @@ exports.enforceMemoizeGetters = (0, createRule_1.createRule)({
54
90
  return {};
55
91
  }
56
92
  const sourceCode = context.getSourceCode();
57
- let hasMemoizeImport = false;
58
- let memoizeAlias = 'Memoize';
59
- let memoizeNamespace = null;
60
- let hasNamedImport = false;
61
93
  let scheduledImportFix = false;
62
94
  /**
63
95
  * The `import { Memoize }` statement rides on a single violation's fix, so
@@ -66,25 +98,59 @@ exports.enforceMemoizeGetters = (0, createRule_1.createRule)({
66
98
  * `@Memoize()`, leaving a decorator with no import.
67
99
  */
68
100
  const isReportSuppressed = (0, disableDirectives_1.createSuppressionChecker)(context);
69
- return {
70
- ImportDeclaration(node) {
71
- if (MEMOIZE_MODULES.has(String(node.source.value))) {
72
- for (const spec of node.specifiers) {
73
- if (spec.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
74
- spec.imported.name === 'Memoize') {
75
- hasMemoizeImport = true;
76
- hasNamedImport = true;
77
- memoizeAlias = spec.local.name;
78
- }
79
- else if (spec.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier) {
80
- hasMemoizeImport = true;
81
- if (!hasNamedImport) {
82
- memoizeNamespace = spec.local.name;
83
- }
101
+ /**
102
+ * The memoize import the file already carries: the local name of a named
103
+ * `Memoize` specifier, or the local name of a namespace import.
104
+ *
105
+ * Read off `Program.body` rather than accumulated by an `ImportDeclaration`
106
+ * visitor, because a class that precedes the import declaration in source
107
+ * order is visited — and fixed — before that visitor runs, and would be
108
+ * judged against state recorded for no import at all. The AST is fixed for
109
+ * the pass, so a single scan serves every violation.
110
+ */
111
+ const readMemoizeImports = () => {
112
+ let hasMemoizeImport = false;
113
+ let memoizeAlias = MEMOIZE_NAME;
114
+ let memoizeNamespace = null;
115
+ let hasNamedImport = false;
116
+ for (const statement of sourceCode.ast.body) {
117
+ if (statement.type !== utils_1.AST_NODE_TYPES.ImportDeclaration) {
118
+ continue;
119
+ }
120
+ if (!MEMOIZE_MODULES.has(String(statement.source.value))) {
121
+ continue;
122
+ }
123
+ for (const spec of statement.specifiers) {
124
+ if (spec.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
125
+ spec.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
126
+ spec.imported.name === MEMOIZE_NAME) {
127
+ hasMemoizeImport = true;
128
+ hasNamedImport = true;
129
+ memoizeAlias = spec.local.name;
130
+ }
131
+ else if (spec.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier) {
132
+ hasMemoizeImport = true;
133
+ if (!hasNamedImport) {
134
+ memoizeNamespace = spec.local.name;
84
135
  }
85
136
  }
86
137
  }
87
- },
138
+ }
139
+ return {
140
+ hasMemoizeImport,
141
+ memoizeAlias,
142
+ memoizeNamespace,
143
+ hasNamedImport,
144
+ };
145
+ };
146
+ let memoizeImportCache = null;
147
+ const memoizeImports = () => {
148
+ if (!memoizeImportCache) {
149
+ memoizeImportCache = readMemoizeImports();
150
+ }
151
+ return memoizeImportCache;
152
+ };
153
+ return {
88
154
  MethodDefinition(node) {
89
155
  // Target: instance private getters
90
156
  if (node.kind !== 'get')
@@ -95,7 +161,10 @@ exports.enforceMemoizeGetters = (0, createRule_1.createRule)({
95
161
  // enforce only "private" accessibility (undefined => public)
96
162
  if (node.accessibility !== 'private')
97
163
  return;
98
- const decoratorAliases = memoizeAlias === 'Memoize' ? ['Memoize'] : ['Memoize', memoizeAlias];
164
+ const { hasMemoizeImport, memoizeAlias, memoizeNamespace, hasNamedImport, } = memoizeImports();
165
+ const decoratorAliases = memoizeAlias === MEMOIZE_NAME
166
+ ? [MEMOIZE_NAME]
167
+ : [MEMOIZE_NAME, memoizeAlias];
99
168
  const hasDecorator = node.decorators?.some((decorator) => decoratorAliases.some((alias) => isMemoizeDecorator(decorator, alias)));
100
169
  if (hasDecorator)
101
170
  return;
@@ -127,6 +196,24 @@ exports.enforceMemoizeGetters = (0, createRule_1.createRule)({
127
196
  return memoizeAlias;
128
197
  };
129
198
  const decoratorIdent = getDecoratorIdent();
199
+ // Resolve `Memoize` through the scope chain at the fixed node
200
+ // whenever the edit spells the decorator bare. A binding that is
201
+ // not a memoize import breaks the edit two ways: the injected
202
+ // import collides with a module-scope declaration (TS2440, or
203
+ // TS2300 when the binding is itself an import), and a shadowing
204
+ // parameter or block-scoped binding captures the emitted decorator
205
+ // with no compile error at all. Declining leaves the report
206
+ // standing so the author resolves the clash deliberately.
207
+ //
208
+ // An alias or namespace decorator (`@Cache()`, `@ns.Memoize()`)
209
+ // neither references the bare name nor injects the import, so it is
210
+ // unaffected by a `Memoize` binding and must not be declined.
211
+ if (decoratorIdent === MEMOIZE_NAME) {
212
+ const existing = ASTHelpers_1.ASTHelpers.findVariableInScope(ASTHelpers_1.ASTHelpers.getScope(context, node), MEMOIZE_NAME);
213
+ if (existing && !bindsMemoize(existing)) {
214
+ return null;
215
+ }
216
+ }
130
217
  // Insert import if needed, at the top alongside other imports
131
218
  if (!hasMemoizeImport && !scheduledImportFix) {
132
219
  const programBody = sourceCode.ast.body;
@@ -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 {};