@cplieger/deadset-ts 7.2.0-dev.4 → 8.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 (122) hide show
  1. package/dist/src/analysis.js +108 -10
  2. package/dist/src/augmentations.js +62 -18
  3. package/dist/src/automatic-types.js +189 -0
  4. package/dist/src/base-url.js +18 -0
  5. package/dist/src/components.js +7 -6
  6. package/dist/src/config.js +1 -0
  7. package/dist/src/configuration-files.js +50 -12
  8. package/dist/src/conformance-record.js +2 -2
  9. package/dist/src/convention-rows.js +98 -0
  10. package/dist/src/conventions.js +101 -5
  11. package/dist/src/decorator.js +21 -18
  12. package/dist/src/dependencies.js +183 -28
  13. package/dist/src/discover.js +132 -11
  14. package/dist/src/entry-point-gaps.js +7 -10
  15. package/dist/src/entry-points.js +112 -11
  16. package/dist/src/findings/completion.js +172 -40
  17. package/dist/src/findings/dependencies-and-module-machinery.js +71 -25
  18. package/dist/src/findings/emitters.js +1 -1
  19. package/dist/src/findings/intra-function.js +44 -13
  20. package/dist/src/findings/non-code-artifacts.js +66 -33
  21. package/dist/src/findings/unused-declarations.js +30 -5
  22. package/dist/src/findings/visibility-narrowing.js +29 -5
  23. package/dist/src/generated-file.js +11 -0
  24. package/dist/src/git-ignore.js +86 -0
  25. package/dist/src/graph.js +4 -1
  26. package/dist/src/import-bindings.js +97 -0
  27. package/dist/src/indexed-access.js +130 -28
  28. package/dist/src/interface-satisfaction.js +30 -12
  29. package/dist/src/intra-function-parts.js +297 -45
  30. package/dist/src/inventory.js +52 -8
  31. package/dist/src/kinds.js +13 -13
  32. package/dist/src/matrix.js +57 -10
  33. package/dist/src/mdx-entries.js +197 -0
  34. package/dist/src/module-declarations.js +9 -13
  35. package/dist/src/overrides.js +41 -10
  36. package/dist/src/print.js +1 -0
  37. package/dist/src/ref.js +10 -0
  38. package/dist/src/references.js +38 -4
  39. package/dist/src/report.js +4 -2
  40. package/dist/src/resolve.js +2 -0
  41. package/dist/src/roots.js +110 -38
  42. package/dist/src/schema.js +1 -0
  43. package/dist/src/serialization-contract.js +54 -8
  44. package/dist/src/session.js +7 -0
  45. package/dist/src/source-files.js +39 -14
  46. package/dist/src/suppress-file.js +10 -2
  47. package/dist/src/suppress-inline.js +18 -2
  48. package/dist/src/suppress.js +8 -5
  49. package/dist/src/sweep.js +27 -10
  50. package/dist/src/symbol-keys.js +103 -2
  51. package/dist/src/test-support.js +119 -18
  52. package/dist/src/type-errors.js +34 -7
  53. package/dist/src/type-parameter-reads.js +172 -0
  54. package/dist/src/verbs/analyze.js +16 -0
  55. package/dist/src/verbs/explain.js +4 -2
  56. package/dist/src/version.js +1 -1
  57. package/dist/src/workflow-steps.js +119 -22
  58. package/dist/src/workspace-programs.js +7 -3
  59. package/dist/src/workspace-resolution.js +53 -10
  60. package/jsr.json +1 -1
  61. package/package.json +2 -2
  62. package/src/analysis.ts +146 -11
  63. package/src/augmentations.ts +76 -22
  64. package/src/automatic-types.ts +246 -0
  65. package/src/base-url.ts +24 -0
  66. package/src/components.ts +7 -6
  67. package/src/config.ts +3 -0
  68. package/src/configuration-files.ts +66 -12
  69. package/src/conformance-record.ts +2 -2
  70. package/src/convention-rows.ts +128 -0
  71. package/src/conventions.ts +121 -7
  72. package/src/decorator.ts +28 -18
  73. package/src/dependencies.ts +247 -41
  74. package/src/discover.ts +156 -13
  75. package/src/entry-point-gaps.ts +8 -11
  76. package/src/entry-points.ts +133 -11
  77. package/src/finding.ts +15 -0
  78. package/src/findings/completion.ts +222 -46
  79. package/src/findings/dependencies-and-module-machinery.ts +90 -28
  80. package/src/findings/emitters.ts +1 -1
  81. package/src/findings/intra-function.ts +56 -14
  82. package/src/findings/non-code-artifacts.ts +81 -41
  83. package/src/findings/unused-declarations.ts +36 -5
  84. package/src/findings/visibility-narrowing.ts +40 -5
  85. package/src/generated-file.ts +16 -0
  86. package/src/git-ignore.ts +98 -0
  87. package/src/graph.ts +6 -0
  88. package/src/host.ts +5 -9
  89. package/src/import-bindings.ts +128 -0
  90. package/src/indexed-access.ts +152 -30
  91. package/src/interface-satisfaction.ts +33 -13
  92. package/src/intra-function-parts.ts +394 -59
  93. package/src/inventory.ts +71 -7
  94. package/src/kinds.ts +13 -13
  95. package/src/matrix.ts +104 -15
  96. package/src/mdx-entries.ts +241 -0
  97. package/src/module-declarations.ts +11 -13
  98. package/src/overrides.ts +51 -12
  99. package/src/print.ts +1 -0
  100. package/src/ref.ts +12 -0
  101. package/src/references.ts +48 -3
  102. package/src/report.ts +16 -3
  103. package/src/resolve.ts +3 -0
  104. package/src/roots.ts +163 -45
  105. package/src/schema.ts +1 -0
  106. package/src/serialization-contract.ts +55 -4
  107. package/src/session.ts +7 -0
  108. package/src/source-files.ts +66 -16
  109. package/src/suppress-file.ts +12 -1
  110. package/src/suppress-inline.ts +18 -1
  111. package/src/suppress.ts +10 -5
  112. package/src/sweep.ts +32 -11
  113. package/src/symbol-keys.ts +129 -2
  114. package/src/test-support.ts +155 -18
  115. package/src/type-errors.ts +38 -6
  116. package/src/type-parameter-reads.ts +227 -0
  117. package/src/verbs/analyze.ts +22 -0
  118. package/src/verbs/explain.ts +5 -2
  119. package/src/version.ts +1 -1
  120. package/src/workflow-steps.ts +127 -22
  121. package/src/workspace-programs.ts +7 -3
  122. package/src/workspace-resolution.ts +62 -10
