@blumintinc/eslint-plugin-blumint 1.20.129 → 1.20.130
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-dynamic-file-naming.js +71 -16
- package/lib/rules/enforce-global-constants.d.ts +1 -1
- package/lib/rules/enforce-global-constants.js +113 -3
- package/lib/rules/enforce-querykey-ts.js +74 -3
- package/package.json +1 -1
- package/release-manifest.json +31 -0
package/lib/index.js
CHANGED
|
@@ -5,19 +5,82 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.DYNAMIC_RULES_LABEL = exports.REQUIRE_DYNAMIC_FIREBASE_IMPORTS_RULE = exports.RULE_NAME = void 0;
|
|
7
7
|
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
8
9
|
const createRule_1 = require("../utils/createRule");
|
|
9
10
|
exports.RULE_NAME = 'enforce-dynamic-file-naming';
|
|
10
11
|
const ENFORCE_DYNAMIC_IMPORTS_RULE = '@blumintinc/blumint/enforce-dynamic-imports';
|
|
11
12
|
exports.REQUIRE_DYNAMIC_FIREBASE_IMPORTS_RULE = '@blumintinc/blumint/require-dynamic-firebase-imports';
|
|
12
13
|
exports.DYNAMIC_RULES_LABEL = `${ENFORCE_DYNAMIC_IMPORTS_RULE} or ${exports.REQUIRE_DYNAMIC_FIREBASE_IMPORTS_RULE}`;
|
|
13
|
-
const SHORTHAND_DISABLE_NEXT_LINE = /\bednl\b/;
|
|
14
|
-
const SHORTHAND_DISABLE_LINE = /\bedl\b/;
|
|
15
14
|
const DISABLE_NEXT_LINE_TOKEN = 'eslint-disable-next-line';
|
|
16
15
|
const DISABLE_LINE_TOKEN = 'eslint-disable-line';
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
const DISABLE_TOKEN = 'eslint-disable';
|
|
17
|
+
/**
|
|
18
|
+
* ESLint splits a directive comment on ` -- ` and treats the tail as a human
|
|
19
|
+
* justification, never as part of the rule list. A rule named only in the tail
|
|
20
|
+
* is prose, so it must not read as a bypass.
|
|
21
|
+
*/
|
|
22
|
+
const JUSTIFICATION_SEPARATOR = /\s-{2,}\s/;
|
|
23
|
+
/**
|
|
24
|
+
* Mirrors ESLint's own `directivesPattern`: the directive has to be the FIRST
|
|
25
|
+
* token of the comment, followed by whitespace or the end of the comment.
|
|
26
|
+
* `// see eslint-disable-next-line <rule>` is prose to ESLint and suppresses
|
|
27
|
+
* nothing, so it cannot count as an acknowledged exception here either.
|
|
28
|
+
*
|
|
29
|
+
* The alternatives are ordered longest-first because `eslint-disable` would
|
|
30
|
+
* otherwise shadow the two suffixed spellings.
|
|
31
|
+
*/
|
|
32
|
+
const DIRECTIVE_PATTERN = new RegExp(`^(${DISABLE_NEXT_LINE_TOKEN}|${DISABLE_LINE_TOKEN}|${DISABLE_TOKEN})(?:\\s|$)`);
|
|
33
|
+
/**
|
|
34
|
+
* The only two directives ESLint honors inside a `//` comment. A bare
|
|
35
|
+
* `// eslint-disable <rule>` is inert — ESLint parses `eslint-disable` only out
|
|
36
|
+
* of a block comment — so blessing it would hand a reviewer a documented
|
|
37
|
+
* exception while the sibling rule keeps failing CI.
|
|
38
|
+
*/
|
|
39
|
+
const LINE_COMMENT_DIRECTIVES = new Set([
|
|
40
|
+
DISABLE_NEXT_LINE_TOKEN,
|
|
41
|
+
DISABLE_LINE_TOKEN,
|
|
42
|
+
]);
|
|
43
|
+
/**
|
|
44
|
+
* The rule names a comment actually disables, or `null` when ESLint would not
|
|
45
|
+
* read the comment as a disable directive at all.
|
|
46
|
+
*/
|
|
47
|
+
const disabledRuleNamesFrom = (comment) => {
|
|
48
|
+
const justification = JUSTIFICATION_SEPARATOR.exec(comment.value);
|
|
49
|
+
const directivePart = (justification ? comment.value.slice(0, justification.index) : comment.value).trim();
|
|
50
|
+
const match = DIRECTIVE_PATTERN.exec(directivePart);
|
|
51
|
+
if (!match) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const directiveText = match[1];
|
|
55
|
+
if (comment.type !== utils_1.AST_TOKEN_TYPES.Block &&
|
|
56
|
+
!LINE_COMMENT_DIRECTIVES.has(directiveText)) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
// ESLint rejects a multi-line `eslint-disable-line` outright and reports the
|
|
60
|
+
// comment as a problem instead of applying it.
|
|
61
|
+
if (directiveText === DISABLE_LINE_TOKEN &&
|
|
62
|
+
comment.loc.start.line !== comment.loc.end.line) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
return directivePart
|
|
66
|
+
.slice(directiveText.length)
|
|
67
|
+
.split(',')
|
|
68
|
+
.map((ruleName) => ruleName.trim().replace(/^(['"])(.*)\1$/s, '$2'))
|
|
69
|
+
.filter((ruleName) => ruleName.length > 0);
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* The rule has to be named explicitly. An unnamed blanket disable is not a
|
|
73
|
+
* counter-example: it silences this rule too, so no report is observable on
|
|
74
|
+
* such a file either way.
|
|
75
|
+
*/
|
|
76
|
+
const disabledRuleNameFrom = (comment) => {
|
|
77
|
+
const disabledRuleNames = disabledRuleNamesFrom(comment);
|
|
78
|
+
if (disabledRuleNames === null) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const disabled = new Set(disabledRuleNames);
|
|
82
|
+
const mentionsEnforceDynamicImports = disabled.has(ENFORCE_DYNAMIC_IMPORTS_RULE);
|
|
83
|
+
const mentionsRequireDynamicFirebaseImports = disabled.has(exports.REQUIRE_DYNAMIC_FIREBASE_IMPORTS_RULE);
|
|
21
84
|
if (mentionsEnforceDynamicImports && mentionsRequireDynamicFirebaseImports) {
|
|
22
85
|
return exports.DYNAMIC_RULES_LABEL;
|
|
23
86
|
}
|
|
@@ -59,16 +122,8 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
59
122
|
const sourceCode = context.getSourceCode();
|
|
60
123
|
const comments = sourceCode.getAllComments();
|
|
61
124
|
for (const comment of comments) {
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
const disablesTargetRule = disabledRuleNameForComment !== null;
|
|
65
|
-
const inlineDisable = (commentText.includes(DISABLE_NEXT_LINE_TOKEN) ||
|
|
66
|
-
commentText.includes(DISABLE_LINE_TOKEN) ||
|
|
67
|
-
SHORTHAND_DISABLE_NEXT_LINE.test(commentText) ||
|
|
68
|
-
SHORTHAND_DISABLE_LINE.test(commentText)) &&
|
|
69
|
-
disablesTargetRule;
|
|
70
|
-
const blockDisable = DISABLE_BLOCK_PATTERN.test(commentText) && disablesTargetRule;
|
|
71
|
-
if (inlineDisable || blockDisable) {
|
|
125
|
+
const disabledRuleNameForComment = disabledRuleNameFrom(comment);
|
|
126
|
+
if (disabledRuleNameForComment !== null) {
|
|
72
127
|
foundDisableDirective = true;
|
|
73
128
|
disabledRuleName = disabledRuleNameForComment;
|
|
74
129
|
break;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
-
type MessageIds = 'useGlobalConstant' | 'extractDefaultToGlobalConstant';
|
|
2
|
+
type MessageIds = 'useGlobalConstant' | 'extractDefaultToGlobalConstant' | 'declareMemoDependency';
|
|
3
3
|
export declare const enforceGlobalConstants: TSESLint.RuleModule<MessageIds, [], TSESLint.RuleListener>;
|
|
4
4
|
export {};
|
|
@@ -5,6 +5,86 @@ const utils_1 = require("@typescript-eslint/utils");
|
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
6
|
const shebang_1 = require("../utils/shebang");
|
|
7
7
|
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
8
|
+
/**
|
|
9
|
+
* Scope kinds whose bindings are established once per module evaluation:
|
|
10
|
+
* globals, imports and module-level declarations. A literal reading one of those
|
|
11
|
+
* can still be hoisted verbatim, because the name it reads is in scope at module
|
|
12
|
+
* level too.
|
|
13
|
+
*/
|
|
14
|
+
const MODULE_LEVEL_SCOPE_TYPES = new Set(['global', 'module']);
|
|
15
|
+
/**
|
|
16
|
+
* True when `inner` lies entirely inside `outer`'s source range.
|
|
17
|
+
*/
|
|
18
|
+
function isRangeWithin(inner, outer) {
|
|
19
|
+
return inner[0] >= outer[0] && inner[1] <= outer[1];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* True when a reference appears purely in type position (an annotation, or the
|
|
23
|
+
* target of an `as`/`satisfies`). Types erase at compile time, so such a name
|
|
24
|
+
* neither blocks hoisting nor belongs in a dependency array. The flags are read
|
|
25
|
+
* defensively: an analyzer that omits them leaves the reference classified as a
|
|
26
|
+
* value, which keeps the conservative answer.
|
|
27
|
+
*/
|
|
28
|
+
function isTypeOnlyReference(reference) {
|
|
29
|
+
const flags = reference;
|
|
30
|
+
return flags.isTypeReference === true && flags.isValueReference === false;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* True when a reference names a value that can differ between renders, i.e. one
|
|
34
|
+
* bound INSIDE the module and OUTSIDE the memo callback: a prop, a local, a
|
|
35
|
+
* destructured value, another hook's result.
|
|
36
|
+
*
|
|
37
|
+
* Everything else leaves hoisting available. An unresolved name is an ambient
|
|
38
|
+
* global. A module- or global-scoped binding is fixed for the module's lifetime
|
|
39
|
+
* and is equally visible from module scope. A binding whose own scope sits
|
|
40
|
+
* inside the callback — the callback's parameters, its locals, a nested
|
|
41
|
+
* function's locals — is created by the callback rather than closed over.
|
|
42
|
+
*/
|
|
43
|
+
function isRenderScopeReference(reference, callbackRange) {
|
|
44
|
+
const variable = reference.resolved;
|
|
45
|
+
if (!variable) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
if (MODULE_LEVEL_SCOPE_TYPES.has(variable.scope.type)) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
return !isRangeWithin(variable.scope.block.range, callbackRange);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The first render-scope value a memo callback reads, in source order, or null
|
|
55
|
+
* when it reads none.
|
|
56
|
+
*
|
|
57
|
+
* Answered from RESOLVED scope references rather than identifier names, so
|
|
58
|
+
* shadowing, destructuring and imports are accounted for exactly as the scope
|
|
59
|
+
* analyzer sees them. The whole callback is the unit of analysis, not just the
|
|
60
|
+
* returned literal: `const debounce = delay * 2; return { debounce };` closes
|
|
61
|
+
* over `delay` just as `return { debounce: delay }` does, and naming the
|
|
62
|
+
* callback-local `debounce` would prescribe a dependency that does not exist
|
|
63
|
+
* outside the callback.
|
|
64
|
+
*/
|
|
65
|
+
function findRenderScopeDependency(callbackScope, callbackRange) {
|
|
66
|
+
let earliest = null;
|
|
67
|
+
const pending = [callbackScope];
|
|
68
|
+
while (pending.length > 0) {
|
|
69
|
+
const current = pending.pop();
|
|
70
|
+
for (const reference of current.references) {
|
|
71
|
+
if (isTypeOnlyReference(reference)) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (!isRenderScopeReference(reference, callbackRange)) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
// Source order rather than traversal order, so the reported name does not
|
|
78
|
+
// depend on how the scope tree happens to be walked.
|
|
79
|
+
if (!earliest ||
|
|
80
|
+
reference.identifier.range[0] < earliest.identifier.range[0]) {
|
|
81
|
+
earliest = reference;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
pending.push(...current.childScopes);
|
|
85
|
+
}
|
|
86
|
+
return earliest ? earliest.identifier.name : null;
|
|
87
|
+
}
|
|
8
88
|
exports.enforceGlobalConstants = (0, createRule_1.createRule)({
|
|
9
89
|
name: 'enforce-global-constants',
|
|
10
90
|
meta: {
|
|
@@ -18,6 +98,7 @@ exports.enforceGlobalConstants = (0, createRule_1.createRule)({
|
|
|
18
98
|
messages: {
|
|
19
99
|
useGlobalConstant: 'Object literal returned from useMemo with empty dependencies creates a new reference every render without providing memoization benefits → this wastes memory and misleads readers into thinking the value is computed → move the object to a module-level constant (e.g., const OPTIONS = { ... } as const;).',
|
|
20
100
|
extractDefaultToGlobalConstant: 'Inline default value in destructuring creates a new reference on every render → this causes unnecessary re-renders in child components due to unstable identity → extract the default to a module-level constant (e.g., const DEFAULT_OPTIONS = { ... } as const;).',
|
|
101
|
+
declareMemoDependency: 'Object literal returned from useMemo reads "{{name}}" from the surrounding render scope while declaring an empty dependency array → the memo keeps the "{{name}}" captured on the first render and never recomputes, so the object silently goes stale, and it cannot be hoisted to a module-level constant because "{{name}}" exists only during a render → declare "{{name}}" (and every other render-scope value the callback reads) in the dependency array, or drop the useMemo if the object is meant to be constant.',
|
|
21
102
|
},
|
|
22
103
|
},
|
|
23
104
|
defaultOptions: [],
|
|
@@ -169,6 +250,19 @@ exports.enforceGlobalConstants = (0, createRule_1.createRule)({
|
|
|
169
250
|
}
|
|
170
251
|
return { kind: 'free' };
|
|
171
252
|
}
|
|
253
|
+
/**
|
|
254
|
+
* The first render-scope value the memo callback reads, or null when it
|
|
255
|
+
* reads none and the literal is therefore hoistable as written.
|
|
256
|
+
*/
|
|
257
|
+
function getRenderScopeDependency(callback) {
|
|
258
|
+
const callbackScope = sourceCode.scopeManager?.acquire(callback);
|
|
259
|
+
if (!callbackScope) {
|
|
260
|
+
// Without scope analysis nothing can be shown to be closed over, so the
|
|
261
|
+
// literal keeps the hoisting report the rule has always emitted.
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
return findRenderScopeDependency(callbackScope, callback.range);
|
|
265
|
+
}
|
|
172
266
|
function buildInitializerText(initText) {
|
|
173
267
|
const needsAsConst = /^(?:true|false|-?\d|\[|\{|[`'"])/.test(initText) &&
|
|
174
268
|
!/\bas const\b/.test(initText);
|
|
@@ -331,15 +425,31 @@ exports.enforceGlobalConstants = (0, createRule_1.createRule)({
|
|
|
331
425
|
if (returnValue.type === utils_1.AST_NODE_TYPES.TSAsExpression) {
|
|
332
426
|
actualReturnValue = returnValue.expression;
|
|
333
427
|
}
|
|
334
|
-
if (actualReturnValue.type
|
|
335
|
-
(actualReturnValue.type === utils_1.AST_NODE_TYPES.ArrayExpression &&
|
|
428
|
+
if (actualReturnValue.type !== utils_1.AST_NODE_TYPES.ObjectExpression &&
|
|
429
|
+
!(actualReturnValue.type === utils_1.AST_NODE_TYPES.ArrayExpression &&
|
|
336
430
|
actualReturnValue.elements.some((element) => element !== null &&
|
|
337
431
|
element.type === utils_1.AST_NODE_TYPES.ObjectExpression))) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
// An empty dependency array means the author DECLARED no dependencies,
|
|
435
|
+
// not that there are none. When the callback closes over a render-scope
|
|
436
|
+
// value, hoisting the literal to module scope does not compile — the
|
|
437
|
+
// name it reads exists only during a render — so prescribing a global
|
|
438
|
+
// constant is advice that cannot be followed. The reachable remedy is
|
|
439
|
+
// the omitted dependency, which is what the split below names.
|
|
440
|
+
const renderScopeDependency = getRenderScopeDependency(callback);
|
|
441
|
+
if (renderScopeDependency !== null) {
|
|
338
442
|
context.report({
|
|
339
443
|
node,
|
|
340
|
-
messageId: '
|
|
444
|
+
messageId: 'declareMemoDependency',
|
|
445
|
+
data: { name: renderScopeDependency },
|
|
341
446
|
});
|
|
447
|
+
return;
|
|
342
448
|
}
|
|
449
|
+
context.report({
|
|
450
|
+
node,
|
|
451
|
+
messageId: 'useGlobalConstant',
|
|
452
|
+
});
|
|
343
453
|
},
|
|
344
454
|
VariableDeclaration(node) {
|
|
345
455
|
const relevantDeclarators = node.declarations.filter((d) => d.id.type === utils_1.AST_NODE_TYPES.ObjectPattern ||
|
|
@@ -39,6 +39,18 @@ const APPROVED_REEXPORT_SOURCES = new Set(['constants', 'constants/index']);
|
|
|
39
39
|
* is.
|
|
40
40
|
*/
|
|
41
41
|
const normalizeSpecifier = (source) => source.replace(/^@\/|^src\//, '').replace(/^(\.\/|\.\.\/)+/, '');
|
|
42
|
+
const ASSERTION_NODE_TYPES = new Set([
|
|
43
|
+
utils_1.AST_NODE_TYPES.TSAsExpression,
|
|
44
|
+
utils_1.AST_NODE_TYPES.TSSatisfiesExpression,
|
|
45
|
+
utils_1.AST_NODE_TYPES.TSNonNullExpression,
|
|
46
|
+
utils_1.AST_NODE_TYPES.TSTypeAssertion,
|
|
47
|
+
]);
|
|
48
|
+
const isTypeAssertion = (node) => ASSERTION_NODE_TYPES.has(node.type);
|
|
49
|
+
/**
|
|
50
|
+
* The expression an assertion — or a stack of them, since they compose — is
|
|
51
|
+
* written onto.
|
|
52
|
+
*/
|
|
53
|
+
const unwrapTypeAssertions = (node) => isTypeAssertion(node) ? unwrapTypeAssertions(node.expression) : node;
|
|
42
54
|
const toPosixPath = (filePath) => filePath.replace(/\\/g, '/');
|
|
43
55
|
const ensureRelativeSpecifier = (specifier) => specifier.startsWith('.') ? specifier : `./${specifier}`;
|
|
44
56
|
const isWindowsDrivePath = (filePath) => /^[A-Za-z]:[\\/]/.test(filePath);
|
|
@@ -387,6 +399,17 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
|
|
|
387
399
|
if (node.type === utils_1.AST_NODE_TYPES.ChainExpression) {
|
|
388
400
|
return isValidQueryKeyUsage(node.expression);
|
|
389
401
|
}
|
|
402
|
+
// The same argument, and it holds more strongly for an assertion: `as
|
|
403
|
+
// const`, `satisfies string`, `!` and `<string>KEY` are erased before
|
|
404
|
+
// anything runs, so what they evaluate to is the very key they wrap.
|
|
405
|
+
// Naming none of these types made an asserted key fall through to
|
|
406
|
+
// `return false`, and an alias records its initializer exactly as
|
|
407
|
+
// written — so a type spelled onto an alias of an approved constant
|
|
408
|
+
// withdrew the carve-out that same alias has without one, reporting a key
|
|
409
|
+
// `prefer-global-router-state-key` accepts (#1840).
|
|
410
|
+
if (isTypeAssertion(node)) {
|
|
411
|
+
return isValidQueryKeyUsage(node.expression);
|
|
412
|
+
}
|
|
390
413
|
if (node.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
391
414
|
const importInfo = queryKeyImports.get(node.name);
|
|
392
415
|
if (importInfo && isQueryKeysSource(importInfo.source)) {
|
|
@@ -452,6 +475,15 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
|
|
|
452
475
|
* Check if a node contains string literals that should be reported
|
|
453
476
|
*/
|
|
454
477
|
function containsInvalidStringLiteral(node) {
|
|
478
|
+
// A type says nothing about where a key came from, and a literal under
|
|
479
|
+
// one came from nowhere just the same. Answering on the assertion node
|
|
480
|
+
// instead of what it wraps let `'key' as const` — and an asserted operand
|
|
481
|
+
// of a concatenation or a ternary — pass for something other than a
|
|
482
|
+
// string literal, so the only bare-key detector this rule has never ran
|
|
483
|
+
// on it (#1842).
|
|
484
|
+
if (isTypeAssertion(node)) {
|
|
485
|
+
return containsInvalidStringLiteral(node.expression);
|
|
486
|
+
}
|
|
455
487
|
// Direct string literal
|
|
456
488
|
if (node.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
457
489
|
typeof node.value === 'string') {
|
|
@@ -517,6 +549,31 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
|
|
|
517
549
|
}
|
|
518
550
|
return null;
|
|
519
551
|
}
|
|
552
|
+
/**
|
|
553
|
+
* The span a substituted constant is written over: the literal together
|
|
554
|
+
* with every assertion written onto it.
|
|
555
|
+
*
|
|
556
|
+
* The assertion is dropped rather than kept because it exists to shape the
|
|
557
|
+
* literal, and the literal is what leaves. `as const` is illegal on a
|
|
558
|
+
* reference (TS1355), so writing the constant *inside* the assertion would
|
|
559
|
+
* trade a report for a file that no longer compiles; the other notations
|
|
560
|
+
* survive that but only to restate a type the constant already has, since
|
|
561
|
+
* `queryKeys.ts` exports it narrowed. Replacing the whole span is therefore
|
|
562
|
+
* both the only uniformly compiling choice and the smaller edit to read.
|
|
563
|
+
*
|
|
564
|
+
* Null where a comment sits in the span the constant would cover: no
|
|
565
|
+
* rewrite can know what a comment beside a key meant, and deleting it is
|
|
566
|
+
* text this fixer does not own. The report then stands unfixed, which
|
|
567
|
+
* leaves the author holding both the key and the comment.
|
|
568
|
+
*/
|
|
569
|
+
function substitutionSpanOf(keyExpression, staticKeyNode) {
|
|
570
|
+
if (keyExpression === staticKeyNode) {
|
|
571
|
+
return keyExpression;
|
|
572
|
+
}
|
|
573
|
+
return sourceCode.getCommentsInside(keyExpression).length === 0
|
|
574
|
+
? keyExpression
|
|
575
|
+
: null;
|
|
576
|
+
}
|
|
520
577
|
/**
|
|
521
578
|
* The `QUERY_KEY_*` constant a key value names, or null when it names none.
|
|
522
579
|
*
|
|
@@ -599,7 +656,18 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
|
|
|
599
656
|
prop.key.name === 'key');
|
|
600
657
|
// If key property exists, check its value
|
|
601
658
|
if (keyProperty && keyProperty.value) {
|
|
602
|
-
const
|
|
659
|
+
const keyExpression = keyProperty.value;
|
|
660
|
+
// A type written onto the key is erased before anything runs,
|
|
661
|
+
// so what the arms below have to judge is the key underneath
|
|
662
|
+
// it. Dispatching on the node as written asked about the
|
|
663
|
+
// assertion — a node type neither arm names — so an invalid key
|
|
664
|
+
// escaped the rule altogether merely by carrying one, while the
|
|
665
|
+
// resolver behind `isValidQueryKeyUsage` had long since seen
|
|
666
|
+
// through it: detecting and resolving are separate paths, and
|
|
667
|
+
// widening one leaves the other exactly as it was (#1842).
|
|
668
|
+
// Reporting the unwrapped node puts the report on the same key
|
|
669
|
+
// the unasserted spelling reports.
|
|
670
|
+
const keyValue = unwrapTypeAssertions(keyExpression);
|
|
603
671
|
// Check if it's a valid query key usage
|
|
604
672
|
if (!isValidQueryKeyUsage(keyValue)) {
|
|
605
673
|
// Check if it contains invalid string literals
|
|
@@ -609,12 +677,15 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
|
|
|
609
677
|
const suggestedConstant = staticKey
|
|
610
678
|
? generateAutoFix(staticKey.text)
|
|
611
679
|
: null;
|
|
680
|
+
const span = staticKey
|
|
681
|
+
? substitutionSpanOf(keyExpression, staticKey.node)
|
|
682
|
+
: null;
|
|
612
683
|
pendingReports.push({
|
|
613
684
|
node: keyValue,
|
|
614
685
|
messageId: 'enforceQueryKeyImport',
|
|
615
|
-
substitution:
|
|
686
|
+
substitution: span && suggestedConstant
|
|
616
687
|
? {
|
|
617
|
-
keyNode:
|
|
688
|
+
keyNode: span,
|
|
618
689
|
constant: suggestedConstant,
|
|
619
690
|
scope: scopeOf(keyValue),
|
|
620
691
|
}
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,35 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.130",
|
|
4
|
+
"date": "2026-08-07T08:38:15.008Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "enforce-dynamic-file-naming",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1843
|
|
11
|
+
],
|
|
12
|
+
"summary": "honor only the disable directives ESLint itself honors (closes #1843)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "enforce-global-constants",
|
|
16
|
+
"changeType": "fix",
|
|
17
|
+
"issues": [
|
|
18
|
+
1841
|
|
19
|
+
],
|
|
20
|
+
"summary": "name the reachable remedy when a memo literal closes over render scope (closes #1841)"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "enforce-querykey-ts",
|
|
24
|
+
"changeType": "fix",
|
|
25
|
+
"issues": [
|
|
26
|
+
1840,
|
|
27
|
+
1842
|
|
28
|
+
],
|
|
29
|
+
"summary": "see through a type assertion at the report site (closes #1842); resolve an aliased key through a type assertion (closes #1840)"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
},
|
|
2
33
|
{
|
|
3
34
|
"version": "1.20.129",
|
|
4
35
|
"date": "2026-08-07T06:47:26.731Z",
|