@blumintinc/eslint-plugin-blumint 1.20.129 → 1.20.131
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-dynamic-imports.d.ts +18 -0
- package/lib/rules/enforce-dynamic-imports.js +51 -18
- 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/lib/rules/enforce-react-type-naming.js +189 -0
- package/package.json +1 -1
- package/release-manifest.json +54 -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;
|
|
@@ -22,5 +22,23 @@ type Options = [
|
|
|
22
22
|
*/
|
|
23
23
|
export declare const DEFAULT_IGNORED_LIBRARIES: string[];
|
|
24
24
|
export declare const DEFAULT_INTERNAL_PREFIXES: string[];
|
|
25
|
+
/**
|
|
26
|
+
* Builds an O(1) + glob matcher from a list of library patterns.
|
|
27
|
+
*
|
|
28
|
+
* With `coverSubpaths`, a non-glob entry stands for the package *and*
|
|
29
|
+
* everything published under it. A package's subpath entry point is the same
|
|
30
|
+
* dependency as its root — `fast-deep-equal/es6` is upstream's documented ESM
|
|
31
|
+
* build, and the spelling `fast-deep-equal-over-microdiff` steers code toward —
|
|
32
|
+
* so exempting the root while enforcing the subpath left the pair of rules
|
|
33
|
+
* jointly unsatisfiable (#1845).
|
|
34
|
+
*
|
|
35
|
+
* The boundary is `entry + '/'`, never a bare substring: `fast-deep-equal-extra`
|
|
36
|
+
* is a different package on the registry and stays enforced. Glob entries keep
|
|
37
|
+
* their minimatch semantics untouched, since a pattern already says how far it
|
|
38
|
+
* reaches.
|
|
39
|
+
*/
|
|
40
|
+
export declare const buildLibraryMatcher: (list: string[], { coverSubpaths }: {
|
|
41
|
+
coverSubpaths: boolean;
|
|
42
|
+
}) => (source: string) => boolean;
|
|
25
43
|
declare const _default: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"dynamicImportRequired", Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
26
44
|
export default _default;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DEFAULT_INTERNAL_PREFIXES = exports.DEFAULT_IGNORED_LIBRARIES = exports.RULE_NAME = void 0;
|
|
3
|
+
exports.buildLibraryMatcher = exports.DEFAULT_INTERNAL_PREFIXES = exports.DEFAULT_IGNORED_LIBRARIES = exports.RULE_NAME = void 0;
|
|
4
4
|
const createRule_1 = require("../utils/createRule");
|
|
5
5
|
const minimatch_1 = require("minimatch");
|
|
6
6
|
const module_1 = require("module");
|
|
@@ -43,6 +43,42 @@ exports.DEFAULT_IGNORED_LIBRARIES = [
|
|
|
43
43
|
'fast-deep-equal', // fast-deep-equal-over-microdiff, for files already on upstream
|
|
44
44
|
];
|
|
45
45
|
exports.DEFAULT_INTERNAL_PREFIXES = ['src/', 'functions/'];
|
|
46
|
+
/**
|
|
47
|
+
* Builds an O(1) + glob matcher from a list of library patterns.
|
|
48
|
+
*
|
|
49
|
+
* With `coverSubpaths`, a non-glob entry stands for the package *and*
|
|
50
|
+
* everything published under it. A package's subpath entry point is the same
|
|
51
|
+
* dependency as its root — `fast-deep-equal/es6` is upstream's documented ESM
|
|
52
|
+
* build, and the spelling `fast-deep-equal-over-microdiff` steers code toward —
|
|
53
|
+
* so exempting the root while enforcing the subpath left the pair of rules
|
|
54
|
+
* jointly unsatisfiable (#1845).
|
|
55
|
+
*
|
|
56
|
+
* The boundary is `entry + '/'`, never a bare substring: `fast-deep-equal-extra`
|
|
57
|
+
* is a different package on the registry and stays enforced. Glob entries keep
|
|
58
|
+
* their minimatch semantics untouched, since a pattern already says how far it
|
|
59
|
+
* reaches.
|
|
60
|
+
*/
|
|
61
|
+
const buildLibraryMatcher = (list, { coverSubpaths }) => {
|
|
62
|
+
const exactSet = new Set();
|
|
63
|
+
const subpathPrefixes = [];
|
|
64
|
+
const globs = [];
|
|
65
|
+
for (const lib of list) {
|
|
66
|
+
const mm = new minimatch_1.Minimatch(lib);
|
|
67
|
+
if (mm.hasMagic()) {
|
|
68
|
+
globs.push(mm);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
exactSet.add(lib);
|
|
72
|
+
if (coverSubpaths) {
|
|
73
|
+
subpathPrefixes.push(lib.endsWith('/') ? lib : `${lib}/`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return (source) => exactSet.has(source) ||
|
|
78
|
+
subpathPrefixes.some((prefix) => source.startsWith(prefix)) ||
|
|
79
|
+
globs.some((mm) => mm.match(source));
|
|
80
|
+
};
|
|
81
|
+
exports.buildLibraryMatcher = buildLibraryMatcher;
|
|
46
82
|
// Pre-built set of Node.js core module names for O(1) lookup.
|
|
47
83
|
const NODE_BUILTINS = new Set(module_1.builtinModules);
|
|
48
84
|
// Returns true for any source that resolves to a Node builtin: bare name
|
|
@@ -106,25 +142,22 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
106
142
|
// When `libraries` is absent, enforce-by-default mode applies:
|
|
107
143
|
// all external imports are flagged unless in `ignoredLibraries`.
|
|
108
144
|
const isWhitelistMode = libraries !== undefined;
|
|
109
|
-
// Build an O(1) + glob matcher from a list of library patterns.
|
|
110
|
-
const buildMatcher = (list) => {
|
|
111
|
-
const exactSet = new Set();
|
|
112
|
-
const globs = [];
|
|
113
|
-
for (const lib of list) {
|
|
114
|
-
const mm = new minimatch_1.Minimatch(lib);
|
|
115
|
-
if (mm.hasMagic()) {
|
|
116
|
-
globs.push(mm);
|
|
117
|
-
}
|
|
118
|
-
else {
|
|
119
|
-
exactSet.add(lib);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
return (source) => exactSet.has(source) || globs.some((mm) => mm.match(source));
|
|
123
|
-
};
|
|
124
145
|
// In whitelist mode, `libraries` is defined (checked above). In
|
|
125
146
|
// enforce-by-default mode, `ignoredLibraries` is used instead.
|
|
126
|
-
|
|
127
|
-
|
|
147
|
+
//
|
|
148
|
+
// Subpath covering is asymmetric between the two lists because the lists
|
|
149
|
+
// point in opposite directions. Widening `ignoredLibraries` only ever
|
|
150
|
+
// REMOVES reports, so it can safely absorb a package's subpath entry
|
|
151
|
+
// points. Widening `libraries` would ADD reports — a consumer who listed
|
|
152
|
+
// `pkg` to restore pre-1.16.0 behaviour would start failing on `pkg/sub` —
|
|
153
|
+
// so the whitelist keeps exact + glob semantics, and consumers who do want
|
|
154
|
+
// the subpaths enforced spell that as a glob (`pkg/**`), which still works.
|
|
155
|
+
const isListedInWhitelist = (0, exports.buildLibraryMatcher)(libraries ?? [], {
|
|
156
|
+
coverSubpaths: false,
|
|
157
|
+
});
|
|
158
|
+
const isIgnoredLibrary = (0, exports.buildLibraryMatcher)(ignoredLibraries, {
|
|
159
|
+
coverSubpaths: true,
|
|
160
|
+
});
|
|
128
161
|
// A source is external only if it looks like an npm package specifier AND
|
|
129
162
|
// is not a known-internal path. Node builtins and configured internal
|
|
130
163
|
// prefixes (e.g. src/, functions/) are excluded to avoid false positives
|
|
@@ -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
|
}
|
|
@@ -8,6 +8,189 @@ const renameFixes_1 = require("../utils/renameFixes");
|
|
|
8
8
|
const LOWERCASE_TYPES = ['ReactNode', 'JSX.Element'];
|
|
9
9
|
// Types that should have uppercase variable names
|
|
10
10
|
const UPPERCASE_TYPES = ['ComponentType', 'FC', 'FunctionComponent'];
|
|
11
|
+
/**
|
|
12
|
+
* `global-const-style` owns the NAME of a module-scope `const`, and the two
|
|
13
|
+
* rules cannot both be satisfied there: it demands UPPER_SNAKE_CASE, this rule
|
|
14
|
+
* demands a lowercase initial for `ReactNode`/`JSX.Element`. Every spelling
|
|
15
|
+
* reports under one or the other, so a consumer running both — they are both
|
|
16
|
+
* `'error'` in `recommended` — cannot write the line at all.
|
|
17
|
+
*
|
|
18
|
+
* Unexported, both renamers also autofix, so `--fix` oscillates (`element` ->
|
|
19
|
+
* `ELEMENT` -> `eLEMENT` -> `E_LEMENT` -> `e_LEMENT` -> …) until ESLint's
|
|
20
|
+
* ten-pass cap and writes the mangled identifier to disk (Issue #1846).
|
|
21
|
+
* EXPORTED, both withhold the rename — an exported name is a cross-file
|
|
22
|
+
* contract a single-file fixer cannot complete — so `--fix` is a no-op and the
|
|
23
|
+
* damage is only the unsatisfiable report pair (Issue #1847).
|
|
24
|
+
*
|
|
25
|
+
* The pair is resolved by this rule yielding: module-scope constant naming is
|
|
26
|
+
* `global-const-style`'s universal contract, while this rule's purpose —
|
|
27
|
+
* telling an element VALUE apart from a COMPONENT — is about local and
|
|
28
|
+
* parameter naming, where nothing competes with it. Do not re-open the
|
|
29
|
+
* carve-out without changing `global-const-style` in the same breath.
|
|
30
|
+
*
|
|
31
|
+
* GOVERNANCE FOLLOWS WHICH RULE REPORTS ON THE NAME, NOT WHICH ONE FIXES IT.
|
|
32
|
+
* #1846 drew the boundary at the fixer war and so excluded exports; that left
|
|
33
|
+
* the exported form unsatisfiable, because `global-const-style` withholds only
|
|
34
|
+
* its FIX there (#1700) and still emits `upperSnakeCase`. The predicate below
|
|
35
|
+
* therefore mirrors that rule's ACTUAL reporting gates, which is also why it
|
|
36
|
+
* cannot be simplified to "module-scope const" — it declines on several shapes,
|
|
37
|
+
* and yielding on one of those would leave the declaration governed by nothing:
|
|
38
|
+
*
|
|
39
|
+
* - `let`/`var`, and any non-module scope, are outside it entirely;
|
|
40
|
+
* - a declaration whose parent is neither `Program` nor an
|
|
41
|
+
* `ExportNamedDeclaration` (a block, a `for` head, a namespace body) never
|
|
42
|
+
* reaches its check;
|
|
43
|
+
* - an exported Next.js reserved name (`config`, `getStaticProps`, …) has its
|
|
44
|
+
* rename declined outright (#1257), so this rule keeps its report there —
|
|
45
|
+
* report-only, since its own fixer stands down for exports too;
|
|
46
|
+
* - a function value or a `memo`/`forwardRef` call makes it skip the whole
|
|
47
|
+
* declaration list — `const button: FC = () => …` is this rule's alone;
|
|
48
|
+
* - an absent initializer, a dynamic value, a binding alias and a
|
|
49
|
+
* `jest.Mock*` cast each silence its rename check.
|
|
50
|
+
*
|
|
51
|
+
* When any of that cannot be established the answer is `false`: keeping a
|
|
52
|
+
* report is recoverable, silently governing nothing is not.
|
|
53
|
+
*/
|
|
54
|
+
const VALUE_WRAPPER_TYPES = new Set([
|
|
55
|
+
utils_1.AST_NODE_TYPES.TSAsExpression,
|
|
56
|
+
utils_1.AST_NODE_TYPES.TSTypeAssertion,
|
|
57
|
+
utils_1.AST_NODE_TYPES.TSSatisfiesExpression,
|
|
58
|
+
utils_1.AST_NODE_TYPES.TSNonNullExpression,
|
|
59
|
+
]);
|
|
60
|
+
const isValueWrapper = (node) => VALUE_WRAPPER_TYPES.has(node.type);
|
|
61
|
+
const unwrapValueWrappers = (node) => {
|
|
62
|
+
let target = node;
|
|
63
|
+
while (isValueWrapper(target)) {
|
|
64
|
+
target = target.expression;
|
|
65
|
+
}
|
|
66
|
+
return target;
|
|
67
|
+
};
|
|
68
|
+
// `global-const-style` unwraps only `as`/`<T>` casts before classifying an
|
|
69
|
+
// initializer as dynamic or as a binding alias, so the mirror does the same.
|
|
70
|
+
const unwrapCasts = (node) => {
|
|
71
|
+
let target = node;
|
|
72
|
+
while (target.type === utils_1.AST_NODE_TYPES.TSTypeAssertion ||
|
|
73
|
+
target.type === utils_1.AST_NODE_TYPES.TSAsExpression) {
|
|
74
|
+
target = target.expression;
|
|
75
|
+
}
|
|
76
|
+
return target;
|
|
77
|
+
};
|
|
78
|
+
const COMPONENT_FACTORY_NAMES = new Set(['forwardRef', 'memo']);
|
|
79
|
+
const isComponentFactoryCall = (node) => {
|
|
80
|
+
if (node.type !== utils_1.AST_NODE_TYPES.CallExpression) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
const { callee } = node;
|
|
84
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
85
|
+
return COMPONENT_FACTORY_NAMES.has(callee.name);
|
|
86
|
+
}
|
|
87
|
+
return (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
88
|
+
!callee.computed &&
|
|
89
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
90
|
+
COMPONENT_FACTORY_NAMES.has(callee.property.name));
|
|
91
|
+
};
|
|
92
|
+
const isFunctionValue = (node) => node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
93
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression;
|
|
94
|
+
const isDynamicValue = (node) => {
|
|
95
|
+
const target = unwrapCasts(node);
|
|
96
|
+
if (target.type === utils_1.AST_NODE_TYPES.CallExpression ||
|
|
97
|
+
target.type === utils_1.AST_NODE_TYPES.NewExpression ||
|
|
98
|
+
target.type === utils_1.AST_NODE_TYPES.BinaryExpression) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
if (target.type === utils_1.AST_NODE_TYPES.ChainExpression) {
|
|
102
|
+
return isDynamicValue(target.expression);
|
|
103
|
+
}
|
|
104
|
+
if (target.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
105
|
+
return isDynamicValue(target.object);
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
};
|
|
109
|
+
const PRIMITIVE_VALUE_GLOBALS = new Set(['undefined', 'NaN', 'Infinity']);
|
|
110
|
+
const isBindingAlias = (node) => {
|
|
111
|
+
const target = unwrapCasts(node);
|
|
112
|
+
return (target.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
113
|
+
!PRIMITIVE_VALUE_GLOBALS.has(target.name));
|
|
114
|
+
};
|
|
115
|
+
const JEST_MOCK_TYPE_NAMES = new Set([
|
|
116
|
+
'Mock',
|
|
117
|
+
'MockedFunction',
|
|
118
|
+
'Mocked',
|
|
119
|
+
'MockedClass',
|
|
120
|
+
]);
|
|
121
|
+
const isJestMockTypeReference = (typeAnnotation) => {
|
|
122
|
+
if (typeAnnotation.type !== utils_1.AST_NODE_TYPES.TSTypeReference) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
const { typeName } = typeAnnotation;
|
|
126
|
+
return (typeName.type === utils_1.AST_NODE_TYPES.TSQualifiedName &&
|
|
127
|
+
typeName.left.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
128
|
+
typeName.left.name === 'jest' &&
|
|
129
|
+
typeName.right.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
130
|
+
JEST_MOCK_TYPE_NAMES.has(typeName.right.name));
|
|
131
|
+
};
|
|
132
|
+
const isJestMockCast = (node) => {
|
|
133
|
+
let current = node;
|
|
134
|
+
while (isValueWrapper(current)) {
|
|
135
|
+
if (current.type === utils_1.AST_NODE_TYPES.TSAsExpression &&
|
|
136
|
+
isJestMockTypeReference(current.typeAnnotation)) {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
current = current.expression;
|
|
140
|
+
}
|
|
141
|
+
return false;
|
|
142
|
+
};
|
|
143
|
+
// Mirrors `global-const-style`'s own list. Next.js recognizes these export
|
|
144
|
+
// names by their literal identifier, so that rule declines the rename outright
|
|
145
|
+
// rather than breaking the framework contract (#1257) — nothing there governs
|
|
146
|
+
// the name, so this rule keeps its report. Only the EXPORT name matters to
|
|
147
|
+
// Next.js, exactly as the sibling gates it.
|
|
148
|
+
const NEXTJS_RESERVED_EXPORTS = new Set([
|
|
149
|
+
'config',
|
|
150
|
+
'getServerSideProps',
|
|
151
|
+
'getStaticProps',
|
|
152
|
+
'getStaticPaths',
|
|
153
|
+
'getInitialProps',
|
|
154
|
+
'middleware',
|
|
155
|
+
]);
|
|
156
|
+
const isGlobalConstStyleGoverned = (declarator) => {
|
|
157
|
+
const declaration = declarator.parent;
|
|
158
|
+
if (!declaration ||
|
|
159
|
+
declaration.type !== utils_1.AST_NODE_TYPES.VariableDeclaration ||
|
|
160
|
+
declaration.kind !== 'const') {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
// The sibling's own scope gate: module scope, whether written bare or behind
|
|
164
|
+
// an inline `export`. Exports are INCLUDED because it reports `upperSnakeCase`
|
|
165
|
+
// on them — it withholds only the FIX (#1700) — so leaving them out kept the
|
|
166
|
+
// pair unsatisfiable for `export const element: JSX.Element = …` (#1847).
|
|
167
|
+
const isExported = declaration.parent?.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration;
|
|
168
|
+
if (declaration.parent?.type !== utils_1.AST_NODE_TYPES.Program && !isExported) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
if (isExported &&
|
|
172
|
+
declarator.id.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
173
|
+
NEXTJS_RESERVED_EXPORTS.has(declarator.id.name)) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
// The function-value / component-factory skip is evaluated over the whole
|
|
177
|
+
// declaration LIST there, so `const a = () => {}, b = <div />;` exempts both.
|
|
178
|
+
const listSkipped = declaration.declarations.some((one) => {
|
|
179
|
+
if (one.id.type !== utils_1.AST_NODE_TYPES.Identifier || !one.init) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
const target = unwrapValueWrappers(one.init);
|
|
183
|
+
return isFunctionValue(target) || isComponentFactoryCall(target);
|
|
184
|
+
});
|
|
185
|
+
if (listSkipped) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
const { init } = declarator;
|
|
189
|
+
if (!init) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
return (!isDynamicValue(init) && !isBindingAlias(init) && !isJestMockCast(init));
|
|
193
|
+
};
|
|
11
194
|
exports.enforceReactTypeNaming = (0, createRule_1.createRule)({
|
|
12
195
|
name: 'enforce-react-type-naming',
|
|
13
196
|
meta: {
|
|
@@ -123,6 +306,12 @@ exports.enforceReactTypeNaming = (0, createRule_1.createRule)({
|
|
|
123
306
|
// Skip destructured variables
|
|
124
307
|
if (isDestructured(id))
|
|
125
308
|
return;
|
|
309
|
+
// Yield the name to `global-const-style` where it governs (Issue #1846).
|
|
310
|
+
// Both branches yield: its UPPER_SNAKE_CASE target already satisfies the
|
|
311
|
+
// component branch's "starts uppercase", so nothing is lost there, and
|
|
312
|
+
// the element branch is the one that cannot coexist with it at all.
|
|
313
|
+
if (isGlobalConstStyleGoverned(node))
|
|
314
|
+
return;
|
|
126
315
|
const variableName = id.name;
|
|
127
316
|
// Get the type annotation
|
|
128
317
|
const typeAnnotation = id.typeAnnotation?.typeAnnotation;
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,58 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.131",
|
|
4
|
+
"date": "2026-08-07T10:28:22.079Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "enforce-dynamic-imports",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1845
|
|
11
|
+
],
|
|
12
|
+
"summary": "cover a package's subpaths from one ignoredLibraries entry (closes #1845)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "enforce-react-type-naming",
|
|
16
|
+
"changeType": "fix",
|
|
17
|
+
"issues": [
|
|
18
|
+
1846,
|
|
19
|
+
1847
|
|
20
|
+
],
|
|
21
|
+
"summary": "yield exported module-scope constants to global-const-style (closes #1847); yield module-scope constants to global-const-style (closes #1846)"
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"version": "1.20.130",
|
|
27
|
+
"date": "2026-08-07T08:38:15.008Z",
|
|
28
|
+
"rules": [
|
|
29
|
+
{
|
|
30
|
+
"name": "enforce-dynamic-file-naming",
|
|
31
|
+
"changeType": "fix",
|
|
32
|
+
"issues": [
|
|
33
|
+
1843
|
|
34
|
+
],
|
|
35
|
+
"summary": "honor only the disable directives ESLint itself honors (closes #1843)"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"name": "enforce-global-constants",
|
|
39
|
+
"changeType": "fix",
|
|
40
|
+
"issues": [
|
|
41
|
+
1841
|
|
42
|
+
],
|
|
43
|
+
"summary": "name the reachable remedy when a memo literal closes over render scope (closes #1841)"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"name": "enforce-querykey-ts",
|
|
47
|
+
"changeType": "fix",
|
|
48
|
+
"issues": [
|
|
49
|
+
1840,
|
|
50
|
+
1842
|
|
51
|
+
],
|
|
52
|
+
"summary": "see through a type assertion at the report site (closes #1842); resolve an aliased key through a type assertion (closes #1840)"
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
},
|
|
2
56
|
{
|
|
3
57
|
"version": "1.20.129",
|
|
4
58
|
"date": "2026-08-07T06:47:26.731Z",
|