package/src/roots.ts CHANGED
@@ -22,19 +22,24 @@ import {
22
22
  type SourceFile,
23
23
  } from "@typescript/native/ast";
24
24
  import { aliasChains } from "./alias-chain.ts";
25
- import type { ParseFile } from "./configuration-files.ts";
25
+ import {
26
+ filesNamedBy,
27
+ outsideChain,
28
+ relativeSpecifiers,
29
+ type ParseFile,
30
+ } from "./configuration-files.ts";
26
31
  import { conventionEntries, type Conventions } from "./conventions.ts";
27
32
  import { entryPoints, type EntryRule } from "./entry-points.ts";
28
33
  import { globExpression } from "./glob.ts";
29
34
  import type { Host } from "./host.ts";
30
35
  import { nodeKey, type Inventory, type InventorySymbol } from "./inventory.ts";
31
36
  import type { Manifest, ManifestEntry } from "./manifest.ts";
32
- import { relativePath, resolvePath } from "./paths.ts";
37
+ import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.ts";
33
38
  import { byPosition } from "./position.ts";
34
39
  import { UNANSWERED } from "./query.ts";
35
40
  import type { ProjectView } from "./session.ts";
36
41
  import type { Symbol as TSSymbol, Type } from "@typescript/native/sync";
37
- import { sourceFilesOf } from "./source-files.ts";
42
+ import { sourceFilesOf, type SourceFiles } from "./source-files.ts";
38
43
  import { typeQueryAliases } from "./type-query-alias.ts";
39
44
 
40
45
  /** Why one declaration is a root. */
@@ -104,6 +109,8 @@ export interface Roots {
104
109
  * unknown; otherwise each one an unanswered published type name may name.
105
110
  */
106
111
  readonly unanswered: readonly string[];
112
+ /** Every manifest entry naming a file to import or run that reads back to no file of this project. */
113
+ readonly unmappedEntries: readonly ManifestEntry[];
107
114
  }
108
115
 
109
116
  /** What decides the roots beside the declarations themselves. */
@@ -134,6 +141,8 @@ export interface RootOptions {
134
141
  * inside the target whose entries are published API whatever the target's kind.
135
142
  */
136
143
  readonly publishedMembers?: ReadonlySet<string> | undefined;
144
+ /** Every file a program of the run holds, so a file another configuration compiles is no outside file. */
145
+ readonly runFiles?: ReadonlySet<string> | undefined;
137
146
  }
138
147
 
139
148
  /** Whether one declaration is written below the directory of a published workspace member. */
@@ -171,11 +180,25 @@ const OUTPUT_EXTENSION = /\.(?:d\.ts|d\.mts|d\.cts|js|mjs|cjs|jsx)$/u;
171
180
  /** The extension a source file's name drops. */
172
181
  const SOURCE_EXTENSION = /\.(?:d\.)?[cm]?[jt]sx?$/u;
173
182
 
