@blumintinc/eslint-plugin-blumint 1.20.19 → 1.20.21
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-querykey-ts.js +120 -15
- package/lib/rules/prefer-global-router-state-key.js +102 -6
- package/package.json +1 -1
- package/release-manifest.json +44 -0
package/lib/index.js
CHANGED
|
@@ -1,14 +1,62 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.enforceQueryKeyTs = void 0;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
4
8
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
9
|
const createRule_1 = require("../utils/createRule");
|
|
10
|
+
// The module's path below the project root doubles as the bare specifier,
|
|
11
|
+
// which is precisely why the root tsconfig `paths` and the Jest mapper resolve
|
|
12
|
+
// it.
|
|
13
|
+
const QUERY_KEYS_MODULE = 'src/util/routing/queryKeys';
|
|
14
|
+
const QUERY_KEYS_SUFFIX = 'util/routing/queryKeys';
|
|
15
|
+
const SRC_TIER_SEGMENT = '/src/';
|
|
6
16
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
17
|
+
* An `@/`-aliased specifier resolves under none of tsc, webpack or Jest, so it
|
|
18
|
+
* is never emitted; it stays recognized because a consumer that does declare
|
|
19
|
+
* the alias must still have its existing imports understood.
|
|
10
20
|
*/
|
|
11
|
-
const
|
|
21
|
+
const ALIASED_QUERY_KEYS_MODULE = '@/util/routing/queryKeys';
|
|
22
|
+
const toPosixPath = (filePath) => filePath.replace(/\\/g, '/');
|
|
23
|
+
const ensureRelativeSpecifier = (specifier) => specifier.startsWith('.') ? specifier : `./${specifier}`;
|
|
24
|
+
const isWindowsDrivePath = (filePath) => /^[A-Za-z]:[\\/]/.test(filePath);
|
|
25
|
+
const isValidRelativePath = (relativePath) => relativePath !== '' &&
|
|
26
|
+
!path_1.default.isAbsolute(relativePath) &&
|
|
27
|
+
!isWindowsDrivePath(relativePath);
|
|
28
|
+
const toAbsoluteFilename = (sourceFilePath, cwd) => toPosixPath(path_1.default.isAbsolute(sourceFilePath)
|
|
29
|
+
? sourceFilePath
|
|
30
|
+
: path_1.default.join(cwd, sourceFilePath));
|
|
31
|
+
/**
|
|
32
|
+
* `queryKeys.ts` lives at `src/util/routing/queryKeys.ts` and is reachable by
|
|
33
|
+
* exactly two forms: a relative path, and the bare `src/…` specifier that the
|
|
34
|
+
* root tsconfig `paths` and the Jest `moduleNameMapper` both resolve. A
|
|
35
|
+
* hardcoded `@/`-aliased specifier therefore turns every fix into a broken
|
|
36
|
+
* import (#1391).
|
|
37
|
+
*
|
|
38
|
+
* Files under a `src/` segment take the relative form, which dominates the
|
|
39
|
+
* codebase and stays correct even where `paths` are unavailable; the `../`
|
|
40
|
+
* count comes from the file's own depth below the root that owns its `src/`
|
|
41
|
+
* segment. Returns null when no correct specifier exists, which makes the
|
|
42
|
+
* caller decline the fix rather than write an import that cannot resolve.
|
|
43
|
+
*/
|
|
44
|
+
function buildQueryKeysSpecifier(sourceFilePath, cwd) {
|
|
45
|
+
const absoluteFilename = toAbsoluteFilename(sourceFilePath, cwd);
|
|
46
|
+
const tierIndex = absoluteFilename.indexOf(SRC_TIER_SEGMENT);
|
|
47
|
+
if (tierIndex === -1) {
|
|
48
|
+
return QUERY_KEYS_MODULE;
|
|
49
|
+
}
|
|
50
|
+
// The project root is everything up to and including the separator that
|
|
51
|
+
// precedes the file's own `src/` segment.
|
|
52
|
+
const projectRoot = absoluteFilename.slice(0, tierIndex + 1);
|
|
53
|
+
const targetPath = path_1.default.join(projectRoot, QUERY_KEYS_MODULE);
|
|
54
|
+
const relativePath = path_1.default.relative(path_1.default.dirname(absoluteFilename), targetPath);
|
|
55
|
+
if (!isValidRelativePath(relativePath)) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return ensureRelativeSpecifier(toPosixPath(relativePath));
|
|
59
|
+
}
|
|
12
60
|
/**
|
|
13
61
|
* Rule to enforce the use of centralized router state key constants imported from
|
|
14
62
|
* `src/util/routing/queryKeys.ts` instead of arbitrary string literals when calling
|
|
@@ -25,18 +73,21 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
|
|
|
25
73
|
fixable: 'code',
|
|
26
74
|
schema: [],
|
|
27
75
|
messages: {
|
|
28
|
-
enforceQueryKeyImport: 'Router state key must come from queryKeys.ts (e.g., "
|
|
76
|
+
enforceQueryKeyImport: 'Router state key must come from queryKeys.ts (e.g., "src/util/routing/queryKeys" or a relative path to that module). Use a QUERY_KEY_* constant instead of string literals.',
|
|
29
77
|
enforceQueryKeyConstant: 'Router state key must use a QUERY_KEY_* constant from queryKeys.ts. Variable "{{variableName}}" is not imported from the correct source.',
|
|
30
78
|
},
|
|
31
79
|
},
|
|
32
80
|
defaultOptions: [],
|
|
33
81
|
create(context) {
|
|
82
|
+
const cwd = typeof context.getCwd === 'function' ? context.getCwd() : process.cwd();
|
|
83
|
+
const absoluteFilename = toAbsoluteFilename(context.getFilename(), cwd);
|
|
84
|
+
const queryKeysSpecifier = buildQueryKeysSpecifier(context.getFilename(), cwd);
|
|
34
85
|
// Track imports from queryKeys.ts
|
|
35
86
|
const queryKeyImports = new Map();
|
|
36
87
|
const localUseRouterStateNames = new Set(['useRouterState']);
|
|
37
88
|
const validQueryKeySources = new Set([
|
|
38
|
-
|
|
39
|
-
|
|
89
|
+
ALIASED_QUERY_KEYS_MODULE,
|
|
90
|
+
QUERY_KEYS_MODULE,
|
|
40
91
|
]);
|
|
41
92
|
const allowedQueryKeyFactories = new Set(['makeQueryKey', 'getQueryKey']);
|
|
42
93
|
const sourceCode = context.getSourceCode();
|
|
@@ -60,8 +111,21 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
|
|
|
60
111
|
* Check if a source path refers to queryKeys.ts
|
|
61
112
|
*/
|
|
62
113
|
function isQueryKeysSource(source) {
|
|
63
|
-
|
|
64
|
-
source.endsWith(
|
|
114
|
+
if (validQueryKeySources.has(source) ||
|
|
115
|
+
source.endsWith(`/${QUERY_KEYS_SUFFIX}`)) {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
// A relative specifier can name the module without spelling out
|
|
119
|
+
// `util/routing`: a sibling reaches it as `./queryKeys`, and a file two
|
|
120
|
+
// directories below `src/util` as `../../routing/queryKeys`. Resolving
|
|
121
|
+
// against the linted file recognizes those, which also keeps the fix's own
|
|
122
|
+
// relative output recognized on the next pass so a second violation
|
|
123
|
+
// extends that import instead of duplicating it.
|
|
124
|
+
if (!source.startsWith('.')) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
const resolved = toPosixPath(path_1.default.resolve(path_1.default.dirname(absoluteFilename), source));
|
|
128
|
+
return resolved.endsWith(`/${QUERY_KEYS_SUFFIX}`);
|
|
65
129
|
}
|
|
66
130
|
function importDeclarationsOf() {
|
|
67
131
|
return sourceCode.ast.body.filter((statement) => statement.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
|
|
@@ -120,13 +184,41 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
|
|
|
120
184
|
}
|
|
121
185
|
return 'bound';
|
|
122
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* A parameter binding holds a different value on every call, so no single
|
|
189
|
+
* `QUERY_KEY_*` constant can stand in for it: a hook that iterates a
|
|
190
|
+
* constant array of keys hands each one to a callback parameter by design
|
|
191
|
+
* (#1393). Reporting such an identifier demands a substitution that does not
|
|
192
|
+
* exist, and the enclosing function is where the caller — not this file —
|
|
193
|
+
* decides which key is passed.
|
|
194
|
+
*/
|
|
195
|
+
function isParameterBinding(identifier) {
|
|
196
|
+
const variable = utils_1.ASTUtils.findVariable(scopeOf(identifier), identifier);
|
|
197
|
+
const definition = variable?.defs[0];
|
|
198
|
+
return definition?.type === utils_1.TSESLint.Scope.DefinitionType.Parameter;
|
|
199
|
+
}
|
|
200
|
+
function queryKeysDeclarationsOf() {
|
|
201
|
+
return importDeclarationsOf().filter((declaration) => isQueryKeysSource(String(declaration.source.value)));
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* The path by which this file reaches queryKeys.ts. An existing declaration
|
|
205
|
+
* is proof of a path that resolves here — including an `@/` one, in a
|
|
206
|
+
* consumer that declares that alias — so it wins over anything derived.
|
|
207
|
+
* Null means the module is unreachable by any specifier this rule can write.
|
|
208
|
+
*/
|
|
209
|
+
function importSourceOf() {
|
|
210
|
+
const [declaration] = queryKeysDeclarationsOf();
|
|
211
|
+
return declaration
|
|
212
|
+
? String(declaration.source.value)
|
|
213
|
+
: queryKeysSpecifier;
|
|
214
|
+
}
|
|
123
215
|
/**
|
|
124
216
|
* Make the substituted constants resolve: extend the file's queryKeys import
|
|
125
217
|
* when there is one to extend, otherwise add a fresh import statement.
|
|
126
218
|
*/
|
|
127
219
|
function buildImportFix(fixer, constants) {
|
|
128
220
|
const importDeclarations = importDeclarationsOf();
|
|
129
|
-
const queryKeysDeclarations =
|
|
221
|
+
const queryKeysDeclarations = queryKeysDeclarationsOf();
|
|
130
222
|
const reusable = queryKeysDeclarations.find((declaration) => declaration.importKind !== 'type' &&
|
|
131
223
|
declaration.specifiers.some(isValueImportSpecifier));
|
|
132
224
|
if (reusable) {
|
|
@@ -136,9 +228,10 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
|
|
|
136
228
|
}
|
|
137
229
|
// A namespace or type-only queryKeys import cannot take named value
|
|
138
230
|
// specifiers, but its path is proof of how this file reaches the module.
|
|
139
|
-
const source =
|
|
140
|
-
|
|
141
|
-
|
|
231
|
+
const source = importSourceOf();
|
|
232
|
+
if (source === null) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
142
235
|
const importText = `import { ${constants.join(', ')} } from '${source}';\n`;
|
|
143
236
|
const [firstImport] = importDeclarations;
|
|
144
237
|
if (firstImport) {
|
|
@@ -152,6 +245,7 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
|
|
|
152
245
|
function flushReports() {
|
|
153
246
|
const resolutions = new Map();
|
|
154
247
|
const missingConstants = [];
|
|
248
|
+
const canImport = importSourceOf() !== null;
|
|
155
249
|
for (const report of pendingReports) {
|
|
156
250
|
if (!report.substitution) {
|
|
157
251
|
continue;
|
|
@@ -159,6 +253,12 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
|
|
|
159
253
|
const { constant, scope } = report.substitution;
|
|
160
254
|
const name = localNameOf(constant);
|
|
161
255
|
const state = resolveBinding(scope, name);
|
|
256
|
+
// Substituting a constant whose import cannot be written would leave
|
|
257
|
+
// the file referencing an undefined identifier, which is worse than the
|
|
258
|
+
// literal it replaced; leaving the report unresolved declines its fix.
|
|
259
|
+
if (state === 'missing' && !canImport) {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
162
262
|
resolutions.set(report, { name, state });
|
|
163
263
|
if (state === 'missing' && !missingConstants.includes(name)) {
|
|
164
264
|
missingConstants.push(name);
|
|
@@ -188,7 +288,11 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
|
|
|
188
288
|
fixer.replaceText(substitution.keyNode, resolution.name),
|
|
189
289
|
];
|
|
190
290
|
if (report === importCarrier && missingConstants.length > 0) {
|
|
191
|
-
|
|
291
|
+
const importFix = buildImportFix(fixer, missingConstants);
|
|
292
|
+
if (!importFix) {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
fixes.unshift(importFix);
|
|
192
296
|
}
|
|
193
297
|
return fixes;
|
|
194
298
|
},
|
|
@@ -403,7 +507,8 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
|
|
|
403
507
|
: undefined,
|
|
404
508
|
});
|
|
405
509
|
}
|
|
406
|
-
else if (keyValue.type === utils_1.AST_NODE_TYPES.Identifier
|
|
510
|
+
else if (keyValue.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
511
|
+
!isParameterBinding(keyValue)) {
|
|
407
512
|
// Report variables that aren't from the correct source
|
|
408
513
|
pendingReports.push({
|
|
409
514
|
node: keyValue,
|
|
@@ -1,8 +1,56 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.preferGlobalRouterStateKey = void 0;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
4
8
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
9
|
const createRule_1 = require("../utils/createRule");
|
|
10
|
+
// The module's path below the project root doubles as the bare specifier,
|
|
11
|
+
// which is precisely why the root tsconfig `paths` and the Jest mapper resolve
|
|
12
|
+
// it.
|
|
13
|
+
const QUERY_KEYS_MODULE = 'src/util/routing/queryKeys';
|
|
14
|
+
const QUERY_KEYS_SUFFIX = 'util/routing/queryKeys';
|
|
15
|
+
const SRC_TIER_SEGMENT = '/src/';
|
|
16
|
+
const toPosixPath = (filePath) => filePath.replace(/\\/g, '/');
|
|
17
|
+
const ensureRelativeSpecifier = (specifier) => specifier.startsWith('.') ? specifier : `./${specifier}`;
|
|
18
|
+
const isWindowsDrivePath = (filePath) => /^[A-Za-z]:[\\/]/.test(filePath);
|
|
19
|
+
const isValidRelativePath = (relativePath) => relativePath !== '' &&
|
|
20
|
+
!path_1.default.isAbsolute(relativePath) &&
|
|
21
|
+
!isWindowsDrivePath(relativePath);
|
|
22
|
+
const toAbsoluteFilename = (sourceFilePath, cwd) => toPosixPath(path_1.default.isAbsolute(sourceFilePath)
|
|
23
|
+
? sourceFilePath
|
|
24
|
+
: path_1.default.join(cwd, sourceFilePath));
|
|
25
|
+
/**
|
|
26
|
+
* `queryKeys.ts` lives at `src/util/routing/queryKeys.ts` and is reachable by
|
|
27
|
+
* exactly two forms: a relative path, and the bare `src/…` specifier that the
|
|
28
|
+
* root tsconfig `paths` and the Jest `moduleNameMapper` both resolve. An
|
|
29
|
+
* `@/`-aliased specifier resolves under none of tsc, webpack or Jest, so a
|
|
30
|
+
* hardcoded one turns every fix into a broken import (#1390).
|
|
31
|
+
*
|
|
32
|
+
* Files under a `src/` segment take the relative form, which dominates the
|
|
33
|
+
* codebase and stays correct even where `paths` are unavailable; the `../`
|
|
34
|
+
* count comes from the file's own depth below the root that owns its `src/`
|
|
35
|
+
* segment. Returns null when no correct specifier exists, which makes the
|
|
36
|
+
* caller decline the fix rather than write an import that cannot resolve.
|
|
37
|
+
*/
|
|
38
|
+
function buildQueryKeysSpecifier(sourceFilePath, cwd) {
|
|
39
|
+
const absoluteFilename = toAbsoluteFilename(sourceFilePath, cwd);
|
|
40
|
+
const tierIndex = absoluteFilename.indexOf(SRC_TIER_SEGMENT);
|
|
41
|
+
if (tierIndex === -1) {
|
|
42
|
+
return QUERY_KEYS_MODULE;
|
|
43
|
+
}
|
|
44
|
+
// The project root is everything up to and including the separator that
|
|
45
|
+
// precedes the file's own `src/` segment.
|
|
46
|
+
const projectRoot = absoluteFilename.slice(0, tierIndex + 1);
|
|
47
|
+
const targetPath = path_1.default.join(projectRoot, QUERY_KEYS_MODULE);
|
|
48
|
+
const relativePath = path_1.default.relative(path_1.default.dirname(absoluteFilename), targetPath);
|
|
49
|
+
if (!isValidRelativePath(relativePath)) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
return ensureRelativeSpecifier(toPosixPath(relativePath));
|
|
53
|
+
}
|
|
6
54
|
/**
|
|
7
55
|
* Rule to enforce the use of centralized router state key constants imported from
|
|
8
56
|
* `src/util/routing/queryKeys.ts` instead of arbitrary string literals when calling
|
|
@@ -19,13 +67,16 @@ exports.preferGlobalRouterStateKey = (0, createRule_1.createRule)({
|
|
|
19
67
|
fixable: 'code',
|
|
20
68
|
schema: [],
|
|
21
69
|
messages: {
|
|
22
|
-
preferGlobalRouterStateKey: 'Router state key {{keyValue}} is a string literal. String literals bypass the shared queryKeys.ts QUERY_KEY_* constants, which leads to duplicate router cache entries and makes allowed keys hard to discover. Import the corresponding QUERY_KEY_* constant from "
|
|
23
|
-
invalidQueryKeySource: 'Router state key variable "{{variableName}}" is not sourced from queryKeys.ts. useRouterState keys must come from QUERY_KEY_* exports so routing cache keys stay stable and traceable. Import the matching constant from "
|
|
70
|
+
preferGlobalRouterStateKey: 'Router state key {{keyValue}} is a string literal. String literals bypass the shared queryKeys.ts QUERY_KEY_* constants, which leads to duplicate router cache entries and makes allowed keys hard to discover. Import the corresponding QUERY_KEY_* constant from "src/util/routing/queryKeys" (a relative path to that module, or an approved re-export) and pass that to useRouterState instead.',
|
|
71
|
+
invalidQueryKeySource: 'Router state key variable "{{variableName}}" is not sourced from queryKeys.ts. useRouterState keys must come from QUERY_KEY_* exports so routing cache keys stay stable and traceable. Import the matching constant from "src/util/routing/queryKeys" (a relative path to that module, or an approved re-export) and use that value here instead of {{variableName}}.',
|
|
24
72
|
},
|
|
25
73
|
},
|
|
26
74
|
defaultOptions: [],
|
|
27
75
|
create(context) {
|
|
28
76
|
const sourceCode = context.sourceCode;
|
|
77
|
+
const cwd = typeof context.getCwd === 'function' ? context.getCwd() : process.cwd();
|
|
78
|
+
const absoluteFilename = toAbsoluteFilename(context.getFilename(), cwd);
|
|
79
|
+
const queryKeysSpecifier = buildQueryKeysSpecifier(context.getFilename(), cwd);
|
|
29
80
|
// Prevent duplicate import insertions when multiple fixes target the same query key constant.
|
|
30
81
|
const scheduledQueryKeyNamedImports = new Set();
|
|
31
82
|
// Track imports from queryKeys.ts
|
|
@@ -48,8 +99,21 @@ exports.preferGlobalRouterStateKey = (0, createRule_1.createRule)({
|
|
|
48
99
|
const normalized = source
|
|
49
100
|
.replace(/^@\/|^src\//, '')
|
|
50
101
|
.replace(/^(\.\/|\.\.\/)+/, '');
|
|
51
|
-
|
|
52
|
-
normalized.endsWith(
|
|
102
|
+
if (validQueryKeySources.has(normalized) ||
|
|
103
|
+
normalized.endsWith(QUERY_KEYS_SUFFIX)) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
// A relative specifier can name the module without spelling out
|
|
107
|
+
// `util/routing`: a sibling reaches it as `./queryKeys`, and a file two
|
|
108
|
+
// directories below `src/util` as `../../routing/queryKeys`. Resolving
|
|
109
|
+
// against the linted file recognizes those, which also keeps the fix's own
|
|
110
|
+
// relative output recognized on the next pass so a second violation
|
|
111
|
+
// extends that import instead of duplicating it.
|
|
112
|
+
if (!source.startsWith('.')) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
const resolved = toPosixPath(path_1.default.resolve(path_1.default.dirname(absoluteFilename), source));
|
|
116
|
+
return resolved.endsWith(`/${QUERY_KEYS_SUFFIX}`);
|
|
53
117
|
}
|
|
54
118
|
/**
|
|
55
119
|
* Check if an identifier is a valid QUERY_KEY constant
|
|
@@ -57,6 +121,28 @@ exports.preferGlobalRouterStateKey = (0, createRule_1.createRule)({
|
|
|
57
121
|
function isValidQueryKeyConstant(name) {
|
|
58
122
|
return name.startsWith('QUERY_KEY_');
|
|
59
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* `SourceCode#getScope` supersedes the deprecated `context.getScope`; the
|
|
126
|
+
* fallback keeps the rule working on ESLint versions that predate it.
|
|
127
|
+
*/
|
|
128
|
+
function scopeOf(node) {
|
|
129
|
+
const scoped = sourceCode;
|
|
130
|
+
return typeof scoped.getScope === 'function'
|
|
131
|
+
? scoped.getScope(node)
|
|
132
|
+
: context.getScope();
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* A parameter binding holds a different value on every call, so no single
|
|
136
|
+
* `QUERY_KEY_*` constant can stand in for it: a hook that iterates a
|
|
137
|
+
* constant array of keys hands each one to a callback parameter by design
|
|
138
|
+
* (#1394). Reporting such an identifier demands a substitution that does not
|
|
139
|
+
* exist, and the caller — not this file — decides which key is passed.
|
|
140
|
+
*/
|
|
141
|
+
function isParameterBinding(identifier) {
|
|
142
|
+
const variable = utils_1.ASTUtils.findVariable(scopeOf(identifier), identifier);
|
|
143
|
+
const definition = variable?.defs[0];
|
|
144
|
+
return definition?.type === utils_1.TSESLint.Scope.DefinitionType.Parameter;
|
|
145
|
+
}
|
|
60
146
|
/**
|
|
61
147
|
* Check if a node represents a valid query key usage
|
|
62
148
|
*/
|
|
@@ -284,7 +370,9 @@ exports.preferGlobalRouterStateKey = (0, createRule_1.createRule)({
|
|
|
284
370
|
if (scheduledQueryKeyNamedImports.has(suggestedConstant)) {
|
|
285
371
|
return fixes;
|
|
286
372
|
}
|
|
287
|
-
const importText =
|
|
373
|
+
const importText = queryKeysSpecifier === null
|
|
374
|
+
? null
|
|
375
|
+
: `import { ${suggestedConstant} } from '${queryKeysSpecifier}';\n`;
|
|
288
376
|
const queryKeysNamedImport = sourceCode.ast.body.find((n) => n.type ===
|
|
289
377
|
utils_1.AST_NODE_TYPES.ImportDeclaration &&
|
|
290
378
|
n.importKind !== 'type' &&
|
|
@@ -304,6 +392,13 @@ exports.preferGlobalRouterStateKey = (0, createRule_1.createRule)({
|
|
|
304
392
|
const lastSpecifier = importSpecifiers[importSpecifiers.length - 1];
|
|
305
393
|
fixes.push(fixer.insertTextAfter(lastSpecifier, `, ${suggestedConstant}`));
|
|
306
394
|
}
|
|
395
|
+
else if (importText === null) {
|
|
396
|
+
// Extending an existing named import needs no
|
|
397
|
+
// specifier of its own, so only a freshly
|
|
398
|
+
// written import statement depends on one being
|
|
399
|
+
// derivable.
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
307
402
|
else if (sideEffectImport) {
|
|
308
403
|
fixes.push(fixer.replaceText(sideEffectImport, importText.trimEnd()));
|
|
309
404
|
}
|
|
@@ -335,7 +430,8 @@ exports.preferGlobalRouterStateKey = (0, createRule_1.createRule)({
|
|
|
335
430
|
},
|
|
336
431
|
});
|
|
337
432
|
}
|
|
338
|
-
else if (keyValue.type === utils_1.AST_NODE_TYPES.Identifier
|
|
433
|
+
else if (keyValue.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
434
|
+
!isParameterBinding(keyValue)) {
|
|
339
435
|
context.report({
|
|
340
436
|
node: keyValue,
|
|
341
437
|
messageId: 'invalidQueryKeySource',
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,48 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.21",
|
|
4
|
+
"date": "2026-07-29T23:50:58.620Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "enforce-querykey-ts",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1393
|
|
11
|
+
],
|
|
12
|
+
"summary": "exempt parameter bindings from the constant requirement (closes #1393)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "prefer-global-router-state-key",
|
|
16
|
+
"changeType": "fix",
|
|
17
|
+
"issues": [
|
|
18
|
+
1394
|
|
19
|
+
],
|
|
20
|
+
"summary": "exempt parameter bindings from the constant requirement (closes #1394)"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"version": "1.20.20",
|
|
26
|
+
"date": "2026-07-29T19:25:11.467Z",
|
|
27
|
+
"rules": [
|
|
28
|
+
{
|
|
29
|
+
"name": "enforce-querykey-ts",
|
|
30
|
+
"changeType": "fix",
|
|
31
|
+
"issues": [
|
|
32
|
+
1391
|
|
33
|
+
],
|
|
34
|
+
"summary": "derive the inserted queryKeys import specifier (closes #1391)"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "prefer-global-router-state-key",
|
|
38
|
+
"changeType": "fix",
|
|
39
|
+
"issues": [
|
|
40
|
+
1390
|
|
41
|
+
],
|
|
42
|
+
"summary": "derive queryKeys import specifier from file path (closes #1390)"
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
},
|
|
2
46
|
{
|
|
3
47
|
"version": "1.20.19",
|
|
4
48
|
"date": "2026-07-29T18:36:35.802Z",
|