@blumintinc/eslint-plugin-blumint 1.21.14 → 1.21.16
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-empty-object-check.js +448 -3
- package/lib/rules/enforce-memoize-async.js +439 -0
- package/lib/rules/global-const-style.js +735 -81
- package/lib/rules/no-compositing-layer-props.js +33 -14
- package/lib/rules/require-memo.js +64 -2
- package/lib/utils/composedFixConfig.js +2 -3
- package/lib/utils/docsFixtures.js +3 -3
- package/lib/utils/fixtureCorpus.js +2 -3
- package/lib/utils/loadPlugin.d.ts +41 -0
- package/lib/utils/loadPlugin.js +33 -0
- package/package.json +5 -1
- package/release-manifest.json +81 -0
package/lib/index.js
CHANGED
|
@@ -235,6 +235,234 @@ function isObjectLikeType(type, checker) {
|
|
|
235
235
|
}
|
|
236
236
|
return 'object';
|
|
237
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* Lib wrappers that hand back the shape they are given.
|
|
240
|
+
*
|
|
241
|
+
* `Readonly<T>` only adds modifiers and `Partial<T>` only removes
|
|
242
|
+
* required-ness, so a pinned `T` stays pinned through either, which is what
|
|
243
|
+
* makes `Readonly<Record<string, string>>` readable as the dictionary it is.
|
|
244
|
+
* The unwrapping applies only when the INNER type is itself pinned, so
|
|
245
|
+
* `Readonly<NextResponse>` stays unread and the class instance it names keeps
|
|
246
|
+
* its decline.
|
|
247
|
+
*/
|
|
248
|
+
const SHAPE_PRESERVING_TYPE_WRAPPERS = new Set(['Readonly', 'Partial']);
|
|
249
|
+
/** Type keywords that contribute no shape to a union. */
|
|
250
|
+
const NULLABLE_TYPE_KEYWORDS = new Set([
|
|
251
|
+
utils_1.AST_NODE_TYPES.TSNullKeyword,
|
|
252
|
+
utils_1.AST_NODE_TYPES.TSUndefinedKeyword,
|
|
253
|
+
utils_1.AST_NODE_TYPES.TSVoidKeyword,
|
|
254
|
+
]);
|
|
255
|
+
/** The statements a node holds directly, for a same-file declaration lookup. */
|
|
256
|
+
function statementsOf(node) {
|
|
257
|
+
switch (node.type) {
|
|
258
|
+
case utils_1.AST_NODE_TYPES.Program:
|
|
259
|
+
case utils_1.AST_NODE_TYPES.BlockStatement:
|
|
260
|
+
case utils_1.AST_NODE_TYPES.TSModuleBlock:
|
|
261
|
+
return node.body;
|
|
262
|
+
default:
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* The type a SAME-FILE alias stands for, walking outwards from the reference so
|
|
268
|
+
* that an inner declaration answers ahead of an outer one.
|
|
269
|
+
*
|
|
270
|
+
* Same-file is the whole boundary: an alias imported from a module this program
|
|
271
|
+
* did not load is not found here and stays unpinned, which is the line #2344
|
|
272
|
+
* drew. Following the import instead would need exactly the cross-file
|
|
273
|
+
* resolution whose absence puts this code on the fall-through in the first
|
|
274
|
+
* place.
|
|
275
|
+
*
|
|
276
|
+
* A GENERIC alias is unpinned for a related reason: its body is written against
|
|
277
|
+
* parameters this lookup does not substitute, and each parameter SHADOWS any
|
|
278
|
+
* same-file alias of the same name. Reading `type Wrapper<Cfg> = Readonly<Cfg>`
|
|
279
|
+
* against a sibling `type Cfg = Record<string, string>` calls
|
|
280
|
+
* `Wrapper<NextResponse>` a dictionary and reinstates the inverted guard.
|
|
281
|
+
*/
|
|
282
|
+
function resolveTypeAlias(reference, name) {
|
|
283
|
+
let current = reference;
|
|
284
|
+
while (current) {
|
|
285
|
+
const statements = statementsOf(current);
|
|
286
|
+
if (statements) {
|
|
287
|
+
for (const statement of statements) {
|
|
288
|
+
const declaration = statement.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration &&
|
|
289
|
+
statement.declaration
|
|
290
|
+
? statement.declaration
|
|
291
|
+
: statement;
|
|
292
|
+
if (declaration.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration &&
|
|
293
|
+
declaration.id.name === name) {
|
|
294
|
+
return declaration.typeParameters ? null : declaration.typeAnnotation;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
current = current.parent;
|
|
299
|
+
}
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* The shape an object type literal writes out.
|
|
304
|
+
*
|
|
305
|
+
* The members answer the same question `isObjectLikeType` asks of a resolved
|
|
306
|
+
* type: a required property makes `Object.keys()` non-empty for every valid
|
|
307
|
+
* value, and a call or construct signature marks behaviour rather than data, so
|
|
308
|
+
* either one leaves the literal unpinned.
|
|
309
|
+
*/
|
|
310
|
+
function pinnedShapeOfMembers(members) {
|
|
311
|
+
let shape = 'optional';
|
|
312
|
+
for (const member of members) {
|
|
313
|
+
switch (member.type) {
|
|
314
|
+
case utils_1.AST_NODE_TYPES.TSIndexSignature:
|
|
315
|
+
shape = 'index';
|
|
316
|
+
break;
|
|
317
|
+
case utils_1.AST_NODE_TYPES.TSPropertySignature:
|
|
318
|
+
case utils_1.AST_NODE_TYPES.TSMethodSignature:
|
|
319
|
+
if (!member.optional) {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
break;
|
|
323
|
+
default:
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return shape;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Whether a declared type the checker cannot resolve nonetheless SPELLS a
|
|
331
|
+
* dictionary, and with which shape.
|
|
332
|
+
*
|
|
333
|
+
* A union answers as soon as one member spells one, mirroring
|
|
334
|
+
* `isObjectLikeType`, where any object-like member makes the whole union
|
|
335
|
+
* object-like. An intersection is the opposite and needs every member pinned:
|
|
336
|
+
* `Record<string, string> & SomeClass` carries the class's required members, so
|
|
337
|
+
* a complete program calls it `non-object`, and reading it as a dictionary
|
|
338
|
+
* would recreate the inverted guard the surrounding branch exists to prevent.
|
|
339
|
+
*
|
|
340
|
+
* `expanding` holds the aliases on the current expansion PATH rather than every
|
|
341
|
+
* alias seen, so a self-referential alias terminates while an alias named twice
|
|
342
|
+
* in sibling positions is still read at each of them.
|
|
343
|
+
*/
|
|
344
|
+
function pinnedShapeOf(node, expanding = new Set()) {
|
|
345
|
+
switch (node.type) {
|
|
346
|
+
case utils_1.AST_NODE_TYPES.TSTypeLiteral:
|
|
347
|
+
return pinnedShapeOfMembers(node.members);
|
|
348
|
+
case utils_1.AST_NODE_TYPES.TSUnionType: {
|
|
349
|
+
const shapes = node.types
|
|
350
|
+
.filter((member) => !NULLABLE_TYPE_KEYWORDS.has(member.type))
|
|
351
|
+
.map((member) => pinnedShapeOf(member, expanding));
|
|
352
|
+
if (!shapes.some((shape) => shape !== null)) {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
return shapes.every((shape) => shape === 'index') ? 'index' : 'optional';
|
|
356
|
+
}
|
|
357
|
+
case utils_1.AST_NODE_TYPES.TSIntersectionType: {
|
|
358
|
+
const shapes = node.types.map((member) => pinnedShapeOf(member, expanding));
|
|
359
|
+
if (shapes.some((shape) => shape === null)) {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
return shapes.every((shape) => shape === 'index') ? 'index' : 'optional';
|
|
363
|
+
}
|
|
364
|
+
case utils_1.AST_NODE_TYPES.TSTypeReference: {
|
|
365
|
+
if (node.typeName.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
const { name } = node.typeName;
|
|
369
|
+
/**
|
|
370
|
+
* A same-file alias is read ahead of the lib names, so a file declaring
|
|
371
|
+
* its own `Record` or `Readonly` is answered by what it wrote.
|
|
372
|
+
*/
|
|
373
|
+
const alias = resolveTypeAlias(node, name);
|
|
374
|
+
if (alias) {
|
|
375
|
+
if (expanding.has(alias)) {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
expanding.add(alias);
|
|
379
|
+
const aliased = pinnedShapeOf(alias, expanding);
|
|
380
|
+
expanding.delete(alias);
|
|
381
|
+
return aliased;
|
|
382
|
+
}
|
|
383
|
+
if (name === 'Record') {
|
|
384
|
+
return 'index';
|
|
385
|
+
}
|
|
386
|
+
const args = node.typeParameters?.params ?? [];
|
|
387
|
+
if (args.length !== 1) {
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
if (SHAPE_PRESERVING_TYPE_WRAPPERS.has(name)) {
|
|
391
|
+
return pinnedShapeOf(args[0], expanding);
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* `Required<T>` makes every named member required, so it preserves only a
|
|
395
|
+
* type whose keys come from an index signature. `Required<{ a?: string }>`
|
|
396
|
+
* carries a required property and is `non-object` to a complete program.
|
|
397
|
+
*/
|
|
398
|
+
if (name === 'Required') {
|
|
399
|
+
return pinnedShapeOf(args[0], expanding) === 'index' ? 'index' : null;
|
|
400
|
+
}
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
default:
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
/** Wrappers whose awaited value is the single argument they carry. */
|
|
408
|
+
const AWAITED_TYPE_WRAPPERS = new Set(['Promise', 'PromiseLike']);
|
|
409
|
+
/**
|
|
410
|
+
* The value an `await` on this declared type produces.
|
|
411
|
+
*
|
|
412
|
+
* Without it an `async` declaration states its shape one wrapper out of reach:
|
|
413
|
+
* `Promise<Record<string, string>>` is not itself a dictionary, so the awaited
|
|
414
|
+
* dictionary would go unread and the guard the rule exists to add would be
|
|
415
|
+
* dropped. A declared type that is not a promise passes through unchanged,
|
|
416
|
+
* because awaiting a plain value yields that same value.
|
|
417
|
+
*/
|
|
418
|
+
function unwrapAwaitedType(node) {
|
|
419
|
+
if (node.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
420
|
+
node.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
421
|
+
AWAITED_TYPE_WRAPPERS.has(node.typeName.name) &&
|
|
422
|
+
node.typeParameters?.params.length === 1) {
|
|
423
|
+
return unwrapAwaitedType(node.typeParameters.params[0]);
|
|
424
|
+
}
|
|
425
|
+
return node;
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Whether an assertion is `as const`, which pins mutability rather than shape.
|
|
429
|
+
*
|
|
430
|
+
* `const` is not a type the source names, so treating it as one would answer
|
|
431
|
+
* `load() as const` with "the source says nothing about the shape" when the
|
|
432
|
+
* asserted expression may state it perfectly well.
|
|
433
|
+
*/
|
|
434
|
+
function isConstAssertion(node) {
|
|
435
|
+
return (node.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
436
|
+
node.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
437
|
+
node.typeName.name === 'const');
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* The return type a class writes on the named member, when it writes one.
|
|
441
|
+
*
|
|
442
|
+
* Only the class's OWN body is read: a return type inherited through `extends`
|
|
443
|
+
* lives in whichever declaration that clause resolves to, and reaching it is
|
|
444
|
+
* the cross-file resolution whose absence puts this code on the fall-through.
|
|
445
|
+
*/
|
|
446
|
+
function returnTypeOfMethod(declaration, name) {
|
|
447
|
+
for (const member of declaration.body.body) {
|
|
448
|
+
if (member.type !== utils_1.AST_NODE_TYPES.MethodDefinition &&
|
|
449
|
+
member.type !== utils_1.AST_NODE_TYPES.PropertyDefinition) {
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
if (member.computed ||
|
|
453
|
+
member.key.type !== utils_1.AST_NODE_TYPES.Identifier ||
|
|
454
|
+
member.key.name !== name) {
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
const value = member.value;
|
|
458
|
+
if (value &&
|
|
459
|
+
(value.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
460
|
+
value.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression)) {
|
|
461
|
+
return value.returnType?.typeAnnotation ?? null;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
238
466
|
/**
|
|
239
467
|
* Reads through an optional chain to the member access or call it holds.
|
|
240
468
|
* `Object?.keys?.(payload)?.length` parses as a single `ChainExpression`
|
|
@@ -1404,6 +1632,197 @@ exports.enforceEmptyObjectCheck = (0, createRule_1.createRule)({
|
|
|
1404
1632
|
}
|
|
1405
1633
|
return false;
|
|
1406
1634
|
}
|
|
1635
|
+
/**
|
|
1636
|
+
* The type the DECLARATION behind this value writes, if it writes one.
|
|
1637
|
+
*
|
|
1638
|
+
* An import is answered ahead of this by `tracesToImport`; what remains
|
|
1639
|
+
* is every place the file states the type of the value a guard tests. A
|
|
1640
|
+
* binding annotation is one of them and not the only one — an annotated
|
|
1641
|
+
* function return, a class method's return and a type assertion state it
|
|
1642
|
+
* just as directly — and reading only the binding left the naming
|
|
1643
|
+
* heuristic deciding alone at six other declaration sites, which is how
|
|
1644
|
+
* `const response = build()` off a `Readonly<NextResponse>` return kept
|
|
1645
|
+
* being rewritten into a guard that holds for every valid value (#2346).
|
|
1646
|
+
*
|
|
1647
|
+
* The annotation has to sit on the BINDING rather than around it: the one
|
|
1648
|
+
* on `const { config }: Props = load()` describes the container, and
|
|
1649
|
+
* reading it as a verdict on a single property would need exactly the
|
|
1650
|
+
* resolution that failed, so a destructured binding keeps the heuristic.
|
|
1651
|
+
*
|
|
1652
|
+
* `expanding` holds the bindings on the current expansion PATH, so a
|
|
1653
|
+
* binding initialized from itself terminates while a binding named twice
|
|
1654
|
+
* in sibling positions is still read at each of them.
|
|
1655
|
+
*/
|
|
1656
|
+
function declaredTypeOf(identifier, expanding = new Set()) {
|
|
1657
|
+
const variable = variableFor(identifier);
|
|
1658
|
+
if (!variable || expanding.has(variable)) {
|
|
1659
|
+
return null;
|
|
1660
|
+
}
|
|
1661
|
+
for (const def of variable.defs) {
|
|
1662
|
+
if (def.type !== 'Parameter' && def.type !== 'Variable') {
|
|
1663
|
+
continue;
|
|
1664
|
+
}
|
|
1665
|
+
if (def.name.typeAnnotation) {
|
|
1666
|
+
return def.name.typeAnnotation.typeAnnotation;
|
|
1667
|
+
}
|
|
1668
|
+
if (def.node.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
1669
|
+
def.node.id.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
1670
|
+
def.node.init) {
|
|
1671
|
+
expanding.add(variable);
|
|
1672
|
+
const initialized = declaredTypeOfExpression(def.node.init, expanding);
|
|
1673
|
+
expanding.delete(variable);
|
|
1674
|
+
if (initialized) {
|
|
1675
|
+
return initialized;
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
return null;
|
|
1680
|
+
}
|
|
1681
|
+
/**
|
|
1682
|
+
* The type an initializer states for the value it produces.
|
|
1683
|
+
*
|
|
1684
|
+
* `satisfies` is deliberately absent: it checks an expression against a
|
|
1685
|
+
* type without changing the type, so the value keeps whatever the
|
|
1686
|
+
* expression already carried and a complete program reaches the heuristic
|
|
1687
|
+
* there too. A spelling this switch does not recognize answers `null`,
|
|
1688
|
+
* which leaves the guard exactly where it sat before the declaration
|
|
1689
|
+
* sites were read at all.
|
|
1690
|
+
*/
|
|
1691
|
+
function declaredTypeOfExpression(node, expanding) {
|
|
1692
|
+
switch (node.type) {
|
|
1693
|
+
case utils_1.AST_NODE_TYPES.TSAsExpression:
|
|
1694
|
+
case utils_1.AST_NODE_TYPES.TSTypeAssertion:
|
|
1695
|
+
return isConstAssertion(node.typeAnnotation)
|
|
1696
|
+
? declaredTypeOfExpression(node.expression, expanding)
|
|
1697
|
+
: node.typeAnnotation;
|
|
1698
|
+
case utils_1.AST_NODE_TYPES.TSNonNullExpression:
|
|
1699
|
+
case utils_1.AST_NODE_TYPES.ChainExpression:
|
|
1700
|
+
return declaredTypeOfExpression(node.expression, expanding);
|
|
1701
|
+
case utils_1.AST_NODE_TYPES.AwaitExpression: {
|
|
1702
|
+
const awaited = declaredTypeOfExpression(node.argument, expanding);
|
|
1703
|
+
return awaited ? unwrapAwaitedType(awaited) : null;
|
|
1704
|
+
}
|
|
1705
|
+
case utils_1.AST_NODE_TYPES.CallExpression:
|
|
1706
|
+
return declaredReturnTypeOf(node.callee, expanding);
|
|
1707
|
+
case utils_1.AST_NODE_TYPES.Identifier:
|
|
1708
|
+
return declaredTypeOf(node, expanding);
|
|
1709
|
+
default:
|
|
1710
|
+
return null;
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
/**
|
|
1714
|
+
* The return type the callee's SAME-FILE declaration writes.
|
|
1715
|
+
*
|
|
1716
|
+
* A callee resolving into a module this program did not load is answered
|
|
1717
|
+
* by `tracesToImport` before this runs, so reaching here means the
|
|
1718
|
+
* declaration is one this file can be read for — the same boundary the
|
|
1719
|
+
* same-file alias lookup draws.
|
|
1720
|
+
*/
|
|
1721
|
+
function declaredReturnTypeOf(callee, expanding) {
|
|
1722
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
1723
|
+
const variable = variableFor(callee);
|
|
1724
|
+
if (!variable) {
|
|
1725
|
+
return null;
|
|
1726
|
+
}
|
|
1727
|
+
for (const def of variable.defs) {
|
|
1728
|
+
if (def.type === 'FunctionName') {
|
|
1729
|
+
return def.node.returnType?.typeAnnotation ?? null;
|
|
1730
|
+
}
|
|
1731
|
+
if (def.type === 'Variable' &&
|
|
1732
|
+
def.node.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
1733
|
+
(def.node.init?.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
1734
|
+
def.node.init?.type === utils_1.AST_NODE_TYPES.FunctionExpression)) {
|
|
1735
|
+
return def.node.init.returnType?.typeAnnotation ?? null;
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
return null;
|
|
1739
|
+
}
|
|
1740
|
+
if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
1741
|
+
!callee.computed &&
|
|
1742
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
1743
|
+
const declaration = classDeclarationOf(callee.object, expanding);
|
|
1744
|
+
return declaration
|
|
1745
|
+
? returnTypeOfMethod(declaration, callee.property.name)
|
|
1746
|
+
: null;
|
|
1747
|
+
}
|
|
1748
|
+
return null;
|
|
1749
|
+
}
|
|
1750
|
+
/**
|
|
1751
|
+
* The same-file class an expression is an instance of, or names.
|
|
1752
|
+
*
|
|
1753
|
+
* A receiver whose class is not written in this file yields `null`, so
|
|
1754
|
+
* the method's return type stays unread rather than being guessed from a
|
|
1755
|
+
* same-named method on an unrelated class.
|
|
1756
|
+
*/
|
|
1757
|
+
function classDeclarationOf(node, expanding) {
|
|
1758
|
+
if (node.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
1759
|
+
return null;
|
|
1760
|
+
}
|
|
1761
|
+
const variable = variableFor(node);
|
|
1762
|
+
if (!variable || expanding.has(variable)) {
|
|
1763
|
+
return null;
|
|
1764
|
+
}
|
|
1765
|
+
for (const def of variable.defs) {
|
|
1766
|
+
if (def.type === 'ClassName') {
|
|
1767
|
+
return def.node;
|
|
1768
|
+
}
|
|
1769
|
+
if (def.type !== 'Variable' ||
|
|
1770
|
+
def.node.type !== utils_1.AST_NODE_TYPES.VariableDeclarator ||
|
|
1771
|
+
!def.node.init) {
|
|
1772
|
+
continue;
|
|
1773
|
+
}
|
|
1774
|
+
const { init } = def.node;
|
|
1775
|
+
if (init.type === utils_1.AST_NODE_TYPES.ClassExpression) {
|
|
1776
|
+
return init;
|
|
1777
|
+
}
|
|
1778
|
+
if (init.type === utils_1.AST_NODE_TYPES.NewExpression) {
|
|
1779
|
+
expanding.add(variable);
|
|
1780
|
+
const declaration = classDeclarationOf(init.callee, expanding);
|
|
1781
|
+
expanding.delete(variable);
|
|
1782
|
+
if (declaration) {
|
|
1783
|
+
return declaration;
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
return null;
|
|
1788
|
+
}
|
|
1789
|
+
/**
|
|
1790
|
+
* Whether a declared type declares NOTHING.
|
|
1791
|
+
*
|
|
1792
|
+
* An explicit `any` is the checker reporting what the source told it, not
|
|
1793
|
+
* a resolution failure, so it leaves the value in the same position as an
|
|
1794
|
+
* unannotated one and the naming heuristic keeps answering. An
|
|
1795
|
+
* intersection carrying `any` reduces to `any`, so it says as little as a
|
|
1796
|
+
* union carrying one. `unknown` is excluded from this reading on purpose:
|
|
1797
|
+
* `Object.keys` rejects an `unknown` operand, so the fix the heuristic
|
|
1798
|
+
* would attach there does not typecheck.
|
|
1799
|
+
*/
|
|
1800
|
+
function declaresNothing(node) {
|
|
1801
|
+
if (node.type === utils_1.AST_NODE_TYPES.TSUnionType ||
|
|
1802
|
+
node.type === utils_1.AST_NODE_TYPES.TSIntersectionType) {
|
|
1803
|
+
return node.types.some(declaresNothing);
|
|
1804
|
+
}
|
|
1805
|
+
return node.type === utils_1.AST_NODE_TYPES.TSAnyKeyword;
|
|
1806
|
+
}
|
|
1807
|
+
/**
|
|
1808
|
+
* Whether a declared type the checker could not resolve nonetheless
|
|
1809
|
+
* SPELLS an index-signature dictionary.
|
|
1810
|
+
*
|
|
1811
|
+
* `Record<K, V>` is an index signature whichever way `K` and `V` resolve,
|
|
1812
|
+
* so the value is a plain data map rather than a class instance and
|
|
1813
|
+
* `Object.keys` measures its emptiness correctly. Reading that from the
|
|
1814
|
+
* source keeps the verdict a complete program gives — `object` — reachable
|
|
1815
|
+
* without one, and the SPELLING is not what carries the guarantee: a
|
|
1816
|
+
* shape-preserving wrapper, a union member, an intersection whose every
|
|
1817
|
+
* member is pinned and a same-file alias each state the same index
|
|
1818
|
+
* signature, and reading only the bare reference left ten such shapes
|
|
1819
|
+
* silent that a complete program reports (#2345). A reference the source
|
|
1820
|
+
* does not pin this way, `Readonly<NextResponse>` among them, carries no
|
|
1821
|
+
* such guarantee.
|
|
1822
|
+
*/
|
|
1823
|
+
function spellsDictionary(node) {
|
|
1824
|
+
return pinnedShapeOf(node) !== null;
|
|
1825
|
+
}
|
|
1407
1826
|
function isLikelyObject(identifier) {
|
|
1408
1827
|
if (checker && parserServices?.esTreeNodeToTSNodeMap) {
|
|
1409
1828
|
try {
|
|
@@ -1424,13 +1843,39 @@ exports.enforceEmptyObjectCheck = (0, createRule_1.createRule)({
|
|
|
1424
1843
|
* absence of evidence became evidence the value can be `{}`.
|
|
1425
1844
|
*
|
|
1426
1845
|
* The heuristic is still the right answer for a value with no
|
|
1427
|
-
* declaration to resolve — removing it outright silences
|
|
1428
|
-
* rule's own
|
|
1429
|
-
*
|
|
1846
|
+
* declaration to resolve — removing it outright silences 56 of this
|
|
1847
|
+
* rule's own 108 fixtures — so the fall-through is carved out only
|
|
1848
|
+
* where evidence EXISTS and went unread: a value from an unresolved
|
|
1849
|
+
* IMPORT (#2252), and a binding that DECLARES a type the checker
|
|
1850
|
+
* could not resolve (#2344). An unresolvable annotation means the
|
|
1851
|
+
* checker could not look, not that the value is loosely typed:
|
|
1852
|
+
* `response: Readonly<NextResponse>` read off its name alone
|
|
1853
|
+
* reported a class instance, whose state lives behind prototype
|
|
1854
|
+
* accessors, so the prescribed `Object.keys` clause holds for every
|
|
1855
|
+
* valid value and inverts the guard it was meant to harden.
|
|
1856
|
+
*
|
|
1857
|
+
* The carve-out costs reach, and the cost is measured rather than
|
|
1858
|
+
* assumed: across the consumer's 14,059 tracked sources it drops 8
|
|
1859
|
+
* reports, and re-running each under a real `ts.Program` — the
|
|
1860
|
+
* verdict this arm exists to preserve — agrees with 6 of them. The
|
|
1861
|
+
* remaining 2 annotate an all-optional imported type
|
|
1862
|
+
* (`Record<string, string[]>` behind a local alias, and an
|
|
1863
|
+
* `algoliasearch-helper` parameter bag), which a resolved program
|
|
1864
|
+
* calls `object` and reports. Nothing at the annotation site
|
|
1865
|
+
* separates those from `Readonly<NextResponse>`: both are bare
|
|
1866
|
+
* references into a module this program did not load, so recovering
|
|
1867
|
+
* them needs the cross-file resolution whose absence defines this
|
|
1868
|
+
* branch. Two silent true positives is the accepted price of six
|
|
1869
|
+
* guard inversions, because a fix that rewrites correct code costs
|
|
1870
|
+
* more than one the rule declines to make.
|
|
1430
1871
|
*/
|
|
1431
1872
|
if (tracesToImport(identifier)) {
|
|
1432
1873
|
return false;
|
|
1433
1874
|
}
|
|
1875
|
+
const declared = declaredTypeOf(identifier);
|
|
1876
|
+
if (declared && !declaresNothing(declared)) {
|
|
1877
|
+
return spellsDictionary(declared);
|
|
1878
|
+
}
|
|
1434
1879
|
}
|
|
1435
1880
|
catch {
|
|
1436
1881
|
// TypeScript parser services can throw when AST-to-TS node mapping fails; fall back to naming heuristic so linting does not crash.
|