183
+ /** Whether no `package.json` sits between one file and the manifest's directory, which holds it. */
184
+ function ofPackage(host: Host, dir: string, file: string): boolean {
185
+ for (let at = dirnamePath(file); at !== dir; at = dirnamePath(at)) {
186
+ if (
187
+ relativePath(dir, at) === undefined ||
188
+ host.kindOf(joinPath(at, "package.json")) === "file"
189
+ ) {
190
+ return false;
191
+ }
192
+ }
193
+ return true;
194
+ }
195
+
174
196
  /**
175
197
  * The rooted files one manifest target reads back to by name: its path below the
176
198
  * manifest's directory, past its first directory where it names one, without its output
177
- * extension, is the name, and a rooted file is one whose path below the same directory,
178
- * without its source extension, ends with that name at a directory boundary.
199
+ * extension, is the name, and a rooted file of the manifest's own package is one whose
200
+ * path below the same directory, without its source extension, ends with that name at a
201
+ * directory boundary.
179
202
  */
180
203
  function readBackByName(
181
204
  host: Host,
@@ -195,13 +218,52 @@ function readBackByName(
195
218
  const found = new Map<string, SourceFile>();
196
219
  for (const file of rooted) {
197
220
  const path = relativePath(dir, file.fileName)?.replace(SOURCE_EXTENSION, "");
198
- if (path !== undefined && (path === name || path.endsWith(`/${name}`))) {
221
+ if (
222
+ path !== undefined &&
223
+ (path === name || path.endsWith(`/${name}`)) &&
224
+ ofPackage(host, dir, file.fileName)
225
+ ) {
199
226
  found.set(file.fileName, file);
200
227
  }
201
228
  }
202
229
  return [...found.values()];
203
230
  }
204
231
 
232
+ /**
233
+ * The program files a manifest target no program holds reaches: the target is a file
234
+ * read as a configuration file outside the program is, and each file of the chain it
235
+ * starts loads the program files its relative specifiers name, read through the emit
236
+ * mapping, as a command's shim loads the compiled entry it runs.
237
+ */
238
+ function throughOutside(
239
+ options: RootOptions,
240
+ targetRoot: string,
241
+ files: SourceFiles,
242
+ entry: ManifestEntry,
243
+ ): readonly SourceFile[] {
244
+ const cwd = options.host.workingDirectory();
245
+ const path = resolvePath(cwd, entry.path);
246
+ if (
247
+ options.parse === undefined ||
248
+ options.runFiles?.has(path) === true ||
249
+ options.host.kindOf(path) !== "file"
250
+ ) {
251
+ return [];
252
+ }
253
+ const chain = outsideChain(options.host, targetRoot, files, options.parse, [
254
+ { path, manifestDir: resolvePath(cwd, entry.dir) },
255
+ ]);
256
+ const found = new Map<string, SourceFile>();
257
+ for (const { file } of chain) {
258
+ for (const specifier of relativeSpecifiers(file)) {
259
+ for (const named of filesNamedBy(files, dirnamePath(file.fileName), specifier)) {
260
+ found.set(named.fileName, named);
261
+ }
262
+ }
263
+ }
264
+ return [...found.values()];
265
+ }
266
+
205
267
  /** Whether a configured string carries either special character. */
206
268
  function isPattern(text: string): boolean {
207
269
  return text.includes("*") || text.includes("?");
@@ -307,9 +369,21 @@ export function roots<Brand>(
307
369
  found.set([id, kind, source].join("\u0000"), { id, kind, source });
308
370
  };
309
371
 
310
- const entered: { file: SourceFile; kind: RootKind; source: string; exports: boolean }[] = [];
311
- const enter = (file: SourceFile, kind: RootKind, source: string, exports: boolean): void => {
312
- entered.push({ file, kind, source, exports });
372
+ const entered: {
373
+ file: SourceFile;
374
+ kind: RootKind;
375
+ source: string;
376
+ exports: boolean;
377
+ names?: readonly string[];
378
+ }[] = [];
379
+ const enter = (
380
+ file: SourceFile,
381
+ kind: RootKind,
382
+ source: string,
383
+ exports: boolean,
384
+ names?: readonly string[],
385
+ ): void => {
386
+ entered.push({ file, kind, source, exports, ...(names === undefined ? {} : { names }) });
313
387
  };
314
388
 
315
389
  const points = entryPoints(
@@ -344,11 +418,28 @@ export function roots<Brand>(
344
418
  ...scripted,
345
419
  ...points.filter((point) => READ_BACK_RULES.has(point.rule)).map((point) => point.file),
346
420
  ];
421
+ const entryFilesOf = new Map<ManifestEntry, readonly SourceFile[]>();
347
422
  const entryFiles = (entry: ManifestEntry): readonly SourceFile[] => {
423
+ const known = entryFilesOf.get(entry);
424
+ if (known !== undefined) {
425
+ return known;
426
+ }
427
+ const found = readEntryFiles(entry);
428
+ entryFilesOf.set(entry, found);
429
+ return found;
430
+ };
431
+ const readEntryFiles = (entry: ManifestEntry): readonly SourceFile[] => {
348
432
  const named = files.named(entry.path);
349
- return named.length > 0 || entry.role === "script"
350
- ? named
351
- : readBackByName(options.host, entry, readBackTo);
433
+ if (named.length > 0 || entry.role === "script") {
434
+ return named;
435
+ }
436
+ const cwd = options.host.workingDirectory();
437
+ const shipped = files.namedAsOutput(resolvePath(cwd, entry.dir), resolvePath(cwd, entry.path));
438
+ if (shipped.length > 0) {
439
+ return shipped;
440
+ }
441
+ const byName = readBackByName(options.host, entry, readBackTo);
442
+ return byName.length > 0 ? byName : throughOutside(options, targetRoot, files, entry);
352
443
  };
353
444
  for (const { entry, publishes } of entries) {
354
445
  if (publishes) {
@@ -356,27 +447,41 @@ export function roots<Brand>(
356
447
  }
357
448
  }
358
449
  const publishedNames = new Set(published.map((file) => file.fileName));
359
- for (const { entry } of entries) {
360
- for (const file of entryFiles(entry)) {
361
- const kind = ENTRY_KINDS[entry.role];
362
- enter(file, kind, entry.member, !publishedNames.has(file.fileName));
363
- }
364
- }
450
+ const patterned: { file: SourceFile; pattern: string }[] = [];
365
451
  for (const pattern of options.entryFiles) {
366
452
  const expression = globExpression(pattern);
367
453
  for (const [path, file] of files.byPath) {
368
454
  if (expression.test(path)) {
369
- enter(file, "entry-file", pattern, true);
455
+ patterned.push({ file, pattern });
370
456
  }
371
457
  }
372
458
  }
459
+ const unmapped: ManifestEntry[] = [];
460
+ const mapped = patterned.map(({ file }) => file);
461
+ for (const { entry } of entries) {
462
+ const named = entryFiles(entry);
463
+ if (named.length === 0 && entry.role !== "script") {
464
+ unmapped.push(entry);
465
+ }
466
+ mapped.push(...named);
467
+ for (const file of named) {
468
+ const kind = ENTRY_KINDS[entry.role];
469
+ enter(file, kind, entry.member, !publishedNames.has(file.fileName));
470
+ }
471
+ }
472
+ const unmappedEntries = unmapped.filter(
473
+ (entry) => readBackByName(options.host, entry, mapped).length === 0,
474
+ );
475
+ for (const { file, pattern } of patterned) {
476
+ enter(file, "entry-file", pattern, true);
477
+ }
373
478
  for (const { file, row } of options.conventions === undefined
374
479
  ? []
375
480
  : conventionEntries(options.conventions, files)) {
376
481
  enter(file, "convention", row, true);
377
482
  }
378
483
  for (const point of points) {
379
- enter(point.file, point.rule, point.source, point.exports);
484
+ enter(point.file, point.rule, point.source, point.exports, point.names);
380
485
  }
381
486
  for (const file of files.byPath.values()) {
382
487
  for (const alias of typeQueryAliases(file)) {
@@ -390,7 +495,11 @@ export function roots<Brand>(
390
495
  }
391
496
  }
392
497
 
393
- const { tables: exportsOf, answered } = exportTables(project, held, [
498
+ const {
499
+ tables: exportsOf,
500
+ named,
501
+ answered,
502
+ } = exportTables(project, held, [
394
503
  ...entered.filter((entry) => entry.exports).map((entry) => entry.file),
395
504
  ...published,
396
505
  ]);
@@ -401,8 +510,13 @@ export function roots<Brand>(
401
510
  }
402
511
  add(id, entry.kind, entry.source);
403
512
  if (entry.exports) {
404
- for (const exported of exportsOf.get(entry.file.fileName) ?? []) {
405
- add(exported, entry.kind, entry.source);
513
+ const byName = named.get(entry.file.fileName);
514
+ const exported =
515
+ entry.names === undefined || byName === undefined
516
+ ? (exportsOf.get(entry.file.fileName) ?? [])
517
+ : entry.names.flatMap((name) => byName.get(name) ?? []);
518
+ for (const one of exported) {
519
+ add(one, entry.kind, entry.source);
406
520
  }
407
521
  }
408
522
  }
@@ -433,6 +547,7 @@ export function roots<Brand>(
433
547
  liveUnderReachability: ordered,
434
548
  unmatched,
435
549
  unanswered: answered ? api.guessed : held.symbols.map((symbol) => symbol.id),
550
+ unmappedEntries,
436
551
  };
437
552
  }
438
553
 
@@ -448,12 +563,18 @@ function exportTables<Brand>(
448
563
  project: ProjectView<Brand>,
449
564
  held: Inventory,
450
565
  wanted: readonly SourceFile[],
451
- ): { readonly tables: ReadonlyMap<string, readonly string[]>; readonly answered: boolean } {
566
+ ): {
567
+ readonly tables: ReadonlyMap<string, readonly string[]>;
568
+ /** Per file whose table the checker answered, the declarations each export name reaches. */
569
+ readonly named: ReadonlyMap<string, ReadonlyMap<string, readonly string[]>>;
570
+ readonly answered: boolean;
571
+ } {
452
572
  const unique = [...new Map(wanted.map((file) => [file.fileName, file])).values()];
453
573
  const tables = new Map<string, readonly string[]>();
574
+ const named = new Map<string, Map<string, string[]>>();
454
575
  let answered = true;
455
576
  if (unique.length === 0) {
456
- return { tables, answered };
577
+ return { tables, named, answered };
457
578
  }
458
579
  const modules = project.symbolsAt(unique.map((file) => project.handle(file)));
459
580
  const chains = aliasChains(project, held);
@@ -475,15 +596,18 @@ function exportTables<Brand>(
475
596
  return;
476
597
  }
477
598
  const ids = new Set<string>();
599
+ const byName = new Map<string, string[]>();
478
600
  for (const exported of table) {
479
601
  for (const target of chains.chainOf(exported)) {
480
602
  answered &&= !target.guessed;
481
603
  ids.add(target.id);
604
+ byName.set(exported.name, [...(byName.get(exported.name) ?? []), target.id]);
482
605
  }
483
606
  }
484
607
  tables.set(file.fileName, [...ids]);
608
+ named.set(file.fileName, byName);
485
609
  });
486
- return { tables, answered };
610
+ return { tables, named, answered };
487
611
  }
488
612
 
489
613
  /** A release tag of a documentation comment: `@` and its name, between white space or the ends. */
@@ -553,11 +677,13 @@ function publishedAPI<Brand>(
553
677
  const nodes = declarationNodes(held, byPath);
554
678
  const chains = aliasChains(project, held);
555
679
  const reached = new Set<string>();
680
+ const entered = new Set<string>();
556
681
  const guessed = new Set<string>();
557
682
  const pending: string[] = [];
558
683
  const reach = (id: string): void => {
559
- if (!reached.has(id) && byId.get(id)?.kind !== "file") {
560
- reached.add(id);
684
+ const file = byId.get(id)?.kind === "file";
685
+ if (!(file ? entered : reached).has(id)) {
686
+ (file ? entered : reached).add(id);
561
687
  pending.push(id);
562
688
  }
563
689
  };
@@ -566,11 +692,7 @@ function publishedAPI<Brand>(
566
692
  }
567
693
  for (const file of published) {
568
694
  for (const id of exportsOf.get(file.fileName) ?? []) {
569
- if (byId.get(id)?.kind === "file") {
570
- pending.push(id);
571
- } else {
572
- reach(id);
573
- }
695
+ reach(id);
574
696
  }
575
697
  }
576
698
  while (pending.length > 0) {
@@ -595,8 +717,7 @@ function publishedAPI<Brand>(
595
717
  project,
596
718
  round.flatMap((id) => {
597
719
  const symbol = byId.get(id);
598
- const node = symbol === undefined ? undefined : nodes.nodeOf(symbol);
599
- return node === undefined ? [] : [node];
720
+ return symbol === undefined ? [] : nodes.nodesOf(symbol);
600
721
  }),
601
722
  )) {
602
723
  for (const target of chains.chainOf(symbol)) {
@@ -625,24 +746,22 @@ function declarationNodes(
625
746
  byPath: ReadonlyMap<string, SourceFile>,
626
747
  ): {
627
748
  typeNamesOf(symbol: InventorySymbol): readonly Node[];
628
- nodeOf(symbol: InventorySymbol): Node | undefined;
749
+ nodesOf(symbol: InventorySymbol): readonly Node[];
629
750
  } {
630
- const walked = new Map<string, { byId: Map<string, Node>; nodes: Set<Node> }>();
631
- const nodesIn = (path: string): { byId: Map<string, Node>; nodes: Set<Node> } => {
751
+ const walked = new Map<string, { byId: Map<string, Node[]>; nodes: Set<Node> }>();
752
+ const nodesIn = (path: string): { byId: Map<string, Node[]>; nodes: Set<Node> } => {
632
753
  const known = walked.get(path);
633
754
  if (known !== undefined) {
634
755
  return known;
635
756
  }
636
- const found = { byId: new Map<string, Node>(), nodes: new Set<Node>() };
757
+ const found = { byId: new Map<string, Node[]>(), nodes: new Set<Node>() };
637
758
  const file = byPath.get(path);
638
759
  if (file !== undefined) {
639
760
  const visit = (node: Node): void => {
640
761
  const id = held.declarations.get(nodeKey(file, node));
641
762
  if (id !== undefined) {
642
763
  found.nodes.add(node);
643
- if (!found.byId.has(id)) {
644
- found.byId.set(id, node);
645
- }
764
+ found.byId.set(id, [...(found.byId.get(id) ?? []), node]);
646
765
  }
647
766
  node.forEachChild(visit);
648
767
  };
@@ -654,10 +773,9 @@ function declarationNodes(
654
773
  return {
655
774
  typeNamesOf: (symbol) => {
656
775
  const { byId, nodes } = nodesIn(symbol.position.path);
657
- const declaration = byId.get(symbol.id);
658
- return declaration === undefined ? [] : typeNamesOf(declaration, nodes);
776
+ return (byId.get(symbol.id) ?? []).flatMap((declaration) => typeNamesOf(declaration, nodes));
659
777
  },
660
- nodeOf: (symbol) => nodesIn(symbol.position.path).byId.get(symbol.id),
778
+ nodesOf: (symbol) => nodesIn(symbol.position.path).byId.get(symbol.id) ?? [],
661
779
  };
662
780
  }
663
781
 
package/src/schema.ts CHANGED
@@ -106,6 +106,7 @@ export const SCHEMA_ROOT: KeyNode = {
106
106
  members: { components: declarations, bases: declarations, members: leaf },
107
107
  },
108
108
  serializers: declarations,
109
+ roots_complete: leaf,
109
110
  },
110
111
  },
111
112
  provenance: { kind: "map" },
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * The serialization-contract class: the data members and `toJSON` of every type a value's
3
3
  * static type reaches, where the value reaches `JSON.stringify` or a call `ts.serializers`
4
- * names, and `toString` too where it reaches an `unknown` or `any` parameter of a function
5
- * outside the analysis or an erased property of a type the program does not declare. A
6
- * function of the project handing such a parameter to a destination is one, to a fixpoint,
7
- * and a function invoked through `call`, `apply` or `bind` is the callee.
4
+ * names, and `toString` too where it reaches an erased parameter of a function outside the
5
+ * analysis, or an erased property of a type the program does not declare. A function of
6
+ * the project handing such a parameter to a destination is one, to a fixpoint, and a
7
+ * function invoked through `call`, `apply` or `bind` is the callee.
8
8
  */
9
9
 
10
10
  import {
@@ -80,6 +80,9 @@ const SERIALIZED_CONVERSION = "toJSON";
80
80
  /** The parameter types that keep nothing of the value they are given. */
81
81
  const ERASED = TypeFlags.Any | TypeFlags.Unknown;
82
82
 
83
+ /** How deep a callee's result is searched for a type parameter: `Promise<Box<T>>` is two levels. */
84
+ const MENTION_DEPTH = 3;
85
+
83
86
  /**
84
87
  * What a destination holds back of a class: its properties, or its properties and its
85
88
  * two conversion methods. The second holds back everything the first does.
@@ -564,6 +567,30 @@ function destinations<Brand>(
564
567
  });
565
568
  return containing.length === 0 ? target.declarations : containing;
566
569
  };
570
+ /** Whether a type names a type parameter, through constituents, type arguments and call signatures. */
571
+ const mentions = (type: Type, parameter: Type, depth: number): boolean => {
572
+ // A conditional type an alias names, `Awaited<T>` among them, carries its arguments
573
+ // only on the alias the client's type object reports.
574
+ const aliased = (type as { readonly aliasTypeArguments?: readonly number[] })
575
+ .aliasTypeArguments;
576
+ if (type.id === parameter.id || aliased?.includes(parameter.id) === true) {
577
+ return true;
578
+ }
579
+ if (depth >= MENTION_DEPTH) {
580
+ return false;
581
+ }
582
+ const inner = [
583
+ ...(type.isUnionType() || type.isIntersectionType() ? must(queries.constituents(type)) : []),
584
+ ...must(queries.typeArguments(type)),
585
+ ...must(queries.signaturesOf(type, SignatureKind.Call)).flatMap((signature) => [
586
+ ...signature.parameters.map((_parameter, at) =>
587
+ must(queries.typeAtPosition(signature, at)),
588
+ ),
589
+ ...[must(queries.returnTypeOf(signature))].filter((one) => one !== undefined),
590
+ ]),
591
+ ];
592
+ return inner.some((one) => mentions(one, parameter, depth + 1));
593
+ };
567
594
  const erasedArguments = new Map<CallExpression, ReadonlySet<number>>();
568
595
  const erasedAt = (call: CallExpression, argument: number): boolean => {
569
596
  let indexes = erasedArguments.get(call);
@@ -577,8 +604,32 @@ function destinations<Brand>(
577
604
  ? []
578
605
  : must(queries.signaturesOf(type, SignatureKind.Call))
579
606
  : [resolvedOf(call)].filter((one) => one !== undefined);
607
+ // A type parameter of the callee that nothing constrains and its result does not name
608
+ // keeps nothing of the value either: the instantiated signature names the value's own
609
+ // type there instead, and a result that names it hands the value back.
610
+ const generic = invoked.indirect ? undefined : resolvedOf(call);
611
+ const declared = generic === undefined ? undefined : must(queries.genericOf(generic));
612
+ const unconstrained = (index: number): boolean => {
613
+ const parameter =
614
+ declared === undefined ? undefined : must(queries.parameterType(declared, index));
615
+ const parts =
616
+ parameter === undefined
617
+ ? []
618
+ : parameter.isUnionType()
619
+ ? must(queries.constituents(parameter))
620
+ : [parameter];
621
+ const result = declared === undefined ? undefined : must(queries.returnTypeOf(declared));
622
+ return parts.some((part) => {
623
+ if (!part.isTypeParameter() || (result !== undefined && mentions(result, part, 0))) {
624
+ return false;
625
+ }
626
+ const constraint = must(queries.constraintOf(part));
627
+ return constraint === undefined || (constraint.flags & ERASED) !== 0;
628
+ });
629
+ };
580
630
  indexes = new Set(
581
631
  invoked.arguments.flatMap((_unused, index) =>
632
+ unconstrained(index) ||
582
633
  signatures.some((signature) => {
583
634
  const parameter = must(queries.parameterType(signature, index));
584
635
  return parameter !== undefined && (parameter.flags & ERASED) !== 0;
package/src/session.ts CHANGED
@@ -129,6 +129,7 @@ export function openEngine(options: EngineOptions): Engine {
129
129
  ...(mapper === undefined ? {} : { runExternalCode: true }),
130
130
  });
131
131
  let closed = false;
132
+ const parsed = new Map<string, { readonly text: string; readonly file: SourceFile }>();
132
133
  return {
133
134
  parseConfigFile: (file) => api.parseConfigFile(file),
134
135
  parseConfigJson: (json, configFile) =>
@@ -160,8 +161,14 @@ export function openEngine(options: EngineOptions): Engine {
160
161
  moduleResolutions: { fallback: "resolve", entries: [...entries] },
161
162
  }),
162
163
  parseSourceFile: (fileName, text) => {
164
+ // Every project of a run reads the same files outside its program, so one parse serves all.
165
+ const known = parsed.get(fileName);
166
+ if (known?.text === text) {
167
+ return known.file;
168
+ }
163
169
  const retained = api.createSourceFile(fileName, text);
164
170
  try {
171
+ parsed.set(fileName, { text, file: retained.sourceFile });
165
172
  return retained.sourceFile;
166
173
  } finally {
167
174
  retained.dispose();
@@ -6,12 +6,18 @@
6
6
 
7
7
  import type { SourceFile } from "@typescript/native/ast";
8
8
  import { commonDirectory, emittedFrom } from "./emit-map.ts";
9
- import { dirnamePath, relativePath, resolvePath } from "./paths.ts";
9
+ import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.ts";
10
10
  import type { ProjectView } from "./session.ts";
11
11
 
12
12
  /** The one character a subpath pattern of a manifest's `exports` holds. */
13
13
  const SUBPATH_WILDCARD = "*";
14
14
 
15
+ /** The extension a source file's name drops when the project writes it out. */
16
+ const SOURCE_EXTENSION = /\.[cm]?[jt]sx?$/u;
17
+
18
+ /** The extensions the project may write a source file's output under. */
19
+ const OUTPUT_EXTENSIONS = [".js", ".mjs", ".cjs", ".jsx", ".d.ts", ".d.mts", ".d.cts"] as const;
20
+
15
21
  /**
16
22
  * Whether one path is what a subpath pattern stands for: the text before the
17
23
  * wildcard, any run of characters including the solidus, then the text after it.
@@ -40,6 +46,47 @@ export interface SourceFiles {
40
46
  named(path: string): readonly SourceFile[];
41
47
  /** The directories the project writes its output below, absolute: its output and declaration directories. */
42
48
  readonly outputDirs: readonly string[];
49
+ /** The output paths, absolute, the project writes one own file to, each of which {@link named} reads back to it. */
50
+ writtenAs(file: SourceFile): readonly string[];
51
+ /**
52
+ * The own files one absolute path names read as an output of the project written below
53
+ * `outputDir`: a manifest that ships from its build directory names its entries there.
54
+ */
55
+ namedAsOutput(outputDir: string, path: string): readonly SourceFile[];
56
+ }
57
+
58
+ /** The first own file of the paths given that the project holds, or none. */
59
+ function heldAt(
60
+ byName: ReadonlyMap<string, SourceFile>,
61
+ paths: readonly string[],
62
+ ): readonly SourceFile[] {
63
+ for (const candidate of paths) {
64
+ const file = byName.get(candidate);
65
+ if (file !== undefined) {
66
+ return [file];
67
+ }
68
+ }
69
+ return [];
70
+ }
71
+
72
+ /** Where one project writes its output, and the directory it writes it from, absolute. */
73
+ function layoutOf<Brand>(
74
+ project: ProjectView<Brand>,
75
+ fileNames: readonly string[],
76
+ ): {
77
+ readonly outDir: string | undefined;
78
+ readonly declarationDir: string | undefined;
79
+ readonly sourceDir: string;
80
+ } {
81
+ const options = project.program.getCompilerOptions();
82
+ const configDir = dirnamePath(project.configFile);
83
+ const at = (option: string | undefined): string | undefined =>
84
+ option === undefined ? undefined : resolvePath(configDir, option);
85
+ return {
86
+ outDir: at(options.outDir),
87
+ declarationDir: at(options.declarationDir),
88
+ sourceDir: at(options.rootDir) ?? commonDirectory(fileNames),
89
+ };
43
90
  }
44
91
 
45
92
  /**
@@ -62,14 +109,7 @@ export function sourceFilesOf<Brand>(project: ProjectView<Brand>, targetRoot: st
62
109
  }
63
110
  }
64
111
 
65
- const options = project.program.getCompilerOptions();
66
- const configDir = dirnamePath(project.configFile);
67
- const at = (option: string | undefined): string | undefined =>
68
- option === undefined ? undefined : resolvePath(configDir, option);
69
- const outDir = at(options.outDir);
70
- const declarationDir = at(options.declarationDir);
71
- const sourceDir = at(options.rootDir) ?? commonDirectory([...byName.keys()]);
72
-
112
+ const { outDir, declarationDir, sourceDir } = layoutOf(project, [...byName.keys()]);
73
113
  const layout = { outDir, declarationDir, sourceDirs: [sourceDir] };
74
114
 
75
115
  /** The source paths one written path may stand for, the path itself first. */
@@ -79,15 +119,25 @@ export function sourceFilesOf<Brand>(project: ProjectView<Brand>, targetRoot: st
79
119
  byPath,
80
120
  byName,
81
121
  outputDirs: [...new Set([outDir, declarationDir])].filter((dir) => dir !== undefined),
122
+ writtenAs: (file) => {
123
+ const below = relativePath(sourceDir, file.fileName);
124
+ if (below === undefined) {
125
+ return [];
126
+ }
127
+ const written = [outDir, declarationDir].flatMap((dir) =>
128
+ dir === undefined
129
+ ? []
130
+ : OUTPUT_EXTENSIONS.map((extension) =>
131
+ joinPath(dir, below.replace(SOURCE_EXTENSION, extension)),
132
+ ),
133
+ );
134
+ return [...new Set(written)].filter((path) => heldAt(byName, candidates(path))[0] === file);
135
+ },
136
+ namedAsOutput: (outputDir, path) =>
137
+ heldAt(byName, emittedFrom(path, { outDir: outputDir, sourceDirs: [sourceDir] })),
82
138
  named: (path) => {
83
139
  if (!path.includes(SUBPATH_WILDCARD)) {
84
- for (const candidate of candidates(path)) {
85
- const held = byName.get(candidate);
86
- if (held !== undefined) {
87
- return [held];
88
- }
89
- }
90
- return [];
140
+ return heldAt(byName, candidates(path));
91
141
  }
92
142
  const patterns = candidates(path).filter((candidate) => candidate.includes(SUBPATH_WILDCARD));
93
143
  return [...byName.values()].filter((file) =>
@@ -19,6 +19,7 @@ import {
19
19
  UNSCOPED_ENTRY,
20
20
  blankReason,
21
21
  isEntryPath,
22
+ isPartKind,
22
23
  isSymbolRef,
23
24
  siteText,
24
25
  type Mechanism,
@@ -155,13 +156,23 @@ function readRecord(value: unknown, site: Position, byReference: ByReference, sh
155
156
  if (refusals.length > 0) {
156
157
  return { records: [], refusals };
157
158
  }
158
- const named = byReference.get(declarationKey(symbol, written.path)) ?? [];
159
+ const named =
160
+ byReference.get(declarationKey(symbol, written.path)) ??
161
+ (isPartKind(code)
162
+ ? byReference.get(declarationKey(parameterHolder(symbol), written.path))
163
+ : undefined) ??
164
+ [];
159
165
  if (named.length === 0) {
160
166
  return { records: [{ ...written, bound: "" }], refusals: [] };
161
167
  }
162
168
  return { records: named.map((one) => ({ ...written, bound: one.id })), refusals: [] };
163
169
  }
164
170
 
171
+ /** The reference of the declaration a parameter's reference names it under, or `ref` itself. */
172
+ function parameterHolder(ref: string): string {
173
+ return ref.endsWith(")") ? ref.slice(0, ref.lastIndexOf("(")) : ref;
174
+ }
175
+
165
176
  function byReferenceOf(symbols: readonly InventorySymbol[]): ByReference {
166
177
  const byReference = new Map<string, InventorySymbol[]>();
167
178
  for (const symbol of symbols) {