@blumintinc/eslint-plugin-blumint 1.14.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 +36 -14
- package/lib/index.js +20 -12
- package/lib/rules/avoid-utils-directory.js +0 -4
- package/lib/rules/consistent-callback-naming.js +68 -3
- package/lib/rules/dynamic-https-errors.d.ts +1 -1
- package/lib/rules/dynamic-https-errors.js +119 -49
- package/lib/rules/enforce-boolean-naming-prefixes.d.ts +1 -0
- package/lib/rules/enforce-boolean-naming-prefixes.js +86 -331
- package/lib/rules/enforce-date-ttime.d.ts +1 -0
- package/lib/rules/enforce-date-ttime.js +156 -0
- package/lib/rules/enforce-dynamic-firebase-imports.d.ts +2 -1
- package/lib/rules/enforce-dynamic-firebase-imports.js +3 -2
- package/lib/rules/enforce-dynamic-imports.d.ts +2 -1
- package/lib/rules/enforce-dynamic-imports.js +42 -21
- package/lib/rules/enforce-f-extension-for-entry-points.d.ts +8 -0
- package/lib/rules/enforce-f-extension-for-entry-points.js +283 -0
- package/lib/rules/enforce-global-constants.js +3 -3
- package/lib/rules/enforce-id-capitalization.js +1 -1
- package/lib/rules/enforce-memoize-async.js +66 -15
- package/lib/rules/enforce-mui-rounded-icons.js +42 -1
- package/lib/rules/enforce-props-argument-name.js +42 -16
- package/lib/rules/enforce-stable-hash-spread-props.js +3 -3
- package/lib/rules/enforce-transform-memoization.js +1 -1
- package/lib/rules/enforce-verb-noun-naming.js +3817 -4641
- package/lib/rules/global-const-style.js +25 -4
- package/lib/rules/logical-top-to-bottom-grouping.js +80 -2
- package/lib/rules/memo-compare-deeply-complex-props.js +183 -6
- package/lib/rules/memo-nested-react-components.js +243 -103
- package/lib/rules/no-array-length-in-deps.js +74 -3
- package/lib/rules/no-async-foreach.js +7 -2
- package/lib/rules/no-circular-references.d.ts +2 -1
- package/lib/rules/no-circular-references.js +150 -489
- package/lib/rules/no-compositing-layer-props.js +31 -0
- package/lib/rules/no-console-error.js +12 -10
- package/lib/rules/no-empty-dependency-use-callbacks.js +1 -1
- package/lib/rules/no-entire-object-hook-deps.js +147 -65
- package/lib/rules/no-excessive-parent-chain.js +3 -0
- package/lib/rules/no-explicit-return-type.js +6 -0
- package/lib/rules/no-hungarian.js +119 -24
- package/lib/rules/no-inline-component-prop.js +16 -7
- package/lib/rules/no-margin-properties.js +7 -38
- package/lib/rules/no-passthrough-getters.d.ts +2 -2
- package/lib/rules/no-passthrough-getters.js +83 -1
- package/lib/rules/no-redundant-this-params.js +50 -1
- package/lib/rules/no-unmemoized-memo-without-props.js +1 -1
- package/lib/rules/no-unnecessary-destructuring-rename.js +2 -5
- 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 +117 -54
- package/lib/rules/prefer-nullish-coalescing-boolean-props.js +109 -4
- package/lib/rules/prefer-params-over-parent-id.js +1 -1
- package/lib/rules/prefer-settings-object.js +27 -10
- package/lib/rules/prefer-type-alias-over-typeof-constant.js +75 -4
- package/lib/rules/prefer-use-deep-compare-memo.js +8 -14
- package/lib/rules/prefer-usememo-over-useeffect-usestate.js +1 -1
- package/lib/rules/prevent-children-clobber.js +9 -5
- package/lib/rules/react-memoize-literals.js +218 -13
- package/lib/rules/require-https-error-cause.js +30 -11
- package/lib/rules/require-memo.js +17 -9
- 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 +49 -1
- package/lib/utils/ASTHelpers.js +394 -112
- package/package.json +7 -6
- package/release-manifest.json +166 -0
|
@@ -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',
|
|
@@ -13,6 +13,9 @@ const defaultOptions = [
|
|
|
13
13
|
'updatethreshold',
|
|
14
14
|
'setthreshold',
|
|
15
15
|
'checkthreshold',
|
|
16
|
+
'commit',
|
|
17
|
+
'flush',
|
|
18
|
+
'saveall',
|
|
16
19
|
],
|
|
17
20
|
},
|
|
18
21
|
];
|
|
@@ -95,81 +98,147 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
95
98
|
return null;
|
|
96
99
|
}
|
|
97
100
|
/**
|
|
98
|
-
*
|
|
101
|
+
* Generic AST visitor for identifier collection
|
|
99
102
|
*/
|
|
100
|
-
function
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
103
|
+
function visitIdentifiers(node, callback, options = {}) {
|
|
104
|
+
if (node.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
105
|
+
const parent = node.parent;
|
|
106
|
+
// Skip non-computed properties in MemberExpressions as they are not value uses
|
|
107
|
+
if (parent && parent.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
108
|
+
if (parent.property === node &&
|
|
109
|
+
!parent.computed &&
|
|
110
|
+
!options.includeMemberProperties) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Skip non-shorthand keys in object literals as they are not value uses
|
|
115
|
+
if (parent && parent.type === utils_1.AST_NODE_TYPES.Property) {
|
|
116
|
+
if (parent.key === node && !parent.computed && !parent.shorthand) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
107
119
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
120
|
+
if (callback(node.name) === true)
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Recursively traverses child nodes while skipping 'parent' to avoid
|
|
125
|
+
* circular back-references, and 'range'/'loc' which are metadata.
|
|
126
|
+
*/
|
|
127
|
+
for (const key in node) {
|
|
128
|
+
if (key === 'parent' || key === 'range' || key === 'loc')
|
|
129
|
+
continue;
|
|
130
|
+
const child = node[key];
|
|
131
|
+
if (child && typeof child === 'object') {
|
|
132
|
+
if (Array.isArray(child)) {
|
|
133
|
+
for (const item of child) {
|
|
134
|
+
if (item && typeof item === 'object' && 'type' in item) {
|
|
135
|
+
if (visitIdentifiers(item, callback, options)) {
|
|
136
|
+
return true;
|
|
118
137
|
}
|
|
119
138
|
}
|
|
120
139
|
}
|
|
121
|
-
|
|
122
|
-
|
|
140
|
+
}
|
|
141
|
+
else if ('type' in child) {
|
|
142
|
+
if (visitIdentifiers(child, callback, options)) {
|
|
143
|
+
return true;
|
|
123
144
|
}
|
|
124
145
|
}
|
|
125
146
|
}
|
|
126
147
|
}
|
|
127
|
-
|
|
128
|
-
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Checks if an identifier is used in a node
|
|
152
|
+
*/
|
|
153
|
+
function isIdentifierUsedInNode(identifier, node) {
|
|
154
|
+
return visitIdentifiers(node, (name) => name === identifier);
|
|
129
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* Extracts all identifiers used in a node
|
|
158
|
+
*/
|
|
159
|
+
function getAllIdentifiers(node, options) {
|
|
160
|
+
const identifiers = new Set();
|
|
161
|
+
visitIdentifiers(node, (name) => {
|
|
162
|
+
identifiers.add(name);
|
|
163
|
+
}, options);
|
|
164
|
+
return identifiers;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Matches coordinator-like identifiers. Intentionally uses substring
|
|
168
|
+
* matching (no word boundaries) to catch patterns like "batchManager",
|
|
169
|
+
* "transactionCollector", etc. This may produce false positives for
|
|
170
|
+
* unrelated managers, but errs on the side of safety.
|
|
171
|
+
*/
|
|
172
|
+
const COORDINATOR_PATTERN = /batch|manager|collector|transaction|tx|unitofwork|accumulator/i;
|
|
130
173
|
/**
|
|
131
174
|
* Checks if there are dependencies between await expressions
|
|
132
175
|
*/
|
|
133
176
|
function hasDependencies(awaitNodes, variableNames, sideEffectPatterns) {
|
|
134
|
-
// If we have fewer than 2 nodes, there are no dependencies to check
|
|
135
177
|
if (awaitNodes.length < 2) {
|
|
136
178
|
return false;
|
|
137
179
|
}
|
|
138
|
-
|
|
139
|
-
|
|
180
|
+
const allIdentifiers = awaitNodes.map((node) => {
|
|
181
|
+
const awaitExpr = getAwaitExpression(node);
|
|
182
|
+
return awaitExpr
|
|
183
|
+
? getAllIdentifiers(awaitExpr.argument, {
|
|
184
|
+
includeMemberProperties: true,
|
|
185
|
+
})
|
|
186
|
+
: new Set();
|
|
187
|
+
});
|
|
188
|
+
// Check all nodes for side effects and shared coordinators
|
|
189
|
+
for (let i = 0; i < awaitNodes.length; i++) {
|
|
140
190
|
const currentNode = awaitNodes[i];
|
|
141
|
-
// Check if any previous variable is used in the current await expression
|
|
142
|
-
for (const varName of variableNames) {
|
|
143
|
-
const awaitExpr = getAwaitExpression(currentNode);
|
|
144
|
-
if (awaitExpr &&
|
|
145
|
-
isIdentifierUsedInNode(varName, awaitExpr.argument)) {
|
|
146
|
-
return true;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
// Check for operations that might have side effects that affect subsequent operations
|
|
150
|
-
// This is a conservative heuristic - we only flag very specific patterns
|
|
151
191
|
const awaitExpr = getAwaitExpression(currentNode);
|
|
152
|
-
if (awaitExpr
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const methodName = callee.property.name;
|
|
159
|
-
if (sideEffectPatterns.some((pattern) => pattern.test(methodName))) {
|
|
192
|
+
if (!awaitExpr)
|
|
193
|
+
continue;
|
|
194
|
+
// 1. Check if current node depends on variables DECLARED in previous awaits
|
|
195
|
+
if (i > 0) {
|
|
196
|
+
for (const varName of variableNames) {
|
|
197
|
+
if (isIdentifierUsedInNode(varName, awaitExpr.argument)) {
|
|
160
198
|
return true;
|
|
161
199
|
}
|
|
162
200
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
201
|
+
}
|
|
202
|
+
// 2. Check for shared coordinators between this node and ANY previous node
|
|
203
|
+
if (i > 0) {
|
|
204
|
+
const currentIds = allIdentifiers[i];
|
|
205
|
+
for (const id of currentIds) {
|
|
206
|
+
if (COORDINATOR_PATTERN.test(id)) {
|
|
207
|
+
for (let j = 0; j < i; j++) {
|
|
208
|
+
if (allIdentifiers[j].has(id)) {
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
167
212
|
}
|
|
168
213
|
}
|
|
169
214
|
}
|
|
215
|
+
// 3. Check for operations that might have side effects
|
|
216
|
+
// If any node has a side effect, we should not parallelize the sequence
|
|
217
|
+
let callExpr = null;
|
|
218
|
+
if (awaitExpr.argument.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
219
|
+
callExpr = awaitExpr.argument;
|
|
220
|
+
}
|
|
221
|
+
else if (awaitExpr.argument.type === utils_1.AST_NODE_TYPES.ChainExpression &&
|
|
222
|
+
awaitExpr.argument.expression.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
223
|
+
callExpr = awaitExpr.argument.expression;
|
|
224
|
+
}
|
|
225
|
+
if (callExpr) {
|
|
226
|
+
const callee = callExpr.callee;
|
|
227
|
+
let methodName = null;
|
|
228
|
+
if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
229
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
230
|
+
methodName = callee.property.name;
|
|
231
|
+
}
|
|
232
|
+
else if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
233
|
+
methodName = callee.name;
|
|
234
|
+
}
|
|
235
|
+
if (methodName &&
|
|
236
|
+
sideEffectPatterns.some((pattern) => pattern.test(methodName))) {
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
170
240
|
}
|
|
171
241
|
// If any node is a variable declaration with destructuring, consider it as having dependencies
|
|
172
|
-
// This is because destructuring often creates variables that are used later
|
|
173
242
|
for (const node of awaitNodes) {
|
|
174
243
|
if (node.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
|
|
175
244
|
for (const declaration of node.declarations) {
|
|
@@ -234,7 +303,6 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
234
303
|
function areInTryCatchBlocks(nodes) {
|
|
235
304
|
for (const node of nodes) {
|
|
236
305
|
let current = node;
|
|
237
|
-
// Traverse up to find if the node is in a try block
|
|
238
306
|
while (current && current.parent) {
|
|
239
307
|
if (current.parent.type === utils_1.AST_NODE_TYPES.TryStatement &&
|
|
240
308
|
current.parent.block === current) {
|
|
@@ -253,7 +321,6 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
253
321
|
function areInLoop(nodes) {
|
|
254
322
|
for (const node of nodes) {
|
|
255
323
|
let current = node;
|
|
256
|
-
// Traverse up to find if the node is in a loop
|
|
257
324
|
while (current && current.parent) {
|
|
258
325
|
if (current.parent.type === utils_1.AST_NODE_TYPES.ForStatement ||
|
|
259
326
|
current.parent.type === utils_1.AST_NODE_TYPES.ForInStatement ||
|
|
@@ -276,14 +343,12 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
276
343
|
if (awaitNodes.length < 2) {
|
|
277
344
|
return null;
|
|
278
345
|
}
|
|
279
|
-
// Extract the await expressions
|
|
280
346
|
const awaitExpressions = awaitNodes
|
|
281
347
|
.map((node) => getAwaitExpression(node))
|
|
282
348
|
.filter((node) => node !== null);
|
|
283
349
|
if (awaitExpressions.length < 2) {
|
|
284
350
|
return null;
|
|
285
351
|
}
|
|
286
|
-
// Get the text of each await argument
|
|
287
352
|
const awaitArguments = awaitExpressions.map((expr) => sourceCode.getText(expr.argument));
|
|
288
353
|
const idsText = [];
|
|
289
354
|
const declKinds = new Set();
|
|
@@ -316,9 +381,7 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
316
381
|
// Simple Promise.all without variable assignments
|
|
317
382
|
promiseAllText = `await Promise.all([\n ${awaitArguments.join(',\n ')}\n]);`;
|
|
318
383
|
}
|
|
319
|
-
// Find the start position, accounting for leading comments
|
|
320
384
|
const startPos = awaitNodes[0].range[0];
|
|
321
|
-
// Replace the range from the start of the first await to the end of the last await
|
|
322
385
|
const endPos = awaitNodes[awaitNodes.length - 1].range[1];
|
|
323
386
|
return fixer.replaceTextRange([startPos, endPos], promiseAllText);
|
|
324
387
|
}
|