@blumintinc/eslint-plugin-blumint 1.15.0 → 1.16.0
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 +28 -5
- package/lib/index.js +7 -1
- package/lib/rules/consistent-callback-naming.js +26 -0
- package/lib/rules/dynamic-https-errors.js +5 -26
- package/lib/rules/enforce-boolean-naming-prefixes.d.ts +1 -0
- package/lib/rules/enforce-boolean-naming-prefixes.js +86 -119
- package/lib/rules/enforce-dynamic-imports.d.ts +2 -1
- package/lib/rules/enforce-dynamic-imports.js +42 -21
- package/lib/rules/enforce-memoize-async.js +1 -4
- package/lib/rules/enforce-mui-rounded-icons.js +42 -1
- package/lib/rules/enforce-verb-noun-naming.js +3 -0
- package/lib/rules/global-const-style.js +9 -0
- package/lib/rules/logical-top-to-bottom-grouping.js +80 -2
- package/lib/rules/memo-compare-deeply-complex-props.js +167 -3
- package/lib/rules/memo-nested-react-components.js +143 -8
- package/lib/rules/no-array-length-in-deps.js +74 -3
- package/lib/rules/no-circular-references.js +145 -482
- package/lib/rules/no-compositing-layer-props.js +31 -0
- package/lib/rules/no-entire-object-hook-deps.js +132 -97
- package/lib/rules/no-explicit-return-type.js +6 -0
- package/lib/rules/no-hungarian.js +119 -24
- package/lib/rules/no-margin-properties.js +7 -38
- package/lib/rules/no-unnecessary-verb-suffix.js +79 -0
- package/lib/rules/no-unused-props.js +215 -37
- package/lib/rules/no-useless-fragment.js +10 -2
- package/lib/rules/parallelize-async-operations.js +1 -3
- package/lib/rules/prefer-type-alias-over-typeof-constant.js +73 -11
- package/lib/rules/prefer-use-deep-compare-memo.js +8 -14
- package/lib/rules/react-memoize-literals.js +87 -1
- package/lib/rules/require-migration-script-metadata.d.ts +9 -0
- package/lib/rules/require-migration-script-metadata.js +206 -0
- package/lib/rules/warn-https-error-message-user-friendly.d.ts +1 -0
- package/lib/rules/warn-https-error-message-user-friendly.js +239 -0
- package/lib/utils/ASTHelpers.d.ts +15 -0
- package/lib/utils/ASTHelpers.js +48 -0
- package/package.json +7 -6
- package/release-manifest.json +166 -0
|
@@ -67,7 +67,13 @@ exports.noMarginProperties = (0, createRule_1.createRule)({
|
|
|
67
67
|
const normalizedName = normalizePropertyName(propertyName);
|
|
68
68
|
return MARGIN_PROPERTIES.has(normalizedName);
|
|
69
69
|
}
|
|
70
|
-
|
|
70
|
+
/**
|
|
71
|
+
* True if node is in a flagged MUI styling context (sx attribute, sx
|
|
72
|
+
* object property, or css() call). Excludes createTheme styleOverrides:
|
|
73
|
+
* theme margins ARE the container-controlled styling (viewport insets,
|
|
74
|
+
* resets of MUI's built-in margins, internal layout), not the
|
|
75
|
+
* sibling-spacing this rule targets.
|
|
76
|
+
*/
|
|
71
77
|
function isMuiStylingContext(node) {
|
|
72
78
|
let current = node;
|
|
73
79
|
while (current?.parent) {
|
|
@@ -83,13 +89,6 @@ exports.noMarginProperties = (0, createRule_1.createRule)({
|
|
|
83
89
|
current.parent.key.name === 'sx') {
|
|
84
90
|
return true;
|
|
85
91
|
}
|
|
86
|
-
// Check for theme overrides (MUI's createTheme)
|
|
87
|
-
if (current.parent.type === utils_1.AST_NODE_TYPES.Property &&
|
|
88
|
-
current.parent.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
89
|
-
(current.parent.key.name === 'styleOverrides' ||
|
|
90
|
-
current.parent.key.name === 'components')) {
|
|
91
|
-
return true;
|
|
92
|
-
}
|
|
93
92
|
// Check for MUI's css function
|
|
94
93
|
if (current.parent.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
95
94
|
current.parent.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
@@ -290,36 +289,6 @@ exports.noMarginProperties = (0, createRule_1.createRule)({
|
|
|
290
289
|
checkObjectExpression(arg);
|
|
291
290
|
}
|
|
292
291
|
}
|
|
293
|
-
// Handle createTheme for MUI theme overrides
|
|
294
|
-
if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
295
|
-
node.callee.name === 'createTheme' &&
|
|
296
|
-
node.arguments.length > 0 &&
|
|
297
|
-
node.arguments[0].type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
298
|
-
const themeObj = node.arguments[0];
|
|
299
|
-
// Find components property in theme object
|
|
300
|
-
const componentsProperty = themeObj.properties.find((prop) => prop.type === utils_1.AST_NODE_TYPES.Property &&
|
|
301
|
-
prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
302
|
-
prop.key.name === 'components' &&
|
|
303
|
-
prop.value.type === utils_1.AST_NODE_TYPES.ObjectExpression);
|
|
304
|
-
if (componentsProperty &&
|
|
305
|
-
componentsProperty.value.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
306
|
-
// Check each component override
|
|
307
|
-
componentsProperty.value.properties.forEach((componentProp) => {
|
|
308
|
-
if (componentProp.type === utils_1.AST_NODE_TYPES.Property &&
|
|
309
|
-
componentProp.value.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
310
|
-
// Find styleOverrides property
|
|
311
|
-
const styleOverrides = componentProp.value.properties.find((prop) => prop.type === utils_1.AST_NODE_TYPES.Property &&
|
|
312
|
-
prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
313
|
-
prop.key.name === 'styleOverrides' &&
|
|
314
|
-
prop.value.type === utils_1.AST_NODE_TYPES.ObjectExpression);
|
|
315
|
-
if (styleOverrides &&
|
|
316
|
-
styleOverrides.value.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
317
|
-
checkObjectExpression(styleOverrides.value);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
292
|
},
|
|
324
293
|
};
|
|
325
294
|
},
|
|
@@ -63,6 +63,77 @@ const COMMON_PREPOSITION_SUFFIXES = new Set([
|
|
|
63
63
|
'Async',
|
|
64
64
|
'Sync',
|
|
65
65
|
]);
|
|
66
|
+
/**
|
|
67
|
+
* Phrasal-verb particles that fuse with a preceding past participle to form an
|
|
68
|
+
* inseparable state adjective (e.g. "signed in", "logged in", "opted in",
|
|
69
|
+
* "logged out", "zoomed in"). When such a particle is the trailing suffix AND
|
|
70
|
+
* the token before it is a past participle, the ending is NOT a redundant
|
|
71
|
+
* verb-preposition action suffix — it is a single adjective describing state.
|
|
72
|
+
*/
|
|
73
|
+
const PHRASAL_PARTICLES = new Set(['In', 'On', 'Out', 'Up', 'Off', 'Down']);
|
|
74
|
+
/**
|
|
75
|
+
* Verb stems that fuse with a phrasal particle into an established phrasal verb
|
|
76
|
+
* where the particle is inseparable (e.g. "signIn", "logOut", "optIn",
|
|
77
|
+
* "checkIn"). Matched in base form ("signIn", "useGuardSignIn") and
|
|
78
|
+
* past-participle form ("signedIn", "loggedOut", "droppedIn"). A particle
|
|
79
|
+
* preceded by a NOUN object instead — "searchItemsIn", "processEventOn",
|
|
80
|
+
* "loadEmbedIn", "isWidgetIn" — is a genuine redundant verb-preposition suffix
|
|
81
|
+
* and stays flagged. Extend this set when a new phrasal verb appears.
|
|
82
|
+
*/
|
|
83
|
+
const PHRASAL_VERB_STEMS = new Set([
|
|
84
|
+
'sign',
|
|
85
|
+
'log',
|
|
86
|
+
'opt',
|
|
87
|
+
'check',
|
|
88
|
+
'zoom',
|
|
89
|
+
'drop',
|
|
90
|
+
'shut',
|
|
91
|
+
'turn',
|
|
92
|
+
'switch',
|
|
93
|
+
'scroll',
|
|
94
|
+
]);
|
|
95
|
+
/**
|
|
96
|
+
* Resolves the lowercased phrasal-verb stem of a final camelCase word, or null
|
|
97
|
+
* when it is not a known phrasal verb. Handles the base form ("sign"), the
|
|
98
|
+
* regular past participle ("signed" → "sign"), and the doubled-consonant
|
|
99
|
+
* participle ("dropped" → "drop"). A noun that merely ends in "ed" (e.g.
|
|
100
|
+
* "embed" → "emb", "shed" → "sh") resolves to null, so it stays flagged — this
|
|
101
|
+
* is what keeps a leaky "ends in ed" heuristic from exempting genuine targets.
|
|
102
|
+
*/
|
|
103
|
+
function phrasalVerbStem(lastWord) {
|
|
104
|
+
const word = lastWord.toLowerCase();
|
|
105
|
+
if (PHRASAL_VERB_STEMS.has(word)) {
|
|
106
|
+
return word;
|
|
107
|
+
}
|
|
108
|
+
if (word.endsWith('ed')) {
|
|
109
|
+
let stem = word.slice(0, -2);
|
|
110
|
+
if (stem.length >= 2 && stem[stem.length - 1] === stem[stem.length - 2]) {
|
|
111
|
+
stem = stem.slice(0, -1);
|
|
112
|
+
}
|
|
113
|
+
if (PHRASAL_VERB_STEMS.has(stem)) {
|
|
114
|
+
return stem;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Returns true when the trailing `suffix` of `name` is a phrasal-verb particle
|
|
121
|
+
* fused to its verb — a past-participle adjective ("signedIn", "loggedOut") or a
|
|
122
|
+
* base-form phrasal verb ("signIn", "logOut"). The word immediately before the
|
|
123
|
+
* particle must be a KNOWN phrasal verb (via phrasalVerbStem), not merely any
|
|
124
|
+
* word ending in "ed"; that keeps redundant suffixes where a noun object
|
|
125
|
+
* precedes the particle ("loadEmbedIn", "isWidgetIn", "searchItemsIn") flagged.
|
|
126
|
+
* This single check also covers boolean predicates: "isSignedIn" resolves the
|
|
127
|
+
* pre-particle "signed" → "sign", while "isWidgetIn" resolves "widget" → null.
|
|
128
|
+
*/
|
|
129
|
+
function isPhrasalVerbEnding(name, suffix) {
|
|
130
|
+
if (!PHRASAL_PARTICLES.has(suffix)) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
const beforeSuffix = name.substring(0, name.length - suffix.length);
|
|
134
|
+
const lastWord = beforeSuffix.match(/[A-Z]?[a-z]+$/)?.[0] ?? '';
|
|
135
|
+
return phrasalVerbStem(lastWord) !== null;
|
|
136
|
+
}
|
|
66
137
|
exports.noUnnecessaryVerbSuffix = (0, createRule_1.createRule)({
|
|
67
138
|
name: 'no-unnecessary-verb-suffix',
|
|
68
139
|
meta: {
|
|
@@ -94,6 +165,14 @@ exports.noUnnecessaryVerbSuffix = (0, createRule_1.createRule)({
|
|
|
94
165
|
// Skip if the suggestion would be empty or just a single character
|
|
95
166
|
if (suggestion.length <= 1)
|
|
96
167
|
continue;
|
|
168
|
+
// Skip phrasal-verb endings (e.g. past-participle adjectives
|
|
169
|
+
// "signedIn"/"loggedOut" or compound phrasal verbs "signIn"/
|
|
170
|
+
// "logOut"): the trailing particle fuses with its verb, so stripping
|
|
171
|
+
// it ("signed", "sign") destroys the meaning. The pre-particle word
|
|
172
|
+
// must be a known phrasal verb, so noun-object endings like
|
|
173
|
+
// "loadEmbedIn"/"isWidgetIn" remain flagged.
|
|
174
|
+
if (isPhrasalVerbEnding(name, suffix))
|
|
175
|
+
continue;
|
|
97
176
|
context.report({
|
|
98
177
|
node,
|
|
99
178
|
messageId: 'unnecessaryVerbSuffix',
|
|
@@ -118,6 +118,45 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
|
|
|
118
118
|
}
|
|
119
119
|
return null;
|
|
120
120
|
};
|
|
121
|
+
/**
|
|
122
|
+
* Resolves the `*Props` type name from a parameter's type-annotation node.
|
|
123
|
+
* Unwraps parenthesized types and descends through generic wrapper type
|
|
124
|
+
* arguments (e.g. `Readonly<Foo>`, `Partial<...>`, `Readonly<Partial<Foo>>`)
|
|
125
|
+
* to find the underlying `*Props` reference. Returns the first `*Props`
|
|
126
|
+
* identifier found, or `null`. Uses the same `endsWith('Props')` heuristic
|
|
127
|
+
* as the direct path so forward-declared Props types still resolve.
|
|
128
|
+
*/
|
|
129
|
+
const resolvePropsTypeName = (typeNode, visited = new Set()) => {
|
|
130
|
+
if (!typeNode || visited.has(typeNode)) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
visited.add(typeNode);
|
|
134
|
+
if (isTSParenthesizedType(typeNode)) {
|
|
135
|
+
return resolvePropsTypeName(typeNode.typeAnnotation, visited);
|
|
136
|
+
}
|
|
137
|
+
if (typeNode.type !== utils_1.AST_NODE_TYPES.TSTypeReference) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
if (typeNode.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
141
|
+
typeNode.typeName.name.endsWith('Props')) {
|
|
142
|
+
return typeNode.typeName.name;
|
|
143
|
+
}
|
|
144
|
+
// Generic wrapper (Readonly/Partial/etc.): descend into its type arguments
|
|
145
|
+
// to find the nested *Props reference. typeParameters is the v5 AST shape;
|
|
146
|
+
// typeArguments is the v6+ shape — handle both for forward compatibility.
|
|
147
|
+
const typeArgs = typeNode.typeParameters ??
|
|
148
|
+
typeNode
|
|
149
|
+
.typeArguments;
|
|
150
|
+
if (typeArgs) {
|
|
151
|
+
for (const arg of typeArgs.params) {
|
|
152
|
+
const resolved = resolvePropsTypeName(arg, visited);
|
|
153
|
+
if (resolved) {
|
|
154
|
+
return resolved;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
};
|
|
121
160
|
const isAnyPropFromSpreadTypeUsed = (spreadTypeName, used, knownProps) => {
|
|
122
161
|
const spreadTypeProps = spreadTypeToPropNames.get(spreadTypeName);
|
|
123
162
|
if (!spreadTypeProps || spreadTypeProps.size === 0) {
|
|
@@ -170,6 +209,131 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
|
|
|
170
209
|
}
|
|
171
210
|
return false;
|
|
172
211
|
};
|
|
212
|
+
/**
|
|
213
|
+
* Records which props an ObjectPattern consumes into `used`. A rest element
|
|
214
|
+
* (`...rest`) consumes all remaining props, so it flips `restUsed` and also
|
|
215
|
+
* marks spread-type keys used. Renamed bindings (`{ a: localA }`) still mark
|
|
216
|
+
* the original prop name `a`. Returns whether a rest element was present.
|
|
217
|
+
*/
|
|
218
|
+
const collectUsedFromObjectPattern = (pattern, typeName, used) => {
|
|
219
|
+
let restUsed = false;
|
|
220
|
+
pattern.properties.forEach((prop) => {
|
|
221
|
+
if (prop.type === utils_1.AST_NODE_TYPES.Property) {
|
|
222
|
+
const propName = getStaticPropName(prop.key);
|
|
223
|
+
if (propName) {
|
|
224
|
+
used.add(propName);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
else if (prop.type === utils_1.AST_NODE_TYPES.RestElement &&
|
|
228
|
+
prop.argument.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
229
|
+
// Rest spread (`{...rest}`): all remaining props are forwarded ⇒ used.
|
|
230
|
+
// Spread-type markers are also flagged so the spread-forwarding skip
|
|
231
|
+
// applies; remaining plain props are handled at report time via
|
|
232
|
+
// `restUsed` (the props map may be incomplete here on forward refs).
|
|
233
|
+
restUsed = true;
|
|
234
|
+
const propsType = propsTypes.get(typeName);
|
|
235
|
+
if (propsType) {
|
|
236
|
+
Object.keys(propsType).forEach((key) => {
|
|
237
|
+
if (key.startsWith('...')) {
|
|
238
|
+
used.add(key);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
return restUsed;
|
|
245
|
+
};
|
|
246
|
+
/**
|
|
247
|
+
* Whether an Identifier node is a value-position *reference* to a variable
|
|
248
|
+
* (vs. a declaration binding, an object-property key, or the property of a
|
|
249
|
+
* member expression like `foo.paramName`). Used to detect opaque param use.
|
|
250
|
+
*/
|
|
251
|
+
const isParamReference = (parent, parentKey) => {
|
|
252
|
+
if (!parent) {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
// `foo.paramName` — the identifier is the member's property, not the param.
|
|
256
|
+
if (parent.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
257
|
+
parentKey === 'property' &&
|
|
258
|
+
!parent.computed) {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
// Non-shorthand object property key: `{ paramName: x }`.
|
|
262
|
+
if (parent.type === utils_1.AST_NODE_TYPES.Property &&
|
|
263
|
+
parentKey === 'key' &&
|
|
264
|
+
!parent.computed) {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
// A binding position (declaration id, function param, etc.).
|
|
268
|
+
if ((parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
269
|
+
parentKey === 'id') ||
|
|
270
|
+
parentKey === 'params') {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
return true;
|
|
274
|
+
};
|
|
275
|
+
const collectBodyDestructuring = (body, paramName, typeName, used) => {
|
|
276
|
+
const result = {
|
|
277
|
+
destructureCount: 0,
|
|
278
|
+
restUsed: false,
|
|
279
|
+
hasOpaqueUsage: false,
|
|
280
|
+
};
|
|
281
|
+
const declaresParamName = (declarators) => declarators.some((d) => d.id.type === utils_1.AST_NODE_TYPES.Identifier && d.id.name === paramName);
|
|
282
|
+
/** Identifier nodes that are the `init` of a `const {} = props` declarator. */
|
|
283
|
+
const destructureInits = new Set();
|
|
284
|
+
const visit = (node, parent, parentKey) => {
|
|
285
|
+
if (!node || typeof node.type !== 'string') {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (node.type === utils_1.AST_NODE_TYPES.VariableDeclarator) {
|
|
289
|
+
if (node.id.type === utils_1.AST_NODE_TYPES.ObjectPattern &&
|
|
290
|
+
node.init?.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
291
|
+
node.init.name === paramName) {
|
|
292
|
+
destructureInits.add(node.init);
|
|
293
|
+
result.destructureCount += 1;
|
|
294
|
+
if (collectUsedFromObjectPattern(node.id, typeName, used)) {
|
|
295
|
+
result.restUsed = true;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
// A bare reference to the param that is NOT a destructuring init is an
|
|
300
|
+
// opaque consumption (member access, spread, argument, return, etc.).
|
|
301
|
+
if (node.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
302
|
+
node.name === paramName &&
|
|
303
|
+
!destructureInits.has(node) &&
|
|
304
|
+
isParamReference(parent, parentKey)) {
|
|
305
|
+
result.hasOpaqueUsage = true;
|
|
306
|
+
}
|
|
307
|
+
// Stop descending into nested functions that re-bind `paramName`, since
|
|
308
|
+
// that shadowing binding refers to a different variable.
|
|
309
|
+
if ((node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
310
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
311
|
+
node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) &&
|
|
312
|
+
node.params.some((p) => p.type === utils_1.AST_NODE_TYPES.Identifier && p.name === paramName)) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (node.type === utils_1.AST_NODE_TYPES.VariableDeclaration &&
|
|
316
|
+
declaresParamName(node.declarations)) {
|
|
317
|
+
// A `const <paramName> = ...` rebind shadows the param from here on.
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
for (const key of Object.keys(node)) {
|
|
321
|
+
if (key === 'parent') {
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
const value = node[key];
|
|
325
|
+
if (Array.isArray(value)) {
|
|
326
|
+
value.forEach((child) => visit(child, node, key));
|
|
327
|
+
}
|
|
328
|
+
else if (value &&
|
|
329
|
+
typeof value.type === 'string') {
|
|
330
|
+
visit(value, node, key);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
visit(body, null, null);
|
|
335
|
+
return result;
|
|
336
|
+
};
|
|
173
337
|
const reportUnusedProps = (typeName, used, restUsed) => {
|
|
174
338
|
const propsType = propsTypes.get(typeName);
|
|
175
339
|
if (!propsType) {
|
|
@@ -190,6 +354,11 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
|
|
|
190
354
|
if (isGenericTypeSpread(prop)) {
|
|
191
355
|
shouldReport = false;
|
|
192
356
|
}
|
|
357
|
+
else if (restUsed) {
|
|
358
|
+
// An explicit rest element (`{ a, ...rest }`) captures every prop not
|
|
359
|
+
// named before it, so the remaining props are forwarded ⇒ used.
|
|
360
|
+
shouldReport = false;
|
|
361
|
+
}
|
|
193
362
|
else if (prop.startsWith('...')) {
|
|
194
363
|
shouldReport = !shouldSkipSpreadType(prop, hasRestSpread, used, knownProps);
|
|
195
364
|
}
|
|
@@ -590,45 +759,54 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
|
|
|
590
759
|
}
|
|
591
760
|
},
|
|
592
761
|
VariableDeclaration(node) {
|
|
593
|
-
if (node.declarations.length
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
762
|
+
if (node.declarations.length !== 1) {
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
const declaration = node.declarations[0];
|
|
766
|
+
if (declaration.init?.type !== utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
const fn = declaration.init;
|
|
770
|
+
const param = fn.params[0];
|
|
771
|
+
if (param?.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
|
|
772
|
+
// Resolve through generic wrappers (e.g. `Readonly<FooProps>`) as well
|
|
773
|
+
// as the plain `FooProps` annotation.
|
|
774
|
+
const typeName = resolvePropsTypeName(param.typeAnnotation?.typeAnnotation);
|
|
775
|
+
if (typeName) {
|
|
776
|
+
const used = new Set();
|
|
777
|
+
const restUsed = collectUsedFromObjectPattern(param, typeName, used);
|
|
778
|
+
currentComponent = { node, typeName, used, restUsed };
|
|
779
|
+
}
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
if (param?.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
783
|
+
// Identifier param (`props: FooProps`): the destructuring happens in
|
|
784
|
+
// the body. Resolve the Props type (unwrapping generic wrappers) and
|
|
785
|
+
// scan the body for `const { ... } = props`.
|
|
786
|
+
const typeName = resolvePropsTypeName(param.typeAnnotation?.typeAnnotation);
|
|
787
|
+
if (!typeName) {
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
const used = new Set();
|
|
791
|
+
const { destructureCount, restUsed, hasOpaqueUsage } = collectBodyDestructuring(fn.body, param.name, typeName, used);
|
|
792
|
+
// Only check when the body destructures the param at least once.
|
|
793
|
+
// Member access (`props.x`), spread (`{...props}`), passing `props`
|
|
794
|
+
// whole, or never referencing it (e.g. `_props`) is left to prior
|
|
795
|
+
// behavior (no report) — those usages can't be enumerated reliably.
|
|
796
|
+
if (destructureCount === 0) {
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
// Mixed usage (some destructure + opaque consumption) forwards the
|
|
800
|
+
// remaining props, so treat every prop as used to avoid false reports.
|
|
801
|
+
if (hasOpaqueUsage) {
|
|
802
|
+
const propsType = propsTypes.get(typeName);
|
|
803
|
+
if (propsType) {
|
|
804
|
+
Object.keys(propsType).forEach((key) => used.add(key));
|
|
630
805
|
}
|
|
806
|
+
currentComponent = { node, typeName, used, restUsed: true };
|
|
807
|
+
return;
|
|
631
808
|
}
|
|
809
|
+
currentComponent = { node, typeName, used, restUsed };
|
|
632
810
|
}
|
|
633
811
|
},
|
|
634
812
|
'VariableDeclaration:exit'(node) {
|
|
@@ -13,8 +13,6 @@ const describeChild = (child) => {
|
|
|
13
13
|
return 'JSX element';
|
|
14
14
|
case 'JSXFragment':
|
|
15
15
|
return 'fragment';
|
|
16
|
-
case 'JSXExpressionContainer':
|
|
17
|
-
return 'expression result';
|
|
18
16
|
case 'JSXText':
|
|
19
17
|
return 'text node';
|
|
20
18
|
case 'JSXSpreadChild':
|
|
@@ -29,6 +27,16 @@ exports.noUselessFragment = {
|
|
|
29
27
|
JSXFragment(node) {
|
|
30
28
|
if (node.children.length === 1) {
|
|
31
29
|
const [child] = node.children;
|
|
30
|
+
/**
|
|
31
|
+
* A fragment whose only child is an expression container — e.g.
|
|
32
|
+
* `<>{portal}</>` — is NOT useless. Unwrapping it to a bare
|
|
33
|
+
* `{portal}` is invalid in statement/return position, and wrapping a
|
|
34
|
+
* single ReactNode expression in a fragment is the idiomatic way to
|
|
35
|
+
* render it. (Mirrors the upstream rule's `allowExpressions`.)
|
|
36
|
+
*/
|
|
37
|
+
if (child.type === 'JSXExpressionContainer') {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
32
40
|
context.report({
|
|
33
41
|
node,
|
|
34
42
|
messageId: 'noUselessFragment',
|
|
@@ -113,9 +113,7 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
113
113
|
}
|
|
114
114
|
// Skip non-shorthand keys in object literals as they are not value uses
|
|
115
115
|
if (parent && parent.type === utils_1.AST_NODE_TYPES.Property) {
|
|
116
|
-
if (parent.key === node &&
|
|
117
|
-
!parent.computed &&
|
|
118
|
-
!parent.shorthand) {
|
|
116
|
+
if (parent.key === node && !parent.computed && !parent.shorthand) {
|
|
119
117
|
return false;
|
|
120
118
|
}
|
|
121
119
|
}
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.preferTypeAliasOverTypeofConstant = void 0;
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
6
7
|
const PAREN_TYPE = utils_1.AST_NODE_TYPES.TSParenthesizedType ??
|
|
7
8
|
'TSParenthesizedType';
|
|
8
9
|
const isParenthesizedType = (node) => {
|
|
@@ -87,6 +88,71 @@ function collectReferencedTypeNames(node, acc = new Set()) {
|
|
|
87
88
|
collectReferencedTypeNames(e, acc);
|
|
88
89
|
break;
|
|
89
90
|
}
|
|
91
|
+
case utils_1.AST_NODE_TYPES.TSIndexedAccessType: {
|
|
92
|
+
const idx = node;
|
|
93
|
+
collectReferencedTypeNames(idx.objectType, acc);
|
|
94
|
+
collectReferencedTypeNames(idx.indexType, acc);
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
case utils_1.AST_NODE_TYPES.TSMappedType: {
|
|
98
|
+
const mapped = node;
|
|
99
|
+
if (mapped.typeAnnotation)
|
|
100
|
+
collectReferencedTypeNames(mapped.typeAnnotation, acc);
|
|
101
|
+
if (mapped.nameType)
|
|
102
|
+
collectReferencedTypeNames(mapped.nameType, acc);
|
|
103
|
+
if (mapped.typeParameter && mapped.typeParameter.constraint) {
|
|
104
|
+
collectReferencedTypeNames(mapped.typeParameter.constraint, acc);
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
case utils_1.AST_NODE_TYPES.TSConditionalType: {
|
|
109
|
+
const cond = node;
|
|
110
|
+
collectReferencedTypeNames(cond.checkType, acc);
|
|
111
|
+
collectReferencedTypeNames(cond.extendsType, acc);
|
|
112
|
+
collectReferencedTypeNames(cond.trueType, acc);
|
|
113
|
+
collectReferencedTypeNames(cond.falseType, acc);
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
case utils_1.AST_NODE_TYPES.TSTypeLiteral: {
|
|
117
|
+
const lit = node;
|
|
118
|
+
for (const m of lit.members) {
|
|
119
|
+
if (m.type === utils_1.AST_NODE_TYPES.TSPropertySignature && m.typeAnnotation) {
|
|
120
|
+
collectReferencedTypeNames(m.typeAnnotation.typeAnnotation, acc);
|
|
121
|
+
}
|
|
122
|
+
else if (m.type === utils_1.AST_NODE_TYPES.TSMethodSignature) {
|
|
123
|
+
if (m.returnType) {
|
|
124
|
+
collectReferencedTypeNames(m.returnType.typeAnnotation, acc);
|
|
125
|
+
}
|
|
126
|
+
for (const p of m.params) {
|
|
127
|
+
if (p.typeAnnotation) {
|
|
128
|
+
collectReferencedTypeNames(p.typeAnnotation.typeAnnotation, acc);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else if (m.type === utils_1.AST_NODE_TYPES.TSIndexSignature) {
|
|
133
|
+
if (m.typeAnnotation) {
|
|
134
|
+
collectReferencedTypeNames(m.typeAnnotation.typeAnnotation, acc);
|
|
135
|
+
}
|
|
136
|
+
for (const p of m.parameters) {
|
|
137
|
+
if (p.typeAnnotation) {
|
|
138
|
+
collectReferencedTypeNames(p.typeAnnotation.typeAnnotation, acc);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else if (m.type === utils_1.AST_NODE_TYPES.TSCallSignatureDeclaration ||
|
|
143
|
+
m.type === utils_1.AST_NODE_TYPES.TSConstructSignatureDeclaration) {
|
|
144
|
+
if (m.returnType) {
|
|
145
|
+
collectReferencedTypeNames(m.returnType.typeAnnotation, acc);
|
|
146
|
+
}
|
|
147
|
+
for (const p of m.params) {
|
|
148
|
+
if (p.typeAnnotation) {
|
|
149
|
+
collectReferencedTypeNames(p.typeAnnotation.typeAnnotation, acc);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
90
156
|
default: {
|
|
91
157
|
if (isParenthesizedType(node)) {
|
|
92
158
|
collectReferencedTypeNames(node.typeAnnotation, acc);
|
|
@@ -225,19 +291,15 @@ exports.preferTypeAliasOverTypeofConstant = (0, createRule_1.createRule)({
|
|
|
225
291
|
TSTypeQuery(node) {
|
|
226
292
|
if (!collected)
|
|
227
293
|
return;
|
|
228
|
-
|
|
294
|
+
const ancestors = ASTHelpers_1.ASTHelpers.getAncestors(context, node);
|
|
295
|
+
// Skip if inside a type alias declaration (Issue #1117, #1175)
|
|
229
296
|
// This allows 'type T = typeof CONST' as the canonical way to define the alias.
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (current.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) {
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
current = current.parent;
|
|
297
|
+
if (ancestors.some((a) => a.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration)) {
|
|
298
|
+
return;
|
|
236
299
|
}
|
|
237
|
-
// Skip `keyof typeof X`
|
|
238
|
-
if (
|
|
239
|
-
|
|
240
|
-
node.parent.operator === 'keyof') {
|
|
300
|
+
// Skip `keyof typeof X` as it's a canonical way to derive a union of keys from a constant object.
|
|
301
|
+
if (ancestors.some((a) => a.type === utils_1.AST_NODE_TYPES.TSTypeOperator &&
|
|
302
|
+
a.operator === 'keyof')) {
|
|
241
303
|
return;
|
|
242
304
|
}
|
|
243
305
|
if (node.exprName.type !== utils_1.AST_NODE_TYPES.Identifier) {
|