@blumintinc/eslint-plugin-blumint 1.20.95 → 1.20.96
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/README.md +1 -1
- package/lib/index.js +1 -1
- package/lib/rules/enforce-css-media-queries.d.ts +2 -1
- package/lib/rules/enforce-css-media-queries.js +203 -8
- package/lib/rules/enforce-exported-function-types.js +209 -6
- package/lib/rules/enforce-firestore-doc-ref-generic.js +71 -18
- package/lib/rules/enforce-mui-rounded-icons.d.ts +2 -1
- package/lib/rules/enforce-mui-rounded-icons.js +194 -13
- package/lib/rules/generic-starts-with-t.js +49 -0
- package/lib/rules/global-const-style.js +101 -51
- package/lib/rules/no-always-true-false-conditions.js +231 -62
- package/lib/rules/no-unnecessary-verb-suffix.js +52 -5
- package/lib/rules/parallelize-loop-awaits.d.ts +1 -0
- package/lib/rules/parallelize-loop-awaits.js +233 -43
- package/lib/rules/prefer-getter-over-parameterless-method.d.ts +2 -1
- package/lib/rules/prefer-getter-over-parameterless-method.js +259 -0
- package/lib/rules/prefer-type-alias-over-typeof-constant.js +63 -4
- package/lib/rules/require-https-error.js +6 -2
- package/lib/rules/use-custom-link.js +45 -1
- package/lib/rules/use-custom-memo.js +30 -0
- package/lib/rules/use-custom-router.js +32 -2
- package/package.json +1 -1
- package/release-manifest.json +128 -0
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
export declare const enforceMuiRoundedIcons: TSESLint.RuleModule<"enforceRoundedVariant", [], TSESLint.RuleListener>;
|
|
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.enforceMuiRoundedIcons = void 0;
|
|
4
4
|
const createRule_1 = require("../utils/createRule");
|
|
5
5
|
const utils_1 = require("@typescript-eslint/utils");
|
|
6
|
+
const renameFixes_1 = require("../utils/renameFixes");
|
|
7
|
+
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
6
8
|
const MUI_ICONS_BARREL = '@mui/icons-material';
|
|
7
9
|
const MUI_ICONS_DEEP_PREFIX = `${MUI_ICONS_BARREL}/`;
|
|
8
10
|
/**
|
|
@@ -66,6 +68,52 @@ const toRoundedIconName = (iconName) => {
|
|
|
66
68
|
}
|
|
67
69
|
return `${baseIconName}Rounded`;
|
|
68
70
|
};
|
|
71
|
+
/** The icon a deep module path names, e.g. `AddReactionOutlined`. */
|
|
72
|
+
const deepImportIconName = (source) => source.split('/').pop();
|
|
73
|
+
/** The binding a deep import introduces for the icon its path names. */
|
|
74
|
+
const deepImportBinding = (node) => {
|
|
75
|
+
const defaultSpecifier = node.specifiers.find((specifier) => specifier.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier);
|
|
76
|
+
return defaultSpecifier ? defaultSpecifier.local : null;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* The Rounded names this declaration would give to *bindings* if its fix ran.
|
|
80
|
+
*
|
|
81
|
+
* Only a binding whose name is the icon name being replaced is ever renamed, so
|
|
82
|
+
* an aliased specifier — whose fix touches the imported name alone — claims no
|
|
83
|
+
* name here. Collected across the file so two variants of one icon
|
|
84
|
+
* (`PersonOutlined` and `PersonSharp`, both retargeting to `PersonRounded`) can
|
|
85
|
+
* be spotted before either fix runs: renaming both bindings would declare the
|
|
86
|
+
* same name twice.
|
|
87
|
+
*/
|
|
88
|
+
const claimedRoundedNames = (node) => {
|
|
89
|
+
if (node.source.type !== utils_1.AST_NODE_TYPES.Literal ||
|
|
90
|
+
typeof node.source.value !== 'string') {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
const source = node.source.value;
|
|
94
|
+
if (source.startsWith(MUI_ICONS_DEEP_PREFIX)) {
|
|
95
|
+
const iconName = deepImportIconName(source);
|
|
96
|
+
const roundedVariant = iconName ? toRoundedIconName(iconName) : null;
|
|
97
|
+
const local = deepImportBinding(node);
|
|
98
|
+
return roundedVariant && local?.name === iconName ? [roundedVariant] : [];
|
|
99
|
+
}
|
|
100
|
+
if (source !== MUI_ICONS_BARREL || node.importKind === 'type') {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
const claimed = [];
|
|
104
|
+
for (const specifier of node.specifiers) {
|
|
105
|
+
if (specifier.type !== utils_1.AST_NODE_TYPES.ImportSpecifier ||
|
|
106
|
+
specifier.importKind === 'type' ||
|
|
107
|
+
specifier.local.name !== specifier.imported.name) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const roundedVariant = toRoundedIconName(specifier.imported.name);
|
|
111
|
+
if (roundedVariant) {
|
|
112
|
+
claimed.push(roundedVariant);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return claimed;
|
|
116
|
+
};
|
|
69
117
|
exports.enforceMuiRoundedIcons = (0, createRule_1.createRule)({
|
|
70
118
|
name: 'enforce-mui-rounded-icons',
|
|
71
119
|
meta: {
|
|
@@ -82,19 +130,124 @@ exports.enforceMuiRoundedIcons = (0, createRule_1.createRule)({
|
|
|
82
130
|
},
|
|
83
131
|
defaultOptions: [],
|
|
84
132
|
create(context) {
|
|
133
|
+
const sourceCode = context.getSourceCode();
|
|
134
|
+
/**
|
|
135
|
+
* Rounded names that more than one binding in the file would take. Renaming
|
|
136
|
+
* every claimant would declare the same name twice, so no claimant of a
|
|
137
|
+
* contested name is renamed.
|
|
138
|
+
*/
|
|
139
|
+
const contestedNames = new Set();
|
|
140
|
+
/**
|
|
141
|
+
* Rewrites one reference to a renamed binding, or answers null when that
|
|
142
|
+
* site cannot be rewritten without changing more than the name.
|
|
143
|
+
*/
|
|
144
|
+
const referenceRenameFixes = (fixer, referenceId, newName) => {
|
|
145
|
+
const parent = referenceId.parent;
|
|
146
|
+
// `export { PersonOutlined }` binds the module's public export name to
|
|
147
|
+
// this token, so renaming it would change an API contract whose importers
|
|
148
|
+
// a single-file fix cannot reach.
|
|
149
|
+
if (parent?.type === utils_1.AST_NODE_TYPES.ExportSpecifier) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
// A shorthand property is one token serving as both key and value:
|
|
153
|
+
// rewriting it renames the key too, and expanding it to `key: value`
|
|
154
|
+
// reshapes source the rule has no business reshaping.
|
|
155
|
+
if (parent?.type === utils_1.AST_NODE_TYPES.Property && parent.shorthand) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
// `<Icon.Sub />` and `<svg:icon />` name the binding through a compound
|
|
159
|
+
// tag whose closing half is not a scope reference, so the two tags cannot
|
|
160
|
+
// be kept in sync from here.
|
|
161
|
+
if (parent?.type === utils_1.AST_NODE_TYPES.JSXMemberExpression ||
|
|
162
|
+
parent?.type === utils_1.AST_NODE_TYPES.JSXNamespacedName) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
const fix = (0, renameFixes_1.renameIdentifierToken)(fixer, sourceCode, referenceId, newName);
|
|
166
|
+
if (!fix) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
if (parent?.type !== utils_1.AST_NODE_TYPES.JSXOpeningElement ||
|
|
170
|
+
parent.name !== referenceId) {
|
|
171
|
+
return [fix];
|
|
172
|
+
}
|
|
173
|
+
// The scope manager records the opening tag's name as a reference but not
|
|
174
|
+
// the closing tag's, so renaming the opening half alone would leave
|
|
175
|
+
// `<PersonRounded></PersonOutlined>` — code that no longer parses.
|
|
176
|
+
const element = parent.parent;
|
|
177
|
+
const closingName = element?.type === utils_1.AST_NODE_TYPES.JSXElement
|
|
178
|
+
? element.closingElement?.name
|
|
179
|
+
: undefined;
|
|
180
|
+
if (!closingName) {
|
|
181
|
+
return [fix];
|
|
182
|
+
}
|
|
183
|
+
// JSX requires the closing name to spell the opening one, so its first
|
|
184
|
+
// token is the same name this rename replaces.
|
|
185
|
+
const closingFix = (0, renameFixes_1.renameIdentifierToken)(fixer, sourceCode, closingName, newName);
|
|
186
|
+
return closingFix ? [fix, closingFix] : null;
|
|
187
|
+
};
|
|
188
|
+
/**
|
|
189
|
+
* Builds the rewrites that rename the binding an icon import introduces:
|
|
190
|
+
* the declaring token(s) plus every in-file reference to it.
|
|
191
|
+
*
|
|
192
|
+
* Answers null when the rename cannot be applied everywhere — the caller
|
|
193
|
+
* then keeps the retarget that leaves the binding alone, or reports without
|
|
194
|
+
* a fix where no such retarget exists.
|
|
195
|
+
*/
|
|
196
|
+
const bindingRenameFixes = (fixer, declaration, localIdentifier, declarationTokens, newName) => {
|
|
197
|
+
if (contestedNames.has(newName)) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
// Resolved by declaration identity rather than by name: a file may bind
|
|
201
|
+
// the same name in several scopes, and only this declaration's symbol
|
|
202
|
+
// carries the references the fix has to rewrite.
|
|
203
|
+
const variable = ASTHelpers_1.ASTHelpers.getDeclaredVariables(context, declaration).find((candidate) => candidate.defs.some((def) => def.name === localIdentifier));
|
|
204
|
+
if (!variable || (0, renameFixes_1.renameWouldCollide)(variable, newName)) {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
const fixes = [];
|
|
208
|
+
for (const token of declarationTokens) {
|
|
209
|
+
const fix = (0, renameFixes_1.renameIdentifierToken)(fixer, sourceCode, token, newName);
|
|
210
|
+
if (!fix) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
fixes.push(fix);
|
|
214
|
+
}
|
|
215
|
+
for (const reference of variable.references) {
|
|
216
|
+
if (reference.identifier === localIdentifier) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const referenceFixes = referenceRenameFixes(fixer, reference.identifier, newName);
|
|
220
|
+
if (!referenceFixes) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
fixes.push(...referenceFixes);
|
|
224
|
+
}
|
|
225
|
+
return fixes;
|
|
226
|
+
};
|
|
85
227
|
/** `import LogoutIcon from '@mui/icons-material/Logout'` — the icon is named by the module path. */
|
|
86
228
|
const checkDeepImport = (node, iconPath) => {
|
|
87
|
-
const iconName = iconPath
|
|
229
|
+
const iconName = deepImportIconName(iconPath);
|
|
88
230
|
const roundedVariant = iconName ? toRoundedIconName(iconName) : null;
|
|
89
231
|
if (!roundedVariant) {
|
|
90
232
|
return;
|
|
91
233
|
}
|
|
234
|
+
const local = deepImportBinding(node);
|
|
235
|
+
// A hand-chosen alias (`import BellIcon from '.../NotificationsActive'`)
|
|
236
|
+
// names the icon's role, not its variant, so only a binding that repeats
|
|
237
|
+
// the icon name is renamed with the path.
|
|
238
|
+
const renamedBinding = local?.name === iconName ? local : null;
|
|
92
239
|
context.report({
|
|
93
240
|
node,
|
|
94
241
|
messageId: 'enforceRoundedVariant',
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
242
|
+
fix: (fixer) => {
|
|
243
|
+
const pathFix = fixer.replaceText(node.source, `'${MUI_ICONS_DEEP_PREFIX}${roundedVariant}'`);
|
|
244
|
+
const renameFixes = renamedBinding
|
|
245
|
+
? bindingRenameFixes(fixer, node, renamedBinding, [renamedBinding], roundedVariant)
|
|
246
|
+
: null;
|
|
247
|
+
// Path and binding move together: one fixer emits both, so a binding
|
|
248
|
+
// rename can never land without the retarget that motivates it.
|
|
249
|
+
return renameFixes ? [pathFix, ...renameFixes] : pathFix;
|
|
250
|
+
},
|
|
98
251
|
});
|
|
99
252
|
};
|
|
100
253
|
/** `import { Logout } from '@mui/icons-material'` — the icon is named by each specifier. */
|
|
@@ -116,22 +269,50 @@ exports.enforceMuiRoundedIcons = (0, createRule_1.createRule)({
|
|
|
116
269
|
continue;
|
|
117
270
|
}
|
|
118
271
|
// An alias keeps the local binding independent of the imported name, so
|
|
119
|
-
// swapping the imported name is a self-contained edit
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
272
|
+
// swapping the imported name is a self-contained edit that leaves the
|
|
273
|
+
// binding — and every reference to it — alone.
|
|
274
|
+
if (specifier.local.name !== importedName) {
|
|
275
|
+
context.report({
|
|
276
|
+
node: specifier,
|
|
277
|
+
messageId: 'enforceRoundedVariant',
|
|
278
|
+
fix: (fixer) => fixer.replaceText(specifier.imported, roundedVariant),
|
|
279
|
+
});
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
// Without an alias the imported name IS the local binding, so changing
|
|
283
|
+
// it renames the binding: the fix must own every reference to it or not
|
|
284
|
+
// exist at all. `import { Logout }` spells the imported name and the
|
|
285
|
+
// local one with a single token, which is rewritten once; the redundant
|
|
286
|
+
// `import { Logout as Logout }` spells them separately, so both move.
|
|
287
|
+
const declarationTokens = specifier.imported.range[0] === specifier.local.range[0]
|
|
288
|
+
? [specifier.local]
|
|
289
|
+
: [specifier.imported, specifier.local];
|
|
125
290
|
context.report({
|
|
126
291
|
node: specifier,
|
|
127
292
|
messageId: 'enforceRoundedVariant',
|
|
128
|
-
fix:
|
|
129
|
-
? (fixer) => fixer.replaceText(specifier.imported, roundedVariant)
|
|
130
|
-
: null,
|
|
293
|
+
fix: (fixer) => bindingRenameFixes(fixer, node, specifier.local, declarationTokens, roundedVariant),
|
|
131
294
|
});
|
|
132
295
|
}
|
|
133
296
|
};
|
|
134
297
|
return {
|
|
298
|
+
// Every import is a top-level statement, so the names the file's fixes
|
|
299
|
+
// would introduce are all knowable before the first report.
|
|
300
|
+
Program(node) {
|
|
301
|
+
const claimCounts = new Map();
|
|
302
|
+
for (const statement of node.body) {
|
|
303
|
+
if (statement.type !== utils_1.AST_NODE_TYPES.ImportDeclaration) {
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
for (const claimed of claimedRoundedNames(statement)) {
|
|
307
|
+
claimCounts.set(claimed, (claimCounts.get(claimed) ?? 0) + 1);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
for (const [name, count] of claimCounts) {
|
|
311
|
+
if (count > 1) {
|
|
312
|
+
contestedNames.add(name);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
},
|
|
135
316
|
ImportDeclaration(node) {
|
|
136
317
|
if (node.source.type !== utils_1.AST_NODE_TYPES.Literal ||
|
|
137
318
|
typeof node.source.value !== 'string') {
|
|
@@ -2,10 +2,59 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.genericStartsWithT = void 0;
|
|
4
4
|
const createRule_1 = require("../utils/createRule");
|
|
5
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
6
|
+
/**
|
|
7
|
+
* A module augmentation targets either an external module
|
|
8
|
+
* (`declare module 'pkg'`, whose id is a string literal) or the global scope
|
|
9
|
+
* (`declare global`). TypeScript requires every declaration of a merged entity
|
|
10
|
+
* to spell its type parameters identically (TS2428, "All declarations of 'X'
|
|
11
|
+
* must have identical type parameters"), so the name inside an augmentation is
|
|
12
|
+
* fixed by the upstream declaration rather than chosen by the author. Renaming
|
|
13
|
+
* it to satisfy the convention breaks the merge.
|
|
14
|
+
*
|
|
15
|
+
* A plain `namespace X` — including the ambient `declare namespace X` and
|
|
16
|
+
* `declare module Foo`, both of which carry an identifier id — augments nothing
|
|
17
|
+
* upstream, so its type parameters stay author-owned and reportable.
|
|
18
|
+
*/
|
|
19
|
+
function isModuleAugmentation(node) {
|
|
20
|
+
if (node.id.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
21
|
+
typeof node.id.value === 'string') {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
// `declare global` carries a dedicated flag, which also covers the bare
|
|
25
|
+
// `global { ... }` form nested inside an ambient module (that form has no
|
|
26
|
+
// `declare` of its own).
|
|
27
|
+
if (node.global === true) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
// Parser versions that predate the `global` flag spell the same block as an
|
|
31
|
+
// ambient module whose id is the `global` keyword. Requiring `declare` keeps
|
|
32
|
+
// an ordinary `namespace global { ... }` reportable.
|
|
33
|
+
return (node.declare === true &&
|
|
34
|
+
node.id.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
35
|
+
node.id.name === 'global');
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The declaration need not be a direct child of the augmentation block; it can
|
|
39
|
+
* sit inside a nested namespace, an interface member signature or any other
|
|
40
|
+
* container within it, so the whole ancestor chain is inspected.
|
|
41
|
+
*/
|
|
42
|
+
function isInsideModuleAugmentation(node) {
|
|
43
|
+
for (let ancestor = node.parent; ancestor; ancestor = ancestor.parent) {
|
|
44
|
+
if (ancestor.type === utils_1.AST_NODE_TYPES.TSModuleDeclaration &&
|
|
45
|
+
isModuleAugmentation(ancestor)) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
5
51
|
exports.genericStartsWithT = (0, createRule_1.createRule)({
|
|
6
52
|
create(context) {
|
|
7
53
|
return {
|
|
8
54
|
TSTypeParameterDeclaration(node) {
|
|
55
|
+
if (isInsideModuleAugmentation(node)) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
9
58
|
for (const param of node.params) {
|
|
10
59
|
if (typeof param.name.name === 'string' &&
|
|
11
60
|
param.name.name[0] !== 'T') {
|
|
@@ -23,6 +23,28 @@ const toUpperSnakeCase = (name) => name
|
|
|
23
23
|
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
|
|
24
24
|
.toUpperCase()
|
|
25
25
|
.replace(/^_/, '');
|
|
26
|
+
// `x as T`, `<T>x`, `x satisfies T` and `x!` annotate or assert an expression
|
|
27
|
+
// without contributing a value of their own, so a check that classifies the
|
|
28
|
+
// *shape* of an initializer must look through all four alike. Recognizing only
|
|
29
|
+
// some of them makes the rule's carve-outs depend on which type syntax an
|
|
30
|
+
// author happened to reach for: a React component written
|
|
31
|
+
// `memo(Foo) satisfies ComponentType` or `memo(Foo)!` read as opaque
|
|
32
|
+
// expressions and were renamed to UPPER_SNAKE_CASE while `memo(Foo) as FC` was
|
|
33
|
+
// exempt (Issue #1681).
|
|
34
|
+
const VALUE_WRAPPER_TYPES = new Set([
|
|
35
|
+
utils_1.AST_NODE_TYPES.TSAsExpression,
|
|
36
|
+
utils_1.AST_NODE_TYPES.TSTypeAssertion,
|
|
37
|
+
utils_1.AST_NODE_TYPES.TSSatisfiesExpression,
|
|
38
|
+
utils_1.AST_NODE_TYPES.TSNonNullExpression,
|
|
39
|
+
]);
|
|
40
|
+
const isValueWrapper = (node) => VALUE_WRAPPER_TYPES.has(node.type);
|
|
41
|
+
const unwrapValueWrappers = (node) => {
|
|
42
|
+
let target = node;
|
|
43
|
+
while (isValueWrapper(target)) {
|
|
44
|
+
target = target.expression;
|
|
45
|
+
}
|
|
46
|
+
return target;
|
|
47
|
+
};
|
|
26
48
|
// Jest mock handles produced by an `as` cast to a `jest.Mock*` type are
|
|
27
49
|
// stateful test doubles that are reassigned/mutated through
|
|
28
50
|
// `.mockImplementation()`, `.mockReturnValue()`, etc. They are not immutable
|
|
@@ -38,11 +60,7 @@ const JEST_MOCK_TYPE_NAMES = new Set([
|
|
|
38
60
|
// `jest.Mocked<...>` / `jest.MockedClass<...>`. The match is kept deliberately
|
|
39
61
|
// narrow — a qualified `jest.<MockType>` type reference — so unrelated `as`
|
|
40
62
|
// casts keep triggering the rename check.
|
|
41
|
-
const
|
|
42
|
-
if (node.type !== utils_1.AST_NODE_TYPES.TSAsExpression) {
|
|
43
|
-
return false;
|
|
44
|
-
}
|
|
45
|
-
const typeAnnotation = node.typeAnnotation;
|
|
63
|
+
const isJestMockTypeReference = (typeAnnotation) => {
|
|
46
64
|
if (typeAnnotation.type !== utils_1.AST_NODE_TYPES.TSTypeReference) {
|
|
47
65
|
return false;
|
|
48
66
|
}
|
|
@@ -53,6 +71,43 @@ const isJestMockCast = (node) => {
|
|
|
53
71
|
typeName.right.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
54
72
|
JEST_MOCK_TYPE_NAMES.has(typeName.right.name));
|
|
55
73
|
};
|
|
74
|
+
// The cast can sit anywhere in a wrapper chain (`(foo as jest.Mock)!`,
|
|
75
|
+
// `foo as jest.Mock satisfies unknown`), so the whole chain is scanned rather
|
|
76
|
+
// than the outermost node alone — a mock handle stays a mock handle whatever is
|
|
77
|
+
// wrapped around the cast.
|
|
78
|
+
const isJestMockCast = (node) => {
|
|
79
|
+
let current = node;
|
|
80
|
+
while (isValueWrapper(current)) {
|
|
81
|
+
if (current.type === utils_1.AST_NODE_TYPES.TSAsExpression &&
|
|
82
|
+
isJestMockTypeReference(current.typeAnnotation)) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
current = current.expression;
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
};
|
|
89
|
+
// React's component factories, called bare (`memo(Foo)`) or through a namespace
|
|
90
|
+
// import (`React.memo(Foo)`).
|
|
91
|
+
const COMPONENT_FACTORY_NAMES = new Set(['forwardRef', 'memo']);
|
|
92
|
+
const isComponentFactoryCall = (node) => {
|
|
93
|
+
if (node.type !== utils_1.AST_NODE_TYPES.CallExpression) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
const { callee } = node;
|
|
97
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
98
|
+
return COMPONENT_FACTORY_NAMES.has(callee.name);
|
|
99
|
+
}
|
|
100
|
+
return (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
101
|
+
!callee.computed &&
|
|
102
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
103
|
+
COMPONENT_FACTORY_NAMES.has(callee.property.name));
|
|
104
|
+
};
|
|
105
|
+
// A function value is a component, hook or helper, never the module-level
|
|
106
|
+
// configuration value this rule governs. The two spellings are interchangeable
|
|
107
|
+
// at a declaration site, so `const Row = function (props) {...}` is exempt on
|
|
108
|
+
// the same terms as `const Row = (props) => {...}` (Issue #1681).
|
|
109
|
+
const isFunctionValue = (node) => node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
110
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression;
|
|
56
111
|
/**
|
|
57
112
|
* Walks the scope chain upward from `scope` (inclusive) and reports whether
|
|
58
113
|
* `targetName` is bound anywhere between `scope` and `stopScope` (inclusive).
|
|
@@ -141,7 +196,16 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
141
196
|
// Check if the file is a TypeScript file
|
|
142
197
|
const isTypeScript = context.getFilename().endsWith('.ts') ||
|
|
143
198
|
context.getFilename().endsWith('.tsx');
|
|
144
|
-
|
|
199
|
+
/**
|
|
200
|
+
* Strips `as`/`<T>` casts only, and is deliberately narrower than
|
|
201
|
+
* `unwrapValueWrappers`. The two carve-outs below — dynamic values and
|
|
202
|
+
* binding aliases — silence the rule entirely, so widening them to see
|
|
203
|
+
* through `!`/`satisfies` would newly exempt `const value = getValue()!`
|
|
204
|
+
* and `const alias = other!` from the rename check. That is a detection
|
|
205
|
+
* change of its own, distinct from the wrapper-blind component exemption
|
|
206
|
+
* `unwrapValueWrappers` cures (Issue #1681).
|
|
207
|
+
*/
|
|
208
|
+
const unwrapCasts = (node) => {
|
|
145
209
|
let target = node;
|
|
146
210
|
while (target.type === utils_1.AST_NODE_TYPES.TSTypeAssertion ||
|
|
147
211
|
target.type === utils_1.AST_NODE_TYPES.TSAsExpression) {
|
|
@@ -150,7 +214,7 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
150
214
|
return target;
|
|
151
215
|
};
|
|
152
216
|
const isDynamicValue = (node) => {
|
|
153
|
-
const target =
|
|
217
|
+
const target = unwrapCasts(node);
|
|
154
218
|
if (target.type === utils_1.AST_NODE_TYPES.CallExpression ||
|
|
155
219
|
target.type === utils_1.AST_NODE_TYPES.NewExpression ||
|
|
156
220
|
target.type === utils_1.AST_NODE_TYPES.BinaryExpression) {
|
|
@@ -174,18 +238,18 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
174
238
|
* such a re-export is preserving a name importers depend on and a
|
|
175
239
|
* single-file fixer cannot rewrite them (Issue #1418).
|
|
176
240
|
*
|
|
177
|
-
* The check unwraps
|
|
241
|
+
* The check unwraps casts so a type-pinned alias (`x as Foo`,
|
|
178
242
|
* `x as const`) is treated the same as the bare form. A `MemberExpression`
|
|
179
243
|
* (`Foo.bar`) is deliberately not covered — it keeps whatever behavior
|
|
180
244
|
* `isDynamicValue` already gives it.
|
|
181
245
|
*/
|
|
182
246
|
const isBindingAlias = (node) => {
|
|
183
|
-
const target =
|
|
247
|
+
const target = unwrapCasts(node);
|
|
184
248
|
return (target.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
185
249
|
!PRIMITIVE_VALUE_GLOBALS.has(target.name));
|
|
186
250
|
};
|
|
187
251
|
const describeValueKind = (node) => {
|
|
188
|
-
const target =
|
|
252
|
+
const target = unwrapValueWrappers(node);
|
|
189
253
|
if (target.type === utils_1.AST_NODE_TYPES.ArrayExpression) {
|
|
190
254
|
return 'an array literal';
|
|
191
255
|
}
|
|
@@ -208,41 +272,22 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
208
272
|
node.parent?.type !== utils_1.AST_NODE_TYPES.ExportNamedDeclaration) {
|
|
209
273
|
return;
|
|
210
274
|
}
|
|
211
|
-
// Skip if any declaration is a function component,
|
|
275
|
+
// Skip if any declaration is a function value (component, hook or
|
|
276
|
+
// helper) or a `memo`/`forwardRef` component factory call. The
|
|
277
|
+
// initializer is classified through any type wrappers, so the pinned
|
|
278
|
+
// forms (`… as FC`, `… satisfies ComponentType`, `…!`) are exempt on
|
|
279
|
+
// the same terms as the bare expression they wrap.
|
|
212
280
|
const shouldSkip = node.declarations.some((declaration) => {
|
|
213
281
|
if (declaration.id.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
214
282
|
return false;
|
|
215
283
|
}
|
|
216
|
-
const name = declaration.id.name;
|
|
217
284
|
const init = declaration.init;
|
|
218
285
|
// Skip if no initializer
|
|
219
286
|
if (!init) {
|
|
220
287
|
return false;
|
|
221
288
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
init.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
225
|
-
return true;
|
|
226
|
-
}
|
|
227
|
-
// Skip any arrow function
|
|
228
|
-
if (init.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
229
|
-
return true;
|
|
230
|
-
}
|
|
231
|
-
// Skip forwardRef and memo calls
|
|
232
|
-
if (init.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
233
|
-
if (init.callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
234
|
-
return ['forwardRef', 'memo'].includes(init.callee.name);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
// Skip type assertions on forwardRef and memo calls
|
|
238
|
-
if (init.type === utils_1.AST_NODE_TYPES.TSAsExpression) {
|
|
239
|
-
const expression = init.expression;
|
|
240
|
-
if (expression.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
241
|
-
expression.callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
242
|
-
return ['forwardRef', 'memo'].includes(expression.callee.name);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
return false;
|
|
289
|
+
const target = unwrapValueWrappers(init);
|
|
290
|
+
return isFunctionValue(target) || isComponentFactoryCall(target);
|
|
246
291
|
});
|
|
247
292
|
if (shouldSkip) {
|
|
248
293
|
return;
|
|
@@ -267,15 +312,19 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
267
312
|
: '';
|
|
268
313
|
// Only check for as const in TypeScript files
|
|
269
314
|
if (isTypeScript) {
|
|
315
|
+
// An `as const` anywhere in the wrapper chain already freezes the
|
|
316
|
+
// value, including when a later wrapper hides it
|
|
317
|
+
// (`{...} as const satisfies Config`, `({...} as const)!`).
|
|
270
318
|
const hasAsConstAssertion = (node) => {
|
|
271
319
|
let current = node;
|
|
272
|
-
while (current
|
|
273
|
-
(current.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
|
|
274
|
-
current.type === utils_1.AST_NODE_TYPES.TSTypeAssertion)
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
typeAnnotation.typeName.type ===
|
|
278
|
-
|
|
320
|
+
while (isValueWrapper(current)) {
|
|
321
|
+
if ((current.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
|
|
322
|
+
current.type === utils_1.AST_NODE_TYPES.TSTypeAssertion) &&
|
|
323
|
+
current.typeAnnotation.type ===
|
|
324
|
+
utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
325
|
+
current.typeAnnotation.typeName.type ===
|
|
326
|
+
utils_1.AST_NODE_TYPES.Identifier &&
|
|
327
|
+
current.typeAnnotation.typeName.name === 'const') {
|
|
279
328
|
return true;
|
|
280
329
|
}
|
|
281
330
|
current = current.expression;
|
|
@@ -287,14 +336,15 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
287
336
|
if (hasAsConstAssertion(node)) {
|
|
288
337
|
return false;
|
|
289
338
|
}
|
|
290
|
-
const target =
|
|
291
|
-
// Skip an initializer already wrapped in a non-`const`
|
|
292
|
-
// (`{...} as T`, `<T>{...}`, `{...} as unknown as T
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
// regex/null/boolean carve-outs
|
|
296
|
-
//
|
|
297
|
-
// `id.typeAnnotation` case
|
|
339
|
+
const target = unwrapValueWrappers(node);
|
|
340
|
+
// Skip an initializer already wrapped in a non-`const` type
|
|
341
|
+
// wrapper (`{...} as T`, `<T>{...}`, `{...} as unknown as T`,
|
|
342
|
+
// `{...} satisfies T`, `{...}!`). A `const` assertion may only be
|
|
343
|
+
// applied to a literal, so appending one after such a chain is
|
|
344
|
+
// TS1355 — the same failure mode the regex/null/boolean carve-outs
|
|
345
|
+
// below exist for. Such a wrapper is also the author pinning the
|
|
346
|
+
// type deliberately, exactly like the `id.typeAnnotation` case
|
|
347
|
+
// skipped next.
|
|
298
348
|
if (target !== node) {
|
|
299
349
|
return false;
|
|
300
350
|
}
|