@cplieger/deadset-ts 6.1.1-dev.1 → 7.0.0-dev.2

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.
Files changed (98) hide show
  1. package/dist/src/analysis.js +45 -32
  2. package/dist/src/component-layer.js +1 -1
  3. package/dist/src/configuration-files.js +227 -27
  4. package/dist/src/conformance-record.js +2 -2
  5. package/dist/src/contextual-uses.js +68 -10
  6. package/dist/src/convention-rows.js +48 -0
  7. package/dist/src/conventions.js +80 -0
  8. package/dist/src/dependencies.js +121 -46
  9. package/dist/src/discover.js +1 -1
  10. package/dist/src/edges.js +1 -1
  11. package/dist/src/exempt-classes.js +9 -22
  12. package/dist/src/exempt.js +3 -15
  13. package/dist/src/findings/completion.js +41 -11
  14. package/dist/src/findings/dependencies-and-module-machinery.js +6 -26
  15. package/dist/src/findings/emitters.js +0 -4
  16. package/dist/src/findings/self-check.js +14 -10
  17. package/dist/src/html-entries.js +29 -14
  18. package/dist/src/indexed-access.js +377 -81
  19. package/dist/src/intra-function-parts.js +57 -11
  20. package/dist/src/inventory.js +3 -12
  21. package/dist/src/kinds.js +6 -6
  22. package/dist/src/references.js +62 -23
  23. package/dist/src/report.js +3 -0
  24. package/dist/src/roots.js +96 -7
  25. package/dist/src/run.js +0 -2
  26. package/dist/src/schema.js +1 -1
  27. package/dist/src/scope.js +2 -2
  28. package/dist/src/session.js +6 -2
  29. package/dist/src/source-files.js +1 -0
  30. package/dist/src/suppress-inline.js +1 -1
  31. package/dist/src/suppress.js +15 -16
  32. package/dist/src/sweep.js +29 -17
  33. package/dist/src/symbol-keys.js +114 -0
  34. package/dist/src/test-joins.js +101 -0
  35. package/dist/src/version.js +1 -1
  36. package/dist/src/workflow-steps.js +21 -15
  37. package/dist/src/workspace-programs.js +1 -4
  38. package/dist/src/workspace-resolution.js +0 -2
  39. package/dist/src/workspace.js +0 -3
  40. package/jsr.json +1 -1
  41. package/package.json +2 -1
  42. package/src/analysis.ts +55 -47
  43. package/src/calls.ts +3 -3
  44. package/src/component-files.ts +7 -7
  45. package/src/component-layer.ts +2 -2
  46. package/src/components.ts +1 -1
  47. package/src/config.ts +4 -4
  48. package/src/configuration-files.ts +246 -28
  49. package/src/conformance-record.ts +2 -2
  50. package/src/contextual-uses.ts +99 -13
  51. package/src/convention-rows.ts +76 -0
  52. package/src/conventions.ts +102 -0
  53. package/src/dependencies.ts +148 -56
  54. package/src/discover.ts +3 -3
  55. package/src/edges.ts +3 -3
  56. package/src/entry-points.ts +1 -1
  57. package/src/exempt-classes.ts +10 -24
  58. package/src/exempt.ts +5 -29
  59. package/src/file-facts.ts +1 -1
  60. package/src/finding.ts +5 -1
  61. package/src/findings/completion.ts +53 -11
  62. package/src/findings/dependencies-and-module-machinery.ts +9 -28
  63. package/src/findings/emitters.ts +3 -8
  64. package/src/findings/self-check.ts +17 -14
  65. package/src/findings-pass.ts +2 -2
  66. package/src/graph.ts +2 -2
  67. package/src/html-entries.ts +34 -16
  68. package/src/indexed-access.ts +476 -102
  69. package/src/intra-function-parts.ts +84 -12
  70. package/src/inventory.ts +5 -33
  71. package/src/json-document.ts +1 -1
  72. package/src/kinds.ts +7 -6
  73. package/src/manifest.ts +1 -1
  74. package/src/namespace-objects.ts +1 -1
  75. package/src/pnpm-workspace.ts +1 -1
  76. package/src/references.ts +93 -33
  77. package/src/report.ts +8 -4
  78. package/src/roots.ts +129 -9
  79. package/src/run.ts +0 -2
  80. package/src/schema.ts +2 -2
  81. package/src/scope.ts +2 -2
  82. package/src/session.ts +7 -3
  83. package/src/source-files.ts +3 -0
  84. package/src/stores.ts +1 -1
  85. package/src/suppress-inline.ts +3 -3
  86. package/src/suppress.ts +21 -24
  87. package/src/sweep.ts +29 -17
  88. package/src/symbol-keys.ts +172 -0
  89. package/src/template-field.ts +1 -1
  90. package/src/test-joins.ts +120 -0
  91. package/src/value-reach.ts +1 -1
  92. package/src/verbs/verb.ts +1 -4
  93. package/src/version.ts +1 -1
  94. package/src/workflow-steps.ts +23 -16
  95. package/src/workspace-programs.ts +3 -10
  96. package/src/workspace-resolution.ts +0 -3
  97. package/src/workspace-run.ts +1 -1
  98. package/src/workspace.ts +1 -9
