@blumintinc/eslint-plugin-blumint 1.21.15 → 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 CHANGED
@@ -224,7 +224,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
224
224
  module.exports = {
225
225
  meta: {
226
226
  name: '@blumintinc/eslint-plugin-blumint',
227
- version: '1.21.15',
227
+ version: '1.21.16',
228
228
  },
229
229
  parseOptions: {
230
230
  ecmaVersion: 2020,
@@ -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`
@@ -1405,18 +1633,29 @@ exports.enforceEmptyObjectCheck = (0, createRule_1.createRule)({
1405
1633
  return false;
1406
1634
  }
1407
1635
  /**
1408
- * The type the binding behind this value DECLARES, if it declares one.
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).
1409
1646
  *
1410
- * Parameters and variables are the two declarations that annotate the
1411
- * value a guard tests; an import is answered ahead of this by
1412
- * `tracesToImport`. The annotation has to sit on the BINDING: the one on
1413
- * `const { config }: Props = load()` describes the container, and reading
1414
- * it as a verdict on a single property would need exactly the resolution
1415
- * that failed, so a destructured binding keeps the naming heuristic.
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.
1416
1655
  */
1417
- function declaredTypeOf(identifier) {
1656
+ function declaredTypeOf(identifier, expanding = new Set()) {
1418
1657
  const variable = variableFor(identifier);
1419
- if (!variable) {
1658
+ if (!variable || expanding.has(variable)) {
1420
1659
  return null;
1421
1660
  }
1422
1661
  for (const def of variable.defs) {
@@ -1426,6 +1665,124 @@ exports.enforceEmptyObjectCheck = (0, createRule_1.createRule)({
1426
1665
  if (def.name.typeAnnotation) {
1427
1666
  return def.name.typeAnnotation.typeAnnotation;
1428
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
+ }
1429
1786
  }
1430
1787
  return null;
1431
1788
  }
@@ -1434,13 +1791,15 @@ exports.enforceEmptyObjectCheck = (0, createRule_1.createRule)({
1434
1791
  *
1435
1792
  * An explicit `any` is the checker reporting what the source told it, not
1436
1793
  * a resolution failure, so it leaves the value in the same position as an
1437
- * unannotated one and the naming heuristic keeps answering. `unknown` is
1438
- * excluded from this reading on purpose: `Object.keys` rejects an
1439
- * `unknown` operand, so the fix the heuristic would attach there does not
1440
- * typecheck.
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.
1441
1799
  */
1442
1800
  function declaresNothing(node) {
1443
- if (node.type === utils_1.AST_NODE_TYPES.TSUnionType) {
1801
+ if (node.type === utils_1.AST_NODE_TYPES.TSUnionType ||
1802
+ node.type === utils_1.AST_NODE_TYPES.TSIntersectionType) {
1444
1803
  return node.types.some(declaresNothing);
1445
1804
  }
1446
1805
  return node.type === utils_1.AST_NODE_TYPES.TSAnyKeyword;
@@ -1453,18 +1812,16 @@ exports.enforceEmptyObjectCheck = (0, createRule_1.createRule)({
1453
1812
  * so the value is a plain data map rather than a class instance and
1454
1813
  * `Object.keys` measures its emptiness correctly. Reading that from the
1455
1814
  * source keeps the verdict a complete program gives — `object` — reachable
1456
- * without one, which is why a union answers yes as soon as one member
1457
- * spells it, mirroring `isObjectLikeType`. A reference the source does not
1458
- * pin this way, `Readonly<NextResponse>` among them, carries no such
1459
- * guarantee.
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.
1460
1822
  */
1461
1823
  function spellsDictionary(node) {
1462
- if (node.type === utils_1.AST_NODE_TYPES.TSUnionType) {
1463
- return node.types.some(spellsDictionary);
1464
- }
1465
- return (node.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
1466
- node.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
1467
- node.typeName.name === 'Record');
1824
+ return pinnedShapeOf(node) !== null;
1468
1825
  }
1469
1826
  function isLikelyObject(identifier) {
1470
1827
  if (checker && parserServices?.esTreeNodeToTSNodeMap) {