@blumintinc/eslint-plugin-blumint 1.20.145 → 1.20.147
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-memoize-async.js +311 -0
- package/lib/rules/no-entire-object-hook-deps.js +266 -3
- package/lib/rules/prefer-use-deep-compare-memo.js +307 -19
- package/lib/rules/prevent-children-clobber.js +297 -35
- package/lib/rules/vertically-group-related-functions.js +154 -0
- package/package.json +1 -1
- package/release-manifest.json +52 -0
|
@@ -103,33 +103,205 @@ function aliasDeclarationNamed(statement, name) {
|
|
|
103
103
|
: undefined;
|
|
104
104
|
}
|
|
105
105
|
/**
|
|
106
|
-
*
|
|
107
|
-
* an
|
|
106
|
+
* The string members of `const NAME = ['a', 'b'] as const`, or `undefined` when
|
|
107
|
+
* the statement declares something else or an element is not a string literal.
|
|
108
|
+
*
|
|
109
|
+
* `prefer-union-from-const-array` rewrites a string-literal union alias into
|
|
110
|
+
* exactly this pair of declarations plus `(typeof NAME)[number]`, so a keep-list
|
|
111
|
+
* that reads decidable before that transform has to stay decidable after it.
|
|
112
|
+
*/
|
|
113
|
+
function constArrayMembersNamed(statement, name) {
|
|
114
|
+
const declared = (0, lexicalScope_1.declarationOf)(statement);
|
|
115
|
+
if (declared.type !== utils_1.AST_NODE_TYPES.VariableDeclaration ||
|
|
116
|
+
declared.kind !== 'const') {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
for (const declarator of declared.declarations) {
|
|
120
|
+
if (declarator.id.type !== utils_1.AST_NODE_TYPES.Identifier ||
|
|
121
|
+
declarator.id.name !== name ||
|
|
122
|
+
declarator.init?.type !== utils_1.AST_NODE_TYPES.TSAsExpression) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const { expression, typeAnnotation } = declarator.init;
|
|
126
|
+
// Without `as const` the elements widen to `string`, and the indexed access
|
|
127
|
+
// yields `string` rather than a union of the literals.
|
|
128
|
+
const isConstAssertion = typeAnnotation.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
129
|
+
typeAnnotation.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
130
|
+
typeAnnotation.typeName.name === 'const';
|
|
131
|
+
if (!isConstAssertion || expression.type !== utils_1.AST_NODE_TYPES.ArrayExpression)
|
|
132
|
+
return undefined;
|
|
133
|
+
const members = [];
|
|
134
|
+
for (const element of expression.elements) {
|
|
135
|
+
if (element?.type !== utils_1.AST_NODE_TYPES.Literal ||
|
|
136
|
+
typeof element.value !== 'string') {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
members.push(element.value);
|
|
140
|
+
}
|
|
141
|
+
return members;
|
|
142
|
+
}
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Lexical lookup over pre-collected statement lists, innermost first, so an
|
|
147
|
+
* inner declaration shadows a same-named outer one.
|
|
108
148
|
*
|
|
109
149
|
* Resolution is by scope rather than by a map built during traversal because
|
|
110
150
|
* type aliases hoist: a component declared above its own props alias must still
|
|
111
151
|
* resolve it.
|
|
112
152
|
*/
|
|
113
153
|
function aliasResolverFrom(lists, startIndex = 0) {
|
|
114
|
-
return
|
|
115
|
-
|
|
116
|
-
for (
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
154
|
+
return {
|
|
155
|
+
typeAlias(name) {
|
|
156
|
+
for (let index = startIndex; index < lists.length; index += 1) {
|
|
157
|
+
for (const statement of lists[index]) {
|
|
158
|
+
const declaration = aliasDeclarationNamed(statement, name);
|
|
159
|
+
if (declaration) {
|
|
160
|
+
return {
|
|
161
|
+
typeNode: declaration.typeAnnotation,
|
|
162
|
+
resolve: aliasResolverFrom(lists, index),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
123
165
|
}
|
|
124
166
|
}
|
|
125
|
-
|
|
126
|
-
|
|
167
|
+
return undefined;
|
|
168
|
+
},
|
|
169
|
+
constArrayKeys(name) {
|
|
170
|
+
for (let index = startIndex; index < lists.length; index += 1) {
|
|
171
|
+
for (const statement of lists[index]) {
|
|
172
|
+
const members = constArrayMembersNamed(statement, name);
|
|
173
|
+
if (members)
|
|
174
|
+
return members;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return undefined;
|
|
178
|
+
},
|
|
127
179
|
};
|
|
128
180
|
}
|
|
129
181
|
function aliasResolverAt(from) {
|
|
130
182
|
return aliasResolverFrom((0, lexicalScope_1.enclosingStatementLists)(from));
|
|
131
183
|
}
|
|
132
|
-
|
|
184
|
+
/**
|
|
185
|
+
* The property names a `Pick<T, K>` keep-list names, or `null` when the list is
|
|
186
|
+
* not decidable from syntax alone.
|
|
187
|
+
*
|
|
188
|
+
* `null` means "prove nothing", never "keeps nothing": a keep-list spelled as a
|
|
189
|
+
* type parameter, `keyof T`, `string`, or a template-literal type can include
|
|
190
|
+
* `children`, and treating an undecidable list as an empty one would exempt
|
|
191
|
+
* exactly those.
|
|
192
|
+
*/
|
|
193
|
+
function stringLiteralKeySet(node, resolveAlias, seen = new Set()) {
|
|
194
|
+
if (node.type === utils_1.AST_NODE_TYPES.TSLiteralType) {
|
|
195
|
+
return node.literal.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
196
|
+
typeof node.literal.value === 'string'
|
|
197
|
+
? new Set([node.literal.value])
|
|
198
|
+
: null;
|
|
199
|
+
}
|
|
200
|
+
if (node.type === utils_1.AST_NODE_TYPES.TSUnionType) {
|
|
201
|
+
const keys = new Set();
|
|
202
|
+
for (const member of node.types) {
|
|
203
|
+
const memberKeys = stringLiteralKeySet(member, resolveAlias, seen);
|
|
204
|
+
if (!memberKeys)
|
|
205
|
+
return null;
|
|
206
|
+
for (const key of memberKeys) {
|
|
207
|
+
keys.add(key);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return keys;
|
|
211
|
+
}
|
|
212
|
+
// `(typeof KEYS)[number]` over a `const KEYS = [...] as const` names exactly
|
|
213
|
+
// the array's members — the spelling `prefer-union-from-const-array` rewrites
|
|
214
|
+
// a literal union alias into.
|
|
215
|
+
if (node.type === utils_1.AST_NODE_TYPES.TSIndexedAccessType &&
|
|
216
|
+
node.indexType.type === utils_1.AST_NODE_TYPES.TSNumberKeyword &&
|
|
217
|
+
node.objectType.type === utils_1.AST_NODE_TYPES.TSTypeQuery &&
|
|
218
|
+
node.objectType.exprName.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
219
|
+
const members = resolveAlias?.constArrayKeys(node.objectType.exprName.name);
|
|
220
|
+
return members ? new Set(members) : null;
|
|
221
|
+
}
|
|
222
|
+
// A bare alias standing for a literal union (`type Keys = 'sx' | 'size'`) is
|
|
223
|
+
// as decidable as the union written inline. A parameterized reference is not:
|
|
224
|
+
// it is a computed key list whose members this rule cannot enumerate.
|
|
225
|
+
if (node.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
226
|
+
node.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
227
|
+
!node.typeParameters) {
|
|
228
|
+
const name = node.typeName.name;
|
|
229
|
+
if (!resolveAlias || seen.has(name))
|
|
230
|
+
return null;
|
|
231
|
+
const alias = resolveAlias.typeAlias(name);
|
|
232
|
+
if (!alias)
|
|
233
|
+
return null;
|
|
234
|
+
// Guarding before the recursion terminates a self-referential alias.
|
|
235
|
+
seen.add(name);
|
|
236
|
+
return stringLiteralKeySet(alias.typeNode, alias.resolve, seen);
|
|
237
|
+
}
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Whether a closed object type provably declares no `propertyName` member.
|
|
242
|
+
*
|
|
243
|
+
* An index signature reopens the type — `{ [key: string]: unknown }` admits
|
|
244
|
+
* `children` — and so does a computed key the rule cannot evaluate, since the
|
|
245
|
+
* constant behind `[KEY]` may well be `'children'`. Both surrender the proof.
|
|
246
|
+
*/
|
|
247
|
+
function typeLiteralExcludesProperty(node, propertyName) {
|
|
248
|
+
return node.members.every((member) => {
|
|
249
|
+
if (member.type === utils_1.AST_NODE_TYPES.TSIndexSignature)
|
|
250
|
+
return false;
|
|
251
|
+
if (member.type !== utils_1.AST_NODE_TYPES.TSPropertySignature &&
|
|
252
|
+
member.type !== utils_1.AST_NODE_TYPES.TSMethodSignature) {
|
|
253
|
+
// Call and construct signatures declare no named member.
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
const key = member.key;
|
|
257
|
+
if (member.computed) {
|
|
258
|
+
return (key.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
259
|
+
String(key.value) !== propertyName);
|
|
260
|
+
}
|
|
261
|
+
if (key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
262
|
+
return key.name !== propertyName;
|
|
263
|
+
}
|
|
264
|
+
if (key.type === utils_1.AST_NODE_TYPES.Literal) {
|
|
265
|
+
return String(key.value) !== propertyName;
|
|
266
|
+
}
|
|
267
|
+
return false;
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Generic wrappers that re-map a type's own members without contributing any of
|
|
272
|
+
* their own, so their argument still stands for the whole props type.
|
|
273
|
+
*
|
|
274
|
+
* `PropsWithChildren` is the counter-example the list exists for: it ADDS
|
|
275
|
+
* `children`, so a proof about its argument says nothing about the wrapper.
|
|
276
|
+
*/
|
|
277
|
+
const PROPERTY_PRESERVING_WRAPPERS = new Set([
|
|
278
|
+
'Readonly',
|
|
279
|
+
'Required',
|
|
280
|
+
'Partial',
|
|
281
|
+
'NonNullable',
|
|
282
|
+
]);
|
|
283
|
+
function wrapperNameOf(typeName) {
|
|
284
|
+
if (typeName.type === utils_1.AST_NODE_TYPES.Identifier)
|
|
285
|
+
return typeName.name;
|
|
286
|
+
if (typeName.type === utils_1.AST_NODE_TYPES.TSQualifiedName) {
|
|
287
|
+
return typeName.right.type === utils_1.AST_NODE_TYPES.Identifier
|
|
288
|
+
? typeName.right.name
|
|
289
|
+
: null;
|
|
290
|
+
}
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Whether a type node provably lacks `propertyName`.
|
|
295
|
+
*
|
|
296
|
+
* `closedLiteralCounts` records whether this position IS the props type rather
|
|
297
|
+
* than merely a part of it. Two of the proofs below — a `Pick<>` keep-list and
|
|
298
|
+
* a closed object literal — describe the type they sit on, so they only carry
|
|
299
|
+
* to the binding when nothing can add members on the way back out. The blanket
|
|
300
|
+
* recursion through a type reference's arguments is where that distinction
|
|
301
|
+
* bites: `PropsWithChildren<{ sx?: string }>` and `Record<string, { a: number }>`
|
|
302
|
+
* both wrap a children-free argument in something that admits `children`.
|
|
303
|
+
*/
|
|
304
|
+
function typeNodeExcludesProperty(node, propertyName, resolveAlias, seen = new Set(), closedLiteralCounts = false) {
|
|
133
305
|
if (node.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
|
|
134
306
|
const typeName = node.typeName.type === utils_1.AST_NODE_TYPES.Identifier
|
|
135
307
|
? node.typeName.name
|
|
@@ -141,34 +313,62 @@ function typeNodeExcludesProperty(node, propertyName, resolveAlias, seen = new S
|
|
|
141
313
|
return true;
|
|
142
314
|
}
|
|
143
315
|
}
|
|
316
|
+
// A keep-list is a stronger guarantee than an omit-list: `Pick` drops every
|
|
317
|
+
// member it does not name, so a fully decidable list without `propertyName`
|
|
318
|
+
// excludes it outright.
|
|
319
|
+
if (closedLiteralCounts &&
|
|
320
|
+
typeName === 'Pick' &&
|
|
321
|
+
node.typeParameters?.params?.[1]) {
|
|
322
|
+
const kept = stringLiteralKeySet(node.typeParameters.params[1], resolveAlias);
|
|
323
|
+
if (kept && !kept.has(propertyName)) {
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
144
327
|
if (node.typeParameters?.params) {
|
|
145
|
-
|
|
328
|
+
const argumentsAreTheType = closedLiteralCounts &&
|
|
329
|
+
PROPERTY_PRESERVING_WRAPPERS.has(wrapperNameOf(node.typeName) ?? '');
|
|
330
|
+
return node.typeParameters.params.some((param) => typeNodeExcludesProperty(param, propertyName, resolveAlias, seen, argumentsAreTheType));
|
|
146
331
|
}
|
|
147
|
-
|
|
148
|
-
|
|
332
|
+
// Keyed by position as well as by name: the same alias proves different
|
|
333
|
+
// things in a props position than inside an arbitrary generic, so a visit
|
|
334
|
+
// in one position must not suppress the visit in the other.
|
|
335
|
+
const seenKey = `${closedLiteralCounts ? 'props' : 'part'}:${typeName}`;
|
|
336
|
+
if (typeName && resolveAlias && !seen.has(seenKey)) {
|
|
337
|
+
const alias = resolveAlias.typeAlias(typeName);
|
|
149
338
|
if (alias) {
|
|
150
339
|
// Guarding before the recursion terminates a self-referential alias.
|
|
151
|
-
seen.add(
|
|
152
|
-
if (typeNodeExcludesProperty(alias.typeNode, propertyName, alias.resolve, seen)) {
|
|
340
|
+
seen.add(seenKey);
|
|
341
|
+
if (typeNodeExcludesProperty(alias.typeNode, propertyName, alias.resolve, seen, closedLiteralCounts)) {
|
|
153
342
|
return true;
|
|
154
343
|
}
|
|
155
344
|
}
|
|
156
345
|
}
|
|
157
346
|
}
|
|
347
|
+
if (node.type === utils_1.AST_NODE_TYPES.TSTypeLiteral) {
|
|
348
|
+
return (closedLiteralCounts && typeLiteralExcludesProperty(node, propertyName));
|
|
349
|
+
}
|
|
158
350
|
if (node.type === utils_1.AST_NODE_TYPES.TSUnionType) {
|
|
159
|
-
return node.types.every((typeNode) => typeNodeExcludesProperty(typeNode, propertyName, resolveAlias, seen));
|
|
351
|
+
return node.types.every((typeNode) => typeNodeExcludesProperty(typeNode, propertyName, resolveAlias, seen, closedLiteralCounts));
|
|
160
352
|
}
|
|
161
353
|
if (node.type === utils_1.AST_NODE_TYPES.TSIntersectionType) {
|
|
162
|
-
return node.types.every((typeNode) => typeNodeExcludesProperty(typeNode, propertyName, resolveAlias, seen));
|
|
354
|
+
return node.types.every((typeNode) => typeNodeExcludesProperty(typeNode, propertyName, resolveAlias, seen, closedLiteralCounts));
|
|
163
355
|
}
|
|
164
356
|
return false;
|
|
165
357
|
}
|
|
166
|
-
|
|
167
|
-
|
|
358
|
+
/**
|
|
359
|
+
* Entry point for a type node that stands for a whole props type, whether it
|
|
360
|
+
* was written as a parameter annotation or supplied as a `forwardRef` type
|
|
361
|
+
* argument.
|
|
362
|
+
*/
|
|
363
|
+
function propsTypeNodeExcludesProperty(typeNode, propertyName, resolveAlias) {
|
|
364
|
+
if (!typeNode)
|
|
168
365
|
return false;
|
|
169
|
-
return typeNodeExcludesProperty(
|
|
366
|
+
return typeNodeExcludesProperty(typeNode, propertyName, resolveAlias, new Set(), true);
|
|
367
|
+
}
|
|
368
|
+
function typeAnnotationExcludesProperty(annotation, propertyName, resolveAlias) {
|
|
369
|
+
return propsTypeNodeExcludesProperty(annotation?.typeAnnotation, propertyName, resolveAlias);
|
|
170
370
|
}
|
|
171
|
-
function collectRestBindingsFromPattern(pattern, ctx,
|
|
371
|
+
function collectRestBindingsFromPattern(pattern, ctx, typeNode, resolveAlias, sourceChildrenSourceId) {
|
|
172
372
|
const childrenPresent = patternHasChildrenProperty(pattern);
|
|
173
373
|
for (const prop of pattern.properties) {
|
|
174
374
|
if (prop.type === utils_1.AST_NODE_TYPES.RestElement &&
|
|
@@ -177,7 +377,7 @@ function collectRestBindingsFromPattern(pattern, ctx, annotation, resolveAlias,
|
|
|
177
377
|
ctx.bindings.set(prop.argument.name, {
|
|
178
378
|
identifier: prop.argument,
|
|
179
379
|
childrenExcluded: childrenPresent,
|
|
180
|
-
typeAnnotationExcludesProperty:
|
|
380
|
+
typeAnnotationExcludesProperty: propsTypeNodeExcludesProperty(typeNode, 'children', resolveAlias),
|
|
181
381
|
childrenSourceId: sourceChildrenSourceId ?? prop.argument.name,
|
|
182
382
|
});
|
|
183
383
|
}
|
|
@@ -212,13 +412,31 @@ function recordChildrenValueBindingsFromPattern(pattern, ctx, sourceChildrenSour
|
|
|
212
412
|
}
|
|
213
413
|
}
|
|
214
414
|
}
|
|
215
|
-
|
|
415
|
+
/**
|
|
416
|
+
* The props type a parameter carries: its own annotation when it has one, and
|
|
417
|
+
* otherwise the type the call site supplies contextually.
|
|
418
|
+
*
|
|
419
|
+
* An explicit annotation always wins. It is the type the body is checked
|
|
420
|
+
* against, so a `forwardRef<E, Clean>((props: MenuProps, ref) => …)` still
|
|
421
|
+
* carries whatever `MenuProps` declares.
|
|
422
|
+
*/
|
|
423
|
+
function propsTypeNodeOf(param, contextualTypeNode) {
|
|
424
|
+
// A `TSParameterProperty` (a constructor's `private x: T`) carries its
|
|
425
|
+
// annotation on the parameter it wraps and never takes part in a component's
|
|
426
|
+
// props, so it simply has no props type of its own.
|
|
427
|
+
const own = param.type === utils_1.AST_NODE_TYPES.TSParameterProperty
|
|
428
|
+
? undefined
|
|
429
|
+
: param.typeAnnotation?.typeAnnotation;
|
|
430
|
+
return own ?? contextualTypeNode;
|
|
431
|
+
}
|
|
432
|
+
function recordParamBindings(param, ctx, resolveAlias, contextualTypeNode) {
|
|
433
|
+
const typeNode = propsTypeNodeOf(param, contextualTypeNode);
|
|
216
434
|
if (param.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
217
435
|
ctx.propsLikeIdentifiers.add(param.name);
|
|
218
436
|
ctx.bindings.set(param.name, {
|
|
219
437
|
identifier: param,
|
|
220
438
|
childrenExcluded: false,
|
|
221
|
-
typeAnnotationExcludesProperty:
|
|
439
|
+
typeAnnotationExcludesProperty: propsTypeNodeExcludesProperty(typeNode, 'children', resolveAlias),
|
|
222
440
|
childrenSourceId: param.name,
|
|
223
441
|
});
|
|
224
442
|
return;
|
|
@@ -229,20 +447,61 @@ function recordParamBindings(param, ctx, resolveAlias) {
|
|
|
229
447
|
ctx.bindings.set(param.left.name, {
|
|
230
448
|
identifier: param.left,
|
|
231
449
|
childrenExcluded: false,
|
|
232
|
-
typeAnnotationExcludesProperty:
|
|
450
|
+
typeAnnotationExcludesProperty: propsTypeNodeExcludesProperty(typeNode, 'children', resolveAlias),
|
|
233
451
|
childrenSourceId: param.left.name,
|
|
234
452
|
});
|
|
235
453
|
return;
|
|
236
454
|
}
|
|
237
455
|
if (param.type === utils_1.AST_NODE_TYPES.AssignmentPattern &&
|
|
238
456
|
param.left.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
|
|
239
|
-
collectRestBindingsFromPattern(param.left, ctx,
|
|
457
|
+
collectRestBindingsFromPattern(param.left, ctx, typeNode, resolveAlias);
|
|
240
458
|
return;
|
|
241
459
|
}
|
|
242
460
|
if (param.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
|
|
243
|
-
collectRestBindingsFromPattern(param, ctx,
|
|
461
|
+
collectRestBindingsFromPattern(param, ctx, typeNode, resolveAlias);
|
|
244
462
|
}
|
|
245
463
|
}
|
|
464
|
+
/**
|
|
465
|
+
* The props type a `forwardRef<Element, Props>(…)` call supplies to its render
|
|
466
|
+
* callback, whose parameters carry no annotation of their own.
|
|
467
|
+
*
|
|
468
|
+
* Without this the props type is invisible in the `forwardRef` spelling, so
|
|
469
|
+
* even the documented `Omit<…, 'children'>` remedy could not be seen and the
|
|
470
|
+
* rule reported on props it could prove children-free in every other spelling.
|
|
471
|
+
*/
|
|
472
|
+
function forwardRefPropsTypeNode(node) {
|
|
473
|
+
const call = node.parent;
|
|
474
|
+
if (call?.type !== utils_1.AST_NODE_TYPES.CallExpression)
|
|
475
|
+
return undefined;
|
|
476
|
+
if (call.arguments[0] !== node)
|
|
477
|
+
return undefined;
|
|
478
|
+
// An optional call detaches the type arguments onto a
|
|
479
|
+
// `TSInstantiationExpression` — `forwardRef<E, P>?.(…)` parses as
|
|
480
|
+
// instantiate-then-call — so both the callee and the type arguments are read
|
|
481
|
+
// through it. The node predates the `LeftHandSideExpression` union this
|
|
482
|
+
// version declares, hence the hand-written narrowing.
|
|
483
|
+
const rawCallee = call.callee;
|
|
484
|
+
const instantiation = rawCallee.type === utils_1.AST_NODE_TYPES.TSInstantiationExpression
|
|
485
|
+
? rawCallee
|
|
486
|
+
: undefined;
|
|
487
|
+
const callee = instantiation?.expression ?? call.callee;
|
|
488
|
+
const calleeName = callee.type === utils_1.AST_NODE_TYPES.Identifier
|
|
489
|
+
? callee.name
|
|
490
|
+
: callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
491
|
+
!callee.computed &&
|
|
492
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier
|
|
493
|
+
? callee.property.name
|
|
494
|
+
: null;
|
|
495
|
+
if (calleeName !== 'forwardRef')
|
|
496
|
+
return undefined;
|
|
497
|
+
// `typeParameters` is where @typescript-eslint/utils 5 puts a call's type
|
|
498
|
+
// arguments; `typeArguments` is the name later majors use. Reading both keeps
|
|
499
|
+
// this working across the rename.
|
|
500
|
+
const instantiated = (instantiation ?? call);
|
|
501
|
+
const typeArguments = instantiated.typeParameters ?? instantiated.typeArguments;
|
|
502
|
+
// The first type argument is the element type; the props type is the second.
|
|
503
|
+
return typeArguments?.params?.[1];
|
|
504
|
+
}
|
|
246
505
|
function findNearestComponentContext(stack) {
|
|
247
506
|
for (let i = stack.length - 1; i >= 0; i -= 1) {
|
|
248
507
|
if (stack[i].isComponent)
|
|
@@ -460,9 +719,12 @@ exports.preventChildrenClobber = (0, createRule_1.createRule)({
|
|
|
460
719
|
// ESLint has assigned `parent` up the chain by the time this visitor
|
|
461
720
|
// runs, but not yet onto the parameter's own type annotation.
|
|
462
721
|
const resolveAlias = aliasResolverAt(node);
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
722
|
+
// Only the first parameter receives the contextual props type; the
|
|
723
|
+
// second is the forwarded ref.
|
|
724
|
+
const contextualPropsType = forwardRefPropsTypeNode(node);
|
|
725
|
+
node.params.forEach((param, index) => {
|
|
726
|
+
recordParamBindings(param, ctx, resolveAlias, index === 0 ? contextualPropsType : undefined);
|
|
727
|
+
});
|
|
466
728
|
}
|
|
467
729
|
functionStack.push(ctx);
|
|
468
730
|
},
|
|
@@ -528,7 +790,7 @@ exports.preventChildrenClobber = (0, createRule_1.createRule)({
|
|
|
528
790
|
const initBinding = findBinding(init.name, functionStack);
|
|
529
791
|
const sourceChildrenSourceId = initBinding?.childrenSourceId ?? init.name;
|
|
530
792
|
recordChildrenValueBindingsFromPattern(id, componentCtx, sourceChildrenSourceId);
|
|
531
|
-
collectRestBindingsFromPattern(id, componentCtx, id.typeAnnotation ?? null, aliasResolverAt(node), sourceChildrenSourceId);
|
|
793
|
+
collectRestBindingsFromPattern(id, componentCtx, id.typeAnnotation?.typeAnnotation ?? null, aliasResolverAt(node), sourceChildrenSourceId);
|
|
532
794
|
}
|
|
533
795
|
},
|
|
534
796
|
JSXElement(node) {
|
|
@@ -422,6 +422,109 @@ function collectReferencedNames(node) {
|
|
|
422
422
|
visit(node, null);
|
|
423
423
|
return names;
|
|
424
424
|
}
|
|
425
|
+
// Type-space shells whose contents are erased at runtime. A reference inside
|
|
426
|
+
// one (`const x: ReturnType<typeof buildHit> = …`) never reads the binding at
|
|
427
|
+
// module evaluation, so it must not count as an eager reference — counting it
|
|
428
|
+
// would withhold autofixes that are perfectly safe to apply.
|
|
429
|
+
const TYPE_SPACE_NODE_TYPES = new Set([
|
|
430
|
+
'TSTypeAnnotation',
|
|
431
|
+
'TSTypeParameterInstantiation',
|
|
432
|
+
'TSTypeParameterDeclaration',
|
|
433
|
+
'TSTypeAliasDeclaration',
|
|
434
|
+
'TSInterfaceDeclaration',
|
|
435
|
+
'TSTypeQuery',
|
|
436
|
+
'TSDeclareFunction',
|
|
437
|
+
]);
|
|
438
|
+
// Names a statement reads at MODULE EVALUATION time. A read inside a function
|
|
439
|
+
// body is deferred to call time and constrains nothing — except when that
|
|
440
|
+
// function is an immediately invoked callee, which runs during evaluation. The
|
|
441
|
+
// distinction is the whole point of this rule: callers referencing helpers
|
|
442
|
+
// inside their bodies is the safe pattern being enforced, while an initializer
|
|
443
|
+
// calling a helper runs the moment the module loads.
|
|
444
|
+
function collectEagerlyReferencedNames(node) {
|
|
445
|
+
const names = new Set();
|
|
446
|
+
const visit = (current, parent) => {
|
|
447
|
+
if (!current || !ASTHelpers_1.ASTHelpers.isNode(current)) {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
if (TYPE_SPACE_NODE_TYPES.has(current.type)) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
// An `as`/`satisfies` wrapper evaluates only its expression; the type side
|
|
454
|
+
// is erased.
|
|
455
|
+
if (current.type === 'TSAsExpression' ||
|
|
456
|
+
current.type === 'TSSatisfiesExpression') {
|
|
457
|
+
visit(current.expression, current);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (current.type === 'FunctionDeclaration' ||
|
|
461
|
+
current.type === 'FunctionExpression' ||
|
|
462
|
+
current.type === 'ArrowFunctionExpression') {
|
|
463
|
+
// Deferred to call time — unless this function is the callee of the call
|
|
464
|
+
// being visited, which the CallExpression case enters directly.
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (current.type === 'CallExpression' &&
|
|
468
|
+
(current.callee.type === 'ArrowFunctionExpression' ||
|
|
469
|
+
current.callee.type === 'FunctionExpression')) {
|
|
470
|
+
// An IIFE's body runs during module evaluation, so its reads are eager.
|
|
471
|
+
visit(current.callee.body, current.callee);
|
|
472
|
+
current.callee.params.forEach((param) => visit(param, current.callee));
|
|
473
|
+
current.arguments.forEach((argument) => visit(argument, current));
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (current.type === 'ClassDeclaration' ||
|
|
477
|
+
current.type === 'ClassExpression') {
|
|
478
|
+
// A class's heritage clause, computed member keys, static initializers,
|
|
479
|
+
// and static blocks all run at evaluation; method bodies and instance
|
|
480
|
+
// property values wait for construction.
|
|
481
|
+
visit(current.superClass, current);
|
|
482
|
+
current.body.body.forEach((member) => {
|
|
483
|
+
if (member.type === 'StaticBlock') {
|
|
484
|
+
member.body.forEach((statement) => visit(statement, member));
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
if ('computed' in member && member.computed && 'key' in member) {
|
|
488
|
+
visit(member.key, member);
|
|
489
|
+
}
|
|
490
|
+
if (member.type === 'PropertyDefinition' &&
|
|
491
|
+
member.static &&
|
|
492
|
+
member.value) {
|
|
493
|
+
visit(member.value, member);
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
if (current.type === 'Identifier') {
|
|
499
|
+
const isMemberProperty = parent?.type === 'MemberExpression' &&
|
|
500
|
+
parent.property === current &&
|
|
501
|
+
!parent.computed;
|
|
502
|
+
const isObjectKey = parent?.type === 'Property' &&
|
|
503
|
+
parent.key === current &&
|
|
504
|
+
!parent.computed;
|
|
505
|
+
if (!isMemberProperty && !isObjectKey) {
|
|
506
|
+
names.add(current.name);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
Object.values(current).forEach((value) => {
|
|
510
|
+
if (!value || value === current || current.parent === value) {
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
if (Array.isArray(value)) {
|
|
514
|
+
value.forEach((child) => {
|
|
515
|
+
if (ASTHelpers_1.ASTHelpers.isNode(child)) {
|
|
516
|
+
visit(child, current);
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
else if (ASTHelpers_1.ASTHelpers.isNode(value)) {
|
|
521
|
+
visit(value, current);
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
};
|
|
525
|
+
visit(node, null);
|
|
526
|
+
return names;
|
|
527
|
+
}
|
|
425
528
|
// Names bound by an interleaved VALUE declaration (const/let/var) — but not by a
|
|
426
529
|
// reorderable function. Hoisting a function above a value binding it references
|
|
427
530
|
// is a genuine runtime declare-before-use. Type aliases are tracked separately
|
|
@@ -548,6 +651,50 @@ function reorderHoistsFunctionAboveDependency(regionStatements, functionStatemen
|
|
|
548
651
|
}
|
|
549
652
|
return false;
|
|
550
653
|
}
|
|
654
|
+
// Decline (return true) any reorder that would place a const/let/var-declared
|
|
655
|
+
// function BELOW an interleaved statement that reads it at module evaluation
|
|
656
|
+
// time. The fixer pins interleaved statements and swaps functions among their
|
|
657
|
+
// own slots, so a helper `const` can be carried past its own module-scope
|
|
658
|
+
// caller (`const CHAMPION = buildHit(…)`) — the emitted file parses and
|
|
659
|
+
// type-checks but throws `ReferenceError: Cannot access '…' before
|
|
660
|
+
// initialization` the moment anything imports it, and `--fix` reports nothing
|
|
661
|
+
// about the file it just broke. Function declarations hoist, so demoting one
|
|
662
|
+
// past an eager reference stays loadable and is not declined. This is the
|
|
663
|
+
// mirror of reorderHoistsFunctionAboveDependency: that guard protects a moved
|
|
664
|
+
// function from losing a dependency ABOVE it; this one protects a pinned
|
|
665
|
+
// statement from losing its dependency BELOW it.
|
|
666
|
+
function reorderDemotesDeclarationBelowEagerReference(regionStatements, functionStatements, expectedOrderInfos) {
|
|
667
|
+
const variableDeclaredNames = new Set();
|
|
668
|
+
expectedOrderInfos.forEach((info) => {
|
|
669
|
+
const declaration = info.statementNode.type === 'ExportNamedDeclaration'
|
|
670
|
+
? info.statementNode.declaration
|
|
671
|
+
: info.statementNode;
|
|
672
|
+
if (declaration?.type === 'VariableDeclaration') {
|
|
673
|
+
variableDeclaredNames.add(info.name);
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
if (variableDeclaredNames.size === 0) {
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
const placedNames = new Set();
|
|
680
|
+
let slotCursor = 0;
|
|
681
|
+
for (const statement of regionStatements) {
|
|
682
|
+
if (functionStatements.has(statement)) {
|
|
683
|
+
const occupant = expectedOrderInfos[slotCursor];
|
|
684
|
+
slotCursor += 1;
|
|
685
|
+
if (occupant) {
|
|
686
|
+
placedNames.add(occupant.name);
|
|
687
|
+
}
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
for (const name of collectEagerlyReferencedNames(statement)) {
|
|
691
|
+
if (variableDeclaredNames.has(name) && !placedNames.has(name)) {
|
|
692
|
+
return true;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
551
698
|
exports.verticallyGroupRelatedFunctions = (0, createRule_1.createRule)({
|
|
552
699
|
name: 'vertically-group-related-functions',
|
|
553
700
|
meta: {
|
|
@@ -729,6 +876,13 @@ exports.verticallyGroupRelatedFunctions = (0, createRule_1.createRule)({
|
|
|
729
876
|
if (reorderHoistsFunctionAboveDependency(slice, functionStatements, expectedOrderInfos)) {
|
|
730
877
|
return null;
|
|
731
878
|
}
|
|
879
|
+
// The mirror hazard: a const-declared helper carried below a pinned
|
|
880
|
+
// statement that calls it at module evaluation. The emitted file
|
|
881
|
+
// would throw at import time, so the rewrite is withheld; the
|
|
882
|
+
// misorderedFunction report still fires.
|
|
883
|
+
if (reorderDemotesDeclarationBelowEagerReference(slice, functionStatements, expectedOrderInfos)) {
|
|
884
|
+
return null;
|
|
885
|
+
}
|
|
732
886
|
if (!blockContainsOnlyFunctions) {
|
|
733
887
|
// Real modules interleave type aliases, consts, and top-level
|
|
734
888
|
// calls (e.g. `void autoRunIfMain();`) between functions. Rather
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,56 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.147",
|
|
4
|
+
"date": "2026-08-13T06:24:35.769Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "no-entire-object-hook-deps",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1985
|
|
11
|
+
],
|
|
12
|
+
"summary": "stop hoisting a guarded dereference into the dependency array (closes #1985)"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "1.20.146",
|
|
18
|
+
"date": "2026-08-13T00:14:52.842Z",
|
|
19
|
+
"rules": [
|
|
20
|
+
{
|
|
21
|
+
"name": "enforce-memoize-async",
|
|
22
|
+
"changeType": "fix",
|
|
23
|
+
"issues": [
|
|
24
|
+
1975
|
|
25
|
+
],
|
|
26
|
+
"summary": "exempt methods taking part in a transaction (closes #1975)"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "prefer-use-deep-compare-memo",
|
|
30
|
+
"changeType": "fix",
|
|
31
|
+
"issues": [
|
|
32
|
+
1979
|
|
33
|
+
],
|
|
34
|
+
"summary": "stop promoting primitive deps via member access (closes #1979)"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "prevent-children-clobber",
|
|
38
|
+
"changeType": "fix",
|
|
39
|
+
"issues": [
|
|
40
|
+
1980
|
|
41
|
+
],
|
|
42
|
+
"summary": "widen the syntactic exemption beyond Omit<> (closes #1980)"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"name": "vertically-group-related-functions",
|
|
46
|
+
"changeType": "fix",
|
|
47
|
+
"issues": [
|
|
48
|
+
1983
|
|
49
|
+
],
|
|
50
|
+
"summary": "decline reorders that demote a helper below its module-scope caller (closes #1983)"
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
},
|
|
2
54
|
{
|
|
3
55
|
"version": "1.20.145",
|
|
4
56
|
"date": "2026-08-12T16:05:12.174Z",
|