package/src/references.ts CHANGED
@@ -51,6 +51,7 @@ import { byPosition, isComponentFile, renderPosition, type Position } from "./po
51
51
  import { DEFAULT_BATCH_CAP, propertyTables, UNANSWERED, type Answer } from "./query.ts";
52
52
  import type { ProjectView } from "./session.ts";
53
53
  import { typeQueryAliases } from "./type-query-alias.ts";
54
+ import { computedKeyNames, keyNameOf, keyedUses, type KeyedAccess } from "./symbol-keys.ts";
54
55
  import { handedOn, valuesOf } from "./value-flow.ts";
55
56
 
56
57
  /**
@@ -77,17 +78,17 @@ import { handedOn, valuesOf } from "./value-flow.ts";
77
78
  * `decorator` names the declaration a decorator expression is attached to, which the
78
79
  * decorator receives when the class is defined.
79
80
  */
80
- export type Use = "read" | "write" | "evaluation" | "decorator";
81
+ type Use = "read" | "write" | "evaluation" | "decorator";
81
82
 
82
83
  /**
83
- * Which path answered for one reference: `batch`, a file's batched lookup; `resolved-symbol`,
84
- * the per-node lookup for its residue; `shorthand`, the value `{ a }` reads; `alias`, a step
85
- * along an import or export chain; `syntax`, the tree alone (a decorator's target, a
86
- * component's markup); `destructured`, the property a pattern names; `contextual`, the member
87
- * a literal property writes or a value's position reads; `override`, the override a base
88
- * member's call dispatches to; `by-name`, every declaration so spelled, where the checker failed.
84
+ * Which path answered for one reference: `batch`, a file's batched lookup; `resolved-symbol`, the
85
+ * per-node lookup for its residue; `shorthand`, the value `{ a }` reads; `alias`, a step along an
86
+ * import or export chain; `syntax`, the tree alone (a decorator's target, a component's markup);
87
+ * `destructured`, the property a pattern names; `contextual`, the member a literal property writes
88
+ * or a value's position reads; `override`, the override a base call dispatches to; `keyed`, the
89
+ * member a symbol key selects; `by-name`, each declaration so spelled, where the checker failed.
89
90
  */
90
- export type Resolution =
91
+ type Resolution =
91
92
  | "batch"
92
93
  | "resolved-symbol"
93
94
  | "shorthand"
@@ -96,6 +97,7 @@ export type Resolution =
96
97
  | "destructured"
97
98
  | "contextual"
98
99
  | "override"
100
+ | "keyed"
99
101
  | "by-name";
100
102
 
101
103
  /** One use of one declaration by one declaration. */
