@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 +1 -1
- package/lib/rules/enforce-assert-safe-object-key.js +112 -25
- package/lib/rules/enforce-memoize-async.js +97 -19
- package/lib/rules/enforce-memoize-getters.js +108 -21
- package/lib/rules/enforce-microdiff.d.ts +2 -1
- package/lib/rules/enforce-microdiff.js +147 -23
- package/lib/rules/enforce-stable-hash-spread-props.js +40 -1
- package/lib/rules/fast-deep-equal-over-microdiff.js +37 -0
- package/lib/rules/no-array-length-in-deps.js +65 -9
- package/lib/rules/prefer-fragment-component.js +127 -73
- package/lib/rules/prefer-global-router-state-key.js +57 -0
- package/lib/rules/prefer-next-dynamic.js +37 -2
- package/lib/rules/prefer-use-deep-compare-memo.js +62 -6
- package/lib/rules/require-dynamic-firebase-imports.d.ts +2 -2
- package/lib/rules/require-dynamic-firebase-imports.js +95 -7
- package/lib/rules/require-memo.js +101 -26
- package/lib/rules/require-memoize-jsx-returners.js +87 -9
- package/lib/rules/use-latest-callback.js +86 -39
- package/package.json +1 -1
- package/release-manifest.json +132 -0
package/lib/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
if (!hasAssertSafeImport) {
|
|
183
|
+
if (!importClaimed && !importsAssertSafe(context.sourceCode.ast)) {
|
|
101
184
|
fixes.push(addAssertSafeImport(fixer));
|
|
102
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
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:
|
|
3
|
+
export declare const enforceMicrodiff: TSESLint.RuleModule<MessageIds, [], TSESLint.RuleListener>;
|
|
3
4
|
export {};
|