@@ -187,8 +189,6 @@ export interface ReferenceCost {
187
189
 
188
190
  /** One project's references, in position order, and what reading them cost. */
189
191
  export interface References {
190
- /** The compiler configuration the project was opened from. */
191
- readonly configFile: string;
192
192
  readonly references: readonly Reference[];
193
193
  /**
194
194
  * Every file the rules classified as a test file, by its path below the target root,
@@ -212,6 +212,8 @@ export interface ReferenceOptions {
212
212
  * Each pattern is a rule of its own, which {@link testFileRulesOf} counts.
213
213
  */
214
214
  readonly testFiles: readonly string[];
215
+ /** The files, absolute, that are test files beside those the patterns name. */
216
+ readonly testFileNames?: ReadonlySet<string>;
215
217
  /**
216
218
  * The consumer whose files the pass walks instead of the target's. Its files are the
217
219
  * program's own files below its root and not below the target root, each position is
@@ -297,12 +299,11 @@ function unparenthesized(node: Node): Node {
297
299
  * Records the name one store writes into.
298
300
  *
299
301
  * A property access writes the member it names. An element access writes the collection
300
- * it stores into, because a collection a module only ever stores into holds nothing
301
- * anything reads, and the key is no declaration of the target. A destructuring target
302
- * writes each name it binds, through the spreads and the nested patterns it binds them
303
- * in.
302
+ * it stores into, since a collection a module only stores into holds nothing anything
303
+ * reads, and joins `accesses` for the member a unique-symbol key selects. A destructuring
304
+ * target writes each name it binds, through its spreads and nested patterns.
304
305
  */
305
- function markWrite(target: Node, writes: Set<number>): void {
306
+ function markWrite(target: Node, writes: Set<number>, accesses: Set<Node>): void {
306
307
  const held = unparenthesized(target);
307
308
  if (isName(held)) {
308
309
  writes.add(held.pos);
@@ -315,37 +316,38 @@ function markWrite(target: Node, writes: Set<number>): void {
315
316
  return;
316
317
  }
317
318
  if (isElementAccessExpression(held)) {
319
+ accesses.add(held);
318
320
  // A store through a member's value lands in storage every holder of the value
319
321
  // reads, so it reads the member; a variable only ever stored into stays written.
320
322
  if (!isPropertyAccessExpression(unparenthesized(held.expression))) {
321
- markWrite(held.expression, writes);
323
+ markWrite(held.expression, writes, accesses);
322
324
  }
323
325
  return;
324
326
  }
325
327
  if (isArrayLiteralExpression(held)) {
326
328
  for (const element of held.elements) {
327
- markWrite(element, writes);
329
+ markWrite(element, writes, accesses);
328
330
  }
329
331
  return;
330
332
  }
331
333
  if (isObjectLiteralExpression(held)) {
332
334
  for (const property of held.properties) {
333
335
  if (isPropertyAssignment(property)) {
334
- markWrite(property.initializer, writes);
336
+ markWrite(property.initializer, writes, accesses);
335
337
  continue;
336
338
  }
337
339
  if (isShorthandPropertyAssignment(property)) {
338
- markWrite(property.name, writes);
340
+ markWrite(property.name, writes, accesses);
339
341
  continue;
340
342
  }
341
343
  if (isSpreadAssignment(property)) {
342
- markWrite(property.expression, writes);
344
+ markWrite(property.expression, writes, accesses);
343
345
  }
344
346
  }
345
347
  return;
346
348
  }
347
349
  if (isSpreadElement(held)) {
348
- markWrite(held.expression, writes);
350
+ markWrite(held.expression, writes, accesses);
349
351
  }
350
352
  }
351
353
 
@@ -356,16 +358,21 @@ function markWrite(target: Node, writes: Set<number>): void {
356
358
  * holds. A store that reads its target as well records it in `reads`: a logical
357
359
  * assignment, and a compound assignment, an increment or a decrement whose value is used.
358
360
  */
359
- function markStores(node: Node, writes: Set<number>, reads: Set<number>): void {
361
+ function markStores(
362
+ node: Node,
363
+ writes: Set<number>,
364
+ reads: Set<number>,
365
+ accesses: { readonly written: Set<Node>; readonly read: Set<Node> },
366
+ ): void {
360
367
  if (isBinaryExpression(node)) {
361
368
  const operator = node.operatorToken.kind;
362
369
  if (ASSIGNMENTS.has(operator)) {
363
- markWrite(node.left, writes);
370
+ markWrite(node.left, writes, accesses.written);
364
371
  if (
365
372
  LOGICAL_ASSIGNMENTS.has(operator) ||
366
373
  (operator !== SyntaxKind.EqualsToken && !valueDiscarded(node))
367
374
  ) {
368
- markWrite(node.left, reads);
375
+ markWrite(node.left, reads, accesses.read);
369
376
  }
370
377
  }
371
378
  return;
@@ -375,20 +382,20 @@ function markStores(node: Node, writes: Set<number>, reads: Set<number>): void {
375
382
  node.operator === SyntaxKind.PlusPlusToken ||
376
383
  node.operator === SyntaxKind.MinusMinusToken
377
384
  ) {
378
- markWrite(node.operand, writes);
385
+ markWrite(node.operand, writes, accesses.written);
379
386
  if (!valueDiscarded(node)) {
380
- markWrite(node.operand, reads);
387
+ markWrite(node.operand, reads, accesses.read);
381
388
  }
382
389
  }
383
390
  return;
384
391
  }
385
392
  if (isDeleteExpression(node)) {
386
- markWrite(node.expression, writes);
393
+ markWrite(node.expression, writes, accesses.written);
387
394
  return;
388
395
  }
389
396
  if (isForInStatement(node) || isForOfStatement(node)) {
390
397
  if (node.initializer.kind !== SyntaxKind.VariableDeclarationList) {
391
- markWrite(node.initializer, writes);
398
+ markWrite(node.initializer, writes, accesses.written);
392
399
  }
393
400
  }
394
401
  }
@@ -475,6 +482,7 @@ interface FileSites {
475
482
  readonly wholes: readonly WholeRead[];
476
483
  readonly urls: readonly ModuleURL[];
477
484
  readonly decorated: readonly Decorated[];
485
+ readonly keyed: readonly KeyedAccess[];
478
486
  }
479
487
 
480
488
  /** Whether one node is `import.meta.url`. */
@@ -667,6 +675,8 @@ function sitesOf(
667
675
  const flows: FlowAt[] = [];
668
676
  const writes = new Set<number>();
669
677
  const alsoRead = new Set<number>();
678
+ const accesses = { written: new Set<Node>(), read: new Set<Node>() };
679
+ const keyed: KeyedAccess[] = [];
670
680
  const declared = new Set<number>();
671
681
  const shorthands = new Map<number, Node>();
672
682
  let enclosing = fileId;
@@ -712,7 +722,17 @@ function sitesOf(
712
722
  return;
713
723
  }
714
724
  markNames(node);
715
- markStores(node, writes, alsoRead);
725
+ markStores(node, writes, alsoRead, accesses);
726
+ const key = isElementAccessExpression(node) ? keyNameOf(node.argumentExpression) : undefined;
727
+ if (key !== undefined && isElementAccessExpression(node)) {
728
+ const written = accesses.written.has(node);
729
+ keyed.push({
730
+ access: node,
731
+ key,
732
+ from: enclosing,
733
+ use: written ? (accesses.read.has(node) ? "both" : "write") : "read",
734
+ });
735
+ }
716
736
  if (isObjectLiteralExpression(node) && !isAssignmentPattern(node)) {
717
737
  literals.push({ literal: node, from: enclosing });
718
738
  }
@@ -747,7 +767,18 @@ function sitesOf(
747
767
  };
748
768
 
749
769
  file.forEachChild(visit);
750
- return { uses: found, literals, flows, reads, links, evaluations, wholes, urls, decorated };
770
+ return {
771
+ uses: found,
772
+ literals,
773
+ flows,
774
+ reads,
775
+ links,
776
+ evaluations,
777
+ wholes,
778
+ urls,
779
+ decorated,
780
+ keyed,
781
+ };
751
782
  }
752
783
 
753
784
  /** The declaration kinds of a property an object literal declares. */
@@ -1194,7 +1225,9 @@ export function references<Brand>(
1194
1225
  const testFile = new Map<SourceFile, boolean>();
1195
1226
  for (const { file } of walked) {
1196
1227
  const path = renderPosition(file, root, 0).path;
1197
- const test = patterns.some((pattern) => pattern.test(path));
1228
+ const test =
1229
+ patterns.some((pattern) => pattern.test(path)) ||
1230
+ options.testFileNames?.has(file.fileName) === true;
1198
1231
  testFile.set(file, test);
1199
1232
  if (test) {
1200
1233
  tests.push(path);
@@ -1230,7 +1263,19 @@ export function references<Brand>(
1230
1263
  }
1231
1264
  }
1232
1265
 
1233
- for (const { file, fileId, uses, reads, links, evaluations, wholes, urls, decorated } of walked) {
1266
+ const keyNames = computedKeyNames(held.symbols);
1267
+ for (const {
1268
+ file,
1269
+ fileId,
1270
+ uses,
1271
+ reads,
1272
+ links,
1273
+ evaluations,
1274
+ wholes,
1275
+ urls,
1276
+ decorated,
1277
+ keyed,
1278
+ } of walked) {
1234
1279
  const test = testFile.get(file) === true;
1235
1280
  // A component file's markup may use any binding its blocks declare at the top level.
1236
1281
  const markup = isComponentFile(file);
@@ -1449,6 +1494,22 @@ export function references<Brand>(
1449
1494
  }
1450
1495
  });
1451
1496
 
1497
+ const resolvedKeys = keyed.flatMap((access) => {
1498
+ const symbol =
1499
+ isIdentifier(access.key) && keyNames.has(access.key.text)
1500
+ ? answered.get(access.key)
1501
+ : undefined;
1502
+ return typeof symbol === "object" ? [{ access, symbol }] : [];
1503
+ });
1504
+ for (const { access, ids } of keyedUses(project, chains, resolvedKeys, tables, cap)) {
1505
+ const position = renderPosition(file, root, access.access.argumentExpression.getStart());
1506
+ for (const id of ids) {
1507
+ for (const use of access.use === "both" ? (["read", "write"] as const) : [access.use]) {
1508
+ found.push({ from: access.from, to: id, position, use, resolution: "keyed", test });
1509
+ }
1510
+ }
1511
+ }
1512
+
1452
1513
  for (const site of uses) {
1453
1514
  const direct: Resolution =
1454
1515
  site.shorthand !== undefined
@@ -1510,7 +1571,6 @@ export function references<Brand>(
1510
1571
  );
1511
1572
 
1512
1573
  return {
1513
- configFile: project.configFile,
1514
1574
  references:
1515
1575
  consumer === undefined
1516
1576
  ? found
package/src/report.ts CHANGED
@@ -30,14 +30,14 @@ export const ANALYZER_NAME = "deadset-ts";
30
30
  /** The language this analyzer claims. */
31
31
  export const LANGUAGE = "ts";
32
32
 
33
- export interface WirePosition {
33
+ interface WirePosition {
34
34
  readonly path: string;
35
35
  readonly line: number;
36
36
  readonly column: number;
37
37
  readonly end_line: number;
38
38
  }
39
39
 
40
- export interface WireDetails {
40
+ interface WireDetails {
41
41
  readonly narrower_visibility?: string;
42
42
  readonly implementations?: readonly {
43
43
  readonly ref: string;
@@ -54,6 +54,7 @@ export interface WireDetails {
54
54
  readonly reason?: string;
55
55
  };
56
56
  readonly overlap?: readonly string[];
57
+ readonly removes_last_use_of?: readonly string[];
57
58
  }
58
59
 
59
60
  /** One finding as the finding schema spells it. */
@@ -93,7 +94,7 @@ export interface WireFinding {
93
94
  readonly details: WireDetails;
94
95
  }
95
96
 
96
- export interface WireEdgeEvaluation {
97
+ interface WireEdgeEvaluation {
97
98
  readonly edge: string;
98
99
  readonly side: string;
99
100
  readonly symbol: string;
@@ -178,7 +179,7 @@ interface ReportConsumer {
178
179
  }
179
180
 
180
181
  /** One consumer the scope declared and the run did not load, with why. */
181
- export interface UnavailableConsumer {
182
+ interface UnavailableConsumer {
182
183
  readonly id: string;
183
184
  readonly reason: string;
184
185
  }
@@ -297,6 +298,9 @@ function wireDetails(details: FindingDetails): WireDetails {
297
298
  },
298
299
  }),
299
300
  ...(details.overlap === undefined ? {} : { overlap: details.overlap }),
301
+ ...(details.removesLastUseOf === undefined
302
+ ? {}
303
+ : { removes_last_use_of: details.removesLastUseOf }),
300
304
  };
301
305
  }
302
306
 
package/src/roots.ts CHANGED
@@ -15,6 +15,8 @@ import {
15
15
  isQualifiedName,
16
16
  isTypeQueryNode,
17
17
  isTypeReferenceNode,
18
+ isPropertyDeclaration,
19
+ isVariableDeclaration,
18
20
  isVariableStatement,
19
21
  type Node,
20
22
  type SourceFile,
@@ -31,6 +33,7 @@ import { relativePath, resolvePath } from "./paths.ts";
31
33
  import { byPosition } from "./position.ts";
32
34
  import { UNANSWERED } from "./query.ts";
33
35
  import type { ProjectView } from "./session.ts";
36
+ import type { Symbol as TSSymbol, Type } from "@typescript/native/unstable/sync";
34
37
  import { sourceFilesOf } from "./source-files.ts";
35
38
  import { typeQueryAliases } from "./type-query-alias.ts";
36
39
 
@@ -78,8 +81,6 @@ export interface Root {
78
81
 
79
82
  /** One project's roots, and every configured string that named nothing in it. */
80
83
  export interface Roots {
81
- /** The compiler configuration the project was opened from. */
82
- readonly configFile: string;
83
84
  /**
84
85
  * Every root of this project, ordered by the site of the declaration it names,
85
86
  * then by kind, then by the string that named it. A declaration that is a root for
@@ -128,6 +129,26 @@ export interface RootOptions {
128
129
  readonly parse?: ParseFile | undefined;
129
130
  /** Whether the target is a library, so a consumer outside it reaches its published API. */
130
131
  readonly publishedAPI: boolean;
132
+ /**
133
+ * The directories of the workspace members whose manifest is not private, each a library
134
+ * inside the target whose entries are published API whatever the target's kind.
135
+ */
136
+ readonly publishedMembers?: ReadonlySet<string> | undefined;
137
+ }
138
+
139
+ /** Whether one declaration is written below the directory of a published workspace member. */
140
+ function inPublishedMember(
141
+ held: Inventory,
142
+ id: string,
143
+ root: string,
144
+ members: ReadonlySet<string> | undefined,
145
+ ): boolean {
146
+ const path = held.symbols.find((symbol) => symbol.id === id)?.position.path;
147
+ if (path === undefined || members === undefined) {
148
+ return false;
149
+ }
150
+ const file = resolvePath(root, path);
151
+ return [...members].some((dir) => file.startsWith(`${dir}/`));
131
152
  }
132
153
 
133
154
  /** The kind of root each role of a manifest entry makes. */
@@ -305,10 +326,16 @@ export function roots<Brand>(
305
326
  ...options.manifest.entries.map((entry) => ({
306
327
  entry,
307
328
  publishes:
329
+ options.publishedAPI &&
308
330
  entry.published &&
309
331
  (options.manifest.declaresExports ? entry.member.startsWith("exports") : true),
310
332
  })),
311
- ...(options.otherPackages ?? []).map((entry) => ({ entry, publishes: entry.published })),
333
+ ...(options.otherPackages ?? []).map((entry) => ({
334
+ entry,
335
+ publishes:
336
+ entry.published &&
337
+ (options.publishedAPI || options.publishedMembers?.has(entry.dir) === true),
338
+ })),
312
339
  ];
313
340
  const scripted = entries.flatMap(({ entry }) =>
314
341
  entry.role === "script" ? files.named(entry.path) : [],
@@ -324,7 +351,7 @@ export function roots<Brand>(
324
351
  : readBackByName(options.host, entry, readBackTo);
325
352
  };
326
353
  for (const { entry, publishes } of entries) {
327
- if (options.publishedAPI && publishes) {
354
+ if (publishes) {
328
355
  published.push(...entryFiles(entry));
329
356
  }
330
357
  }
@@ -380,9 +407,14 @@ export function roots<Brand>(
380
407
  }
381
408
  }
382
409
 
383
- const api = options.publishedAPI
384
- ? publishedAPI(project, published, exportsOf, held, files.byPath, releaseTagged(held, files))
385
- : { ids: [], guessed: [] };
410
+ const tagged = releaseTagged(held, files).filter(
411
+ (id) =>
412
+ options.publishedAPI || inPublishedMember(held, id, targetRoot, options.publishedMembers),
413
+ );
414
+ const api =
415
+ published.length > 0 || tagged.length > 0
416
+ ? publishedAPI(project, published, exportsOf, held, files.byPath, tagged)
417
+ : { ids: [], guessed: [] };
386
418
  for (const id of api.ids) {
387
419
  add(id, "published-api", "");
388
420
  }
@@ -398,7 +430,6 @@ export function roots<Brand>(
398
430
  });
399
431
 
400
432
  return {
401
- configFile: project.configFile,
402
433
  liveUnderReachability: ordered,
403
434
  unmatched,
404
435
  unanswered: answered ? api.guessed : held.symbols.map((symbol) => symbol.id),
@@ -560,6 +591,18 @@ function publishedAPI<Brand>(
560
591
  const symbol = byId.get(id);
561
592
  return symbol === undefined || symbol.kind === "file" ? [] : nodes.typeNamesOf(symbol);
562
593
  });
594
+ for (const symbol of inferredTypeSymbols(
595
+ project,
596
+ round.flatMap((id) => {
597
+ const symbol = byId.get(id);
598
+ const node = symbol === undefined ? undefined : nodes.nodeOf(symbol);
599
+ return node === undefined ? [] : [node];
600
+ }),
601
+ )) {
602
+ for (const target of chains.chainOf(symbol)) {
603
+ reach(target.id);
604
+ }
605
+ }
563
606
  project.symbolsAt(named.map((node) => project.handle(node))).forEach((symbol, index) => {
564
607
  if (symbol === UNANSWERED) {
565
608
  for (const id of held.byName.get(named[index]?.getText() ?? "") ?? []) {
@@ -580,7 +623,10 @@ function publishedAPI<Brand>(
580
623
  function declarationNodes(
581
624
  held: Inventory,
582
625
  byPath: ReadonlyMap<string, SourceFile>,
583
- ): { typeNamesOf(symbol: InventorySymbol): readonly Node[] } {
626
+ ): {
627
+ typeNamesOf(symbol: InventorySymbol): readonly Node[];
628
+ nodeOf(symbol: InventorySymbol): Node | undefined;
629
+ } {
584
630
  const walked = new Map<string, { byId: Map<string, Node>; nodes: Set<Node> }>();
585
631
  const nodesIn = (path: string): { byId: Map<string, Node>; nodes: Set<Node> } => {
586
632
  const known = walked.get(path);
@@ -611,7 +657,81 @@ function declarationNodes(
611
657
  const declaration = byId.get(symbol.id);
612
658
  return declaration === undefined ? [] : typeNamesOf(declaration, nodes);
613
659
  },
660
+ nodeOf: (symbol) => nodesIn(symbol.position.path).byId.get(symbol.id),
661
+ };
662
+ }
663
+
664
+ /** How deep the type arguments and constituents of an inferred type are followed. */
665
+ const INFERRED_DEPTH = 4;
666
+
667
+ /**
668
+ * The declarations of the types the compiler infers where a declaration writes none: the
669
+ * type of a variable or a property with no annotation, and the result of a function or a
670
+ * method with no result annotation, each with the types its type arguments and its union
671
+ * or intersection constituents name, to a fixed depth. A question the checker leaves
672
+ * unanswered names nothing.
673
+ */
674
+ function inferredTypeSymbols<Brand>(
675
+ project: ProjectView<Brand>,
676
+ declarations: readonly Node[],
677
+ ): readonly TSSymbol[] {
678
+ const { queries } = project;
679
+ const valued = declarations.flatMap((node) =>
680
+ (isVariableDeclaration(node) || isPropertyDeclaration(node)) &&
681
+ node.type === undefined &&
682
+ !isFunctionLikeDeclaration(node.initializer ?? node)
683
+ ? [node.name]
684
+ : [],
685
+ );
686
+ const types: Type[] = [];
687
+ queries.typesAt(valued).forEach((type) => {
688
+ if (type !== UNANSWERED && type !== undefined) {
689
+ types.push(type);
690
+ }
691
+ });
692
+ for (const node of declarations) {
693
+ const signature = isFunctionLikeDeclaration(node)
694
+ ? node
695
+ : isVariableDeclaration(node) &&
696
+ node.type === undefined &&
697
+ node.initializer !== undefined &&
698
+ isFunctionLikeDeclaration(node.initializer)
699
+ ? node.initializer
700
+ : undefined;
701
+ if (signature === undefined || signature.type !== undefined || signature.body === undefined) {
702
+ continue;
703
+ }
704
+ const typed = queries.signatureOf(signature);
705
+ const result =
706
+ typed === UNANSWERED || typed === undefined ? undefined : queries.returnTypeOf(typed);
707
+ if (result !== UNANSWERED && result !== undefined) {
708
+ types.push(result);
709
+ }
710
+ }
711
+ const found = new Map<number, TSSymbol>();
712
+ const seen = new Set<Type>();
713
+ const visit = (type: Type, depth: number): void => {
714
+ if (depth > INFERRED_DEPTH || seen.has(type)) {
715
+ return;
716
+ }
717
+ seen.add(type);
718
+ const symbol = queries.symbolOfType(type);
719
+ if (symbol !== UNANSWERED && symbol !== undefined) {
720
+ found.set(symbol.id, symbol);
721
+ }
722
+ const parts = queries.constituents(type);
723
+ const args = queries.typeArguments(type);
724
+ for (const next of [
725
+ ...(parts === UNANSWERED || (parts.length === 1 && parts[0] === type) ? [] : parts),
726
+ ...(args === UNANSWERED ? [] : args),
727
+ ]) {
728
+ visit(next, depth + 1);
729
+ }
614
730
  };
731
+ types.forEach((type) => {
732
+ visit(type, 0);
733
+ });
734
+ return [...found.values()];
615
735
  }
616
736
 
617
737
  /**
package/src/run.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { ConfigError, REPOSITORY_DOCUMENT, type Inputs } from "./config.ts";
2
2
  import { DiscoveryError, renderDiagnostic } from "./discover.ts";
3
- import { EMITTERS } from "./findings/emitters.ts";
4
3
  import type { Host } from "./host.ts";
5
4
  import { joinPath, resolvePath } from "./paths.ts";
6
5
  import { readScope, ScopeError, scopeForDir, type Scope } from "./scope.ts";
@@ -264,7 +263,6 @@ function invoke(
264
263
  });
265
264
  return opened;
266
265
  },
267
- emitters: EMITTERS,
268
266
  });
269
267
  } catch (error: unknown) {
270
268
  opened?.close();
package/src/schema.ts CHANGED
@@ -15,7 +15,7 @@ import { ConfigError } from "./config.ts";
15
15
  * - `object` is one setting written as an object: its member names are declared
16
16
  * and a source supplies the object whole.
17
17
  */
18
- export type KeyKind = "leaf" | "section" | "map" | "list" | "object";
18
+ type KeyKind = "leaf" | "section" | "map" | "list" | "object";
19
19
 
20
20
  /**
21
21
  * One node of the closed key list. `members` holds a section's declared members,
@@ -259,7 +259,7 @@ export function malformed(label: string, path: string, detail: string): ConfigEr
259
259
  * A refusal of a key the closed key list does not declare, naming the nearest key
260
260
  * it does.
261
261
  */
262
- export function unimplementedKey(label: string, path: string): ConfigError {
262
+ function unimplementedKey(label: string, path: string): ConfigError {
263
263
  const nearest = nearestKey(path);
264
264
  let hint = "";
265
265
  if (nearest === path) {
package/src/scope.ts CHANGED
@@ -4,8 +4,8 @@ import { dirnamePath, isAbsolutePath, joinPath, resolvePath } from "./paths.ts";
4
4
  import { checkDocument, type KeyNode } from "./schema.ts";
5
5
 
6
6
  /** The roles a scope document declares for a module. */
7
- export const ROLE_TARGET = "target";
8
- export const ROLE_CONSUMER = "consumer";
7
+ const ROLE_TARGET = "target";
8
+ const ROLE_CONSUMER = "consumer";
9
9
 
10
10
  /**
11
11
  * A scope document above this length is refused rather than decoded. It names one
package/src/session.ts CHANGED
@@ -347,7 +347,13 @@ function viewOf<Brand>(
347
347
  if ((symbol.flags & SymbolFlags.Alias) === 0) {
348
348
  return undefined;
349
349
  }
350
- const held = queries.immediateAliased(symbol);
350
+ // An alias the checker synthesizes declares nothing, the default a module that assigns
351
+ // its export gets under esModuleInterop, and the immediate step fails on it while the
352
+ // whole resolution answers.
353
+ const held =
354
+ symbol.declarations.length === 0
355
+ ? queries.aliased(symbol)
356
+ : queries.immediateAliased(symbol);
351
357
  // An alias that resolves to nothing answers with the checker's own unknown
352
358
  // symbol, which declares nothing; a symbol that is no alias answers with
353
359
  // nothing at all.
@@ -411,7 +417,6 @@ export function ownedAs<Brand>(
411
417
 
412
418
  /** Every diagnostic set a project is refused for carrying an error in. */
413
419
  export interface ProjectDiagnostics {
414
- readonly configFile: string;
415
420
  readonly syntactic: readonly Diagnostic[];
416
421
  readonly semantic: readonly Diagnostic[];
417
422
  readonly configParsing: readonly Diagnostic[];
@@ -425,7 +430,6 @@ export interface ProjectDiagnostics {
425
430
  /** The three diagnostic sets one project carries, read before any analysis. */
426
431
  export function diagnosticsOf<Brand>(project: ProjectView<Brand>): ProjectDiagnostics {
427
432
  return {
428
- configFile: project.configFile,
429
433
  syntactic: project.program.getSyntacticDiagnostics(),
430
434
  semantic: project.program.getSemanticDiagnostics(),
431
435
  configParsing: project.program.getConfigFileParsingDiagnostics(),
@@ -38,6 +38,8 @@ export interface SourceFiles {
38
38
  * nothing.
39
39
  */
40
40
  named(path: string): readonly SourceFile[];
41
+ /** The directories the project writes its output below, absolute: its output and declaration directories. */
42
+ readonly outputDirs: readonly string[];
41
43
  }
42
44
 
43
45
  /**
@@ -76,6 +78,7 @@ export function sourceFilesOf<Brand>(project: ProjectView<Brand>, targetRoot: st
76
78
  return {
77
79
  byPath,
78
80
  byName,
81
+ outputDirs: [...new Set([outDir, declarationDir])].filter((dir) => dir !== undefined),
79
82
  named: (path) => {
80
83
  if (!path.includes(SUBPATH_WILDCARD)) {
81
84
  for (const candidate of candidates(path)) {
package/src/stores.ts CHANGED
@@ -37,7 +37,7 @@ export interface Stores {
37
37
  }
38
38
 
39
39
  /** What one configuration contributes to the run's stores. */
40
- export interface ConfigurationStores {
40
+ interface ConfigurationStores {
41
41
  readonly references: readonly Reference[];
42
42
  readonly accessors: readonly string[];
43
43
  }
@@ -37,7 +37,7 @@ const NO_REASON = new RegExp(DIRECTIVE_EXPRESSIONS.noReason);
37
37
  const DIRECTIVE_FORM = "// deadset:ignore DS0000[,DS0000...] -- <reason>";
38
38
 
39
39
  /** What the grammar's decision procedure makes of one comment. */
40
- export type Directive =
40
+ type Directive =
41
41
  | { readonly is: "none" }
42
42
  | { readonly is: "directive"; readonly codes: readonly string[]; readonly reason: string }
43
43
  | { readonly is: "no-reason"; readonly codes: readonly string[] }
@@ -64,7 +64,7 @@ export function classifyComment(text: string): Directive {
64
64
  }
65
65
 
66
66
  /** One `//` line comment: the offset of its first solidus, and its text to the end of its line. */
67
- export interface LineComment {
67
+ interface LineComment {
68
68
  readonly offset: number;
69
69
  readonly text: string;
70
70
  }
@@ -123,7 +123,7 @@ export function tokenStart(text: string, from: number): number {
123
123
  * syntax tree holds are skipped, so a `//` inside a string, a template or a regular
124
124
  * expression starts nothing.
125
125
  */
126
- export function lineComments(file: SourceFile): readonly LineComment[] {
126
+ function lineComments(file: SourceFile): readonly LineComment[] {
127
127
  const text = file.text;
128
128
  const spans: { readonly start: number; readonly end: number }[] = [];
129
129
  const visit = (node: Node): void => {