@cplieger/deadset-ts 8.0.0-dev.1 → 8.0.0-dev.3

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 (66) hide show
  1. package/dist/src/analysis.js +30 -6
  2. package/dist/src/augmentations.js +105 -12
  3. package/dist/src/build-scripts.js +437 -0
  4. package/dist/src/conformance-record.js +1 -1
  5. package/dist/src/convention-rows.js +62 -2
  6. package/dist/src/conventions.js +15 -0
  7. package/dist/src/dependencies.js +89 -0
  8. package/dist/src/entry-points.js +9 -18
  9. package/dist/src/findings/completion.js +122 -49
  10. package/dist/src/findings/intra-function.js +4 -2
  11. package/dist/src/findings/reads-and-writes.js +7 -6
  12. package/dist/src/findings/unused-declarations.js +13 -5
  13. package/dist/src/findings/visibility-narrowing.js +8 -3
  14. package/dist/src/graph.js +5 -3
  15. package/dist/src/intra-function-parts.js +45 -17
  16. package/dist/src/kinds.js +8 -8
  17. package/dist/src/manifest.js +18 -0
  18. package/dist/src/matrix.js +76 -4
  19. package/dist/src/references.js +34 -3
  20. package/dist/src/report.js +23 -4
  21. package/dist/src/reporters/sarif.js +12 -8
  22. package/dist/src/roots.js +138 -11
  23. package/dist/src/serialization-contract.js +258 -8
  24. package/dist/src/suppress.js +1 -0
  25. package/dist/src/sweep.js +7 -9
  26. package/dist/src/test-globals.js +234 -0
  27. package/dist/src/test-support.js +8 -11
  28. package/dist/src/type-errors.js +11 -3
  29. package/dist/src/unseen-loads.js +107 -0
  30. package/dist/src/verbs/analyze.js +24 -2
  31. package/dist/src/yaml-document.js +355 -0
  32. package/jsr.json +1 -1
  33. package/package.json +1 -1
  34. package/src/analysis.ts +48 -8
  35. package/src/augmentations.ts +134 -16
  36. package/src/build-scripts.ts +564 -0
  37. package/src/conformance-record.ts +1 -1
  38. package/src/convention-rows.ts +72 -2
  39. package/src/conventions.ts +16 -0
  40. package/src/dependencies.ts +96 -0
  41. package/src/entry-points.ts +10 -25
  42. package/src/file-facts.ts +3 -0
  43. package/src/finding.ts +5 -0
  44. package/src/findings/completion.ts +153 -56
  45. package/src/findings/intra-function.ts +4 -2
  46. package/src/findings/reads-and-writes.ts +7 -6
  47. package/src/findings/unused-declarations.ts +17 -5
  48. package/src/findings/visibility-narrowing.ts +9 -3
  49. package/src/graph.ts +5 -3
  50. package/src/intra-function-parts.ts +55 -16
  51. package/src/kinds.ts +8 -8
  52. package/src/manifest.ts +21 -3
  53. package/src/matrix.ts +122 -4
  54. package/src/references.ts +38 -2
  55. package/src/report.ts +56 -16
  56. package/src/reporters/sarif.ts +16 -8
  57. package/src/roots.ts +162 -12
  58. package/src/serialization-contract.ts +309 -7
  59. package/src/suppress.ts +1 -0
  60. package/src/sweep.ts +21 -9
  61. package/src/test-globals.ts +275 -0
  62. package/src/test-support.ts +17 -13
  63. package/src/type-errors.ts +14 -3
  64. package/src/unseen-loads.ts +145 -0
  65. package/src/verbs/analyze.ts +28 -2
  66. package/src/yaml-document.ts +377 -0
package/src/analysis.ts CHANGED
@@ -107,6 +107,7 @@ import { references, testFileRulesOf, type Reference, type TestFileRule } from "
107
107
  import {
108
108
  roots,
109
109
  unmatchedEverywhere,
110
+ type Root,
110
111
  type RootKind,
111
112
  type RootOptions,
112
113
  type Roots,
@@ -114,7 +115,10 @@ import {
114
115
  import type { Scope } from "./scope.ts";
115
116
  import { serializationContract } from "./serialization-contract.ts";
116
117
  import { outsideTestReferences, testFilesAt, type OutsideTest } from "./outside-tests.ts";
118
+ import { mergedGlobalInterfaces } from "./augmentations.ts";
119
+ import { unseenLoadsOf, type UnseenLoad } from "./unseen-loads.ts";
117
120
  import { joinedTestFiles, withJoinedTests } from "./test-joins.ts";
121
+ import { GLOBALS_RUNNER, namesTestGlobals, withTestGlobals } from "./test-globals.ts";
118
122
  import { supportReferences, testSupportFiles, type TestEvidence } from "./test-support.ts";
119
123
  import {
120
124
  diagnosticsOf,
@@ -428,11 +432,17 @@ function readProjects<Answer>(
428
432
  const joins = discovered.derived
429
433
  ? joinedTestFiles(engine, host, absoluteRoot, discovered.configFiles, testPatterns)
430
434
  : new Map<string, readonly string[]>();
431
- const programs = withBaseUrlPaths(
435
+ const programs = withTestGlobals(
432
436
  engine,
433
437
  host,
434
- [...discovered.configFiles, ...consumerOf.keys()],
435
- workspace?.programs ?? new Map(),
438
+ parse,
439
+ discovered.configFiles,
440
+ withBaseUrlPaths(
441
+ engine,
442
+ host,
443
+ [...discovered.configFiles, ...consumerOf.keys()],
444
+ workspace?.programs ?? new Map(),
445
+ ),
436
446
  );
437
447
  const session = runSession(
438
448
  engine,
@@ -804,6 +814,8 @@ interface ReadProject<Extra> {
804
814
  readonly componentWarnings: readonly ComponentWarning[];
805
815
  /** Per own file below the target root, the module specifiers its imports write. */
806
816
  readonly imports: ReadonlyMap<string, readonly string[]>;
817
+ /** The global augmentations a declaration outside the project names. */
818
+ readonly augmented: readonly Root[];
807
819
  readonly extra: Extra;
808
820
  }
809
821
 
@@ -940,6 +952,9 @@ function readRun<Extra>(
940
952
  disabled,
941
953
  );
942
954
  const components = project.ownSourceFiles().filter(isComponentFile);
955
+ const injected = project.program
956
+ .getSourceFileNames()
957
+ .some((name) => name.endsWith(`/node_modules/${GLOBALS_RUNNER}/globals.d.ts`));
943
958
  const pathOf = (file: (typeof components)[number]): string =>
944
959
  relativePath(targetRoot, file.fileName) ?? file.fileName;
945
960
  return {
@@ -948,12 +963,27 @@ function readRun<Extra>(
948
963
  testFiles: resolved.testFilePaths,
949
964
  evidence,
950
965
  componentFiles: components.map(pathOf),
966
+ augmented: mergedGlobalInterfaces(
967
+ project,
968
+ projectRead.held,
969
+ sourceFilesOf(project, targetRoot),
970
+ ).map((id): Root => ({ id, kind: "global-augmentation", source: "" })),
951
971
  imports: new Map(
952
972
  project.ownSourceFiles().flatMap((file) => {
953
973
  const path = relativePath(targetRoot, file.fileName);
954
- return path === undefined
955
- ? []
956
- : [[path, file.imports.filter(isStringLiteralLikeNode).map((one) => one.text)]];
974
+ if (path === undefined) {
975
+ return [];
976
+ }
977
+ const specifiers = file.imports.filter(isStringLiteralLikeNode).map((one) => one.text);
978
+ // A file calling the globals a program declares is written for their runner.
979
+ return [
980
+ [
981
+ path,
982
+ injected && namesTestGlobals(project, file)
983
+ ? [...specifiers, GLOBALS_RUNNER]
984
+ : specifiers,
985
+ ],
986
+ ];
957
987
  }),
958
988
  ),
959
989
  componentWarnings: components.flatMap((file) =>
@@ -977,7 +1007,7 @@ function readRun<Extra>(
977
1007
  }));
978
1008
  // Test-support code is code only test code imports in any configuration of the run: a
979
1009
  // configuration that holds no test file says nothing about what the tests import.
980
- const support = testSupportFiles(
1010
+ const { support, testUsed } = testSupportFiles(
981
1011
  withOutside.flatMap((one) => one.projectRead.held.symbols),
982
1012
  withOutside.flatMap((one) => one.references),
983
1013
  withOutside.flatMap((one) => one.projectRead.rooted.liveUnderReachability),
@@ -999,13 +1029,17 @@ function readRun<Extra>(
999
1029
  const made = supportReferences([...one.references, ...(satisfied?.references ?? [])], support);
1000
1030
  const held = new Set(projectRead.held.symbols.map((symbol) => symbol.position.path));
1001
1031
  const supportFiles = [...support].filter((path) => held.has(path)).sort();
1032
+ const testUsedFiles = [...testUsed].filter((path) => held.has(path)).sort();
1002
1033
  const configured: Configured = {
1003
1034
  configuration: projectRead.configuration,
1004
1035
  symbols: projectRead.held.symbols,
1005
1036
  references: [...made, ...evidenceReferences(projectRead.held.symbols, detected.inTestFiles)],
1006
- roots: projectRead.rooted.liveUnderReachability,
1037
+ // The augmentations are rooted after test support is read: an outside declaration
1038
+ // naming one calls nothing of its file.
1039
+ roots: [...projectRead.rooted.liveUnderReachability, ...one.augmented],
1007
1040
  testFiles: [...one.testFiles, ...supportFiles],
1008
1041
  ...(supportFiles.length === 0 ? {} : { supportFiles }),
1042
+ ...(testUsedFiles.length === 0 ? {} : { testUsedFiles }),
1009
1043
  ...(projectRead.held.ambient.size === 0 ? {} : { ambient: projectRead.held.ambient }),
1010
1044
  ...(projectRead.referenceOnly.length === 0
1011
1045
  ? {}
@@ -1244,6 +1278,7 @@ interface EmitterExtra {
1244
1278
  readonly directives: readonly InlineDirective[];
1245
1279
  readonly declarationsNamed: readonly boolean[];
1246
1280
  readonly parts: ProjectParts;
1281
+ readonly unseenLoads: readonly UnseenLoad[];
1247
1282
  }
1248
1283
 
1249
1284
  /** Reads a run's projects with every per-project fact an emitter reads. */
@@ -1279,6 +1314,7 @@ export function readEmitterRun(
1279
1314
  ),
1280
1315
  declarationsNamed: declarationsNamed(project, read.held, configuredDeclarations(config)),
1281
1316
  parts: projectParts(project, read.held, references, targetRoot),
1317
+ unseenLoads: unseenLoadsOf(project, read.configuration, targetRoot, testFiles),
1282
1318
  }),
1283
1319
  );
1284
1320
  // Whether a declaration carries the marker decides its code, so one whose marker went
@@ -1384,6 +1420,7 @@ export function emitterInputOver(
1384
1420
  heldByInclusion: new Set(projects.flatMap((one) => one.extra.heldByInclusion)),
1385
1421
  generated: new Set(projects.flatMap((one) => one.extra.generated)),
1386
1422
  inOperands: new Set(projects.flatMap((one) => one.extra.inOperands)),
1423
+ unseenLoads: projects.flatMap((one) => one.extra.unseenLoads),
1387
1424
  },
1388
1425
  boundary: {
1389
1426
  consumers: {
@@ -1474,6 +1511,7 @@ function readAnalysis(
1474
1511
  librariesUnfound: read.librariesUnfound,
1475
1512
  conventionsApplied: read.conventionsApplied,
1476
1513
  unmappedEntries: unmappedEverywhere(read.projects),
1514
+ unseenLoads: read.projects.flatMap((one) => one.extra.unseenLoads),
1477
1515
  },
1478
1516
  configured: read.projects.map((one) => one.configured),
1479
1517
  symbols,
@@ -1572,6 +1610,8 @@ interface RunFacts {
1572
1610
  readonly conventionsApplied: readonly AppliedConvention[];
1573
1611
  /** The manifest entries no project of the run reads back to a file, each once. */
1574
1612
  readonly unmappedEntries: readonly ManifestEntry[];
1613
+ /** The loads of production files whose target the run cannot resolve, per configuration. */
1614
+ readonly unseenLoads: readonly UnseenLoad[];
1575
1615
  }
1576
1616
 
1577
1617
  /** One configuration whose component files a run cannot read. */
@@ -7,9 +7,18 @@
7
7
  * as `import.meta` or `process.env`, so a use of its member is a use of it.
8
8
  */
9
9
 
10
- import type { Inventory, InventorySymbol } from "./inventory.ts";
10
+ import {
11
+ isInterfaceDeclaration,
12
+ isModuleBlock,
13
+ isModuleDeclaration,
14
+ type Node,
15
+ } from "@typescript/native/ast";
16
+ import { nodeKey, type Inventory, type InventorySymbol } from "./inventory.ts";
11
17
  import { referenceKey } from "./matrix.ts";
12
18
  import type { Reference } from "./references.ts";
19
+ import type { ProjectView } from "./session.ts";
20
+ import type { SourceFiles } from "./source-files.ts";
21
+ import type { NodeHandle } from "@typescript/native/sync";
13
22
 
14
23
  /** Whether one declaration is a `declare global` block: a module-level namespace named `global`. */
15
24
  function isGlobalBlock(
@@ -26,25 +35,24 @@ function isGlobalBlock(
26
35
  }
27
36
 
28
37
  /**
29
- * For each reference to a declaration an augmentation block, or the global scope of a
30
- * file in `scripts`, holds, a read at the same place of every interface and namespace
31
- * between it and the block or the file, and of the block itself. `scripts` holds the
32
- * files that are no module, by identifier.
38
+ * Per declaration, the interfaces and namespaces between it and the augmentation block or
39
+ * the file in `scripts` that holds it, with the block itself, or none where no such block
40
+ * or file holds it; and whether that holder is the global scope rather than a module
41
+ * declaration. `scripts` holds the files that are no module, by identifier.
33
42
  */
34
- export function augmentationReferences(
43
+ function holdersOf(
35
44
  held: Inventory,
36
- found: readonly Reference[],
37
- scripts: ReadonlySet<string> = new Set(),
38
- ): readonly Reference[] {
45
+ scripts: ReadonlySet<string>,
46
+ ): ((id: string) => { readonly ids: readonly string[]; readonly global: boolean }) | undefined {
39
47
  const files = new Set(held.symbols.filter((one) => one.kind === "file").map((one) => one.id));
40
48
  const globals = new Set(
41
49
  held.symbols.filter((one) => isGlobalBlock(one, held, files)).map((one) => one.id),
42
50
  );
43
51
  if (globals.size === 0 && held.modules.size === 0 && scripts.size === 0) {
44
- return [];
52
+ return undefined;
45
53
  }
46
54
  const byId = new Map(held.symbols.map((one) => [one.id, one]));
47
- const holders = (id: string): string[] => {
55
+ return (id) => {
48
56
  const ids: string[] = [];
49
57
  for (
50
58
  let at = byId.get(byId.get(id)?.parent ?? "");
@@ -52,29 +60,139 @@ export function augmentationReferences(
52
60
  at = byId.get(at.parent)
53
61
  ) {
54
62
  if (globals.has(at.id) || held.modules.has(at.id)) {
55
- return [...ids, at.id];
63
+ return { ids: [...ids, at.id], global: globals.has(at.id) };
56
64
  }
57
65
  if (scripts.has(at.id)) {
58
- return ids;
66
+ return { ids, global: true };
59
67
  }
60
68
  if (at.kind === "interface" || at.kind === "namespace") {
61
69
  ids.push(at.id);
62
70
  }
63
71
  }
72
+ return { ids: [], global: false };
73
+ };
74
+ }
75
+
76
+ /**
77
+ * The ambient interfaces of the global scope among `interfaces`: each one a declaration
78
+ * file or a `declare` holds that a `declare global` block, or the top level of a file in
79
+ * `scripts`, declares, directly or inside a namespace there.
80
+ */
81
+ export function ambientGlobalInterfaces(
82
+ held: Inventory,
83
+ interfaces: readonly string[],
84
+ scripts: ReadonlySet<string>,
85
+ ): readonly string[] {
86
+ const holders = holdersOf(held, scripts);
87
+ return holders === undefined
88
+ ? []
89
+ : interfaces.filter((id) => held.ambient.has(id) && holders(id).global);
90
+ }
91
+
92
+ /**
93
+ * Each ambient global interface of the project's own files that merges into an interface
94
+ * declared outside them, with every interface, namespace and block between it and the
95
+ * global scope: the outside declaration names it wherever a value is typed by it. Where
96
+ * every outside declaration is a package's, not the compiler's library, and declares no
97
+ * member, each member it declares as well: such an interface is a slot the package reads
98
+ * by type, as `keyof` does, in declarations the analysis does not walk.
99
+ */
100
+ export function mergedGlobalInterfaces<Brand>(
101
+ project: ProjectView<Brand>,
102
+ held: Inventory,
103
+ files: SourceFiles,
104
+ ): readonly string[] {
105
+ const scripts = new Set<string>();
106
+ const names = new Map<string, Node>();
107
+ for (const file of files.byName.values()) {
108
+ const fileId = held.declarations.get(nodeKey(file, file));
109
+ if (fileId !== undefined && file.externalModuleIndicator === undefined) {
110
+ scripts.add(fileId);
111
+ }
112
+ const visit = (node: Node): void => {
113
+ if (isInterfaceDeclaration(node)) {
114
+ const id = held.declarations.get(nodeKey(file, node));
115
+ if (id !== undefined) {
116
+ names.set(id, node.name);
117
+ }
118
+ } else if (isModuleDeclaration(node) || isModuleBlock(node)) {
119
+ node.forEachChild(visit);
120
+ }
121
+ };
122
+ file.forEachChild(visit);
123
+ }
124
+ const global = new Set(ambientGlobalInterfaces(held, [...names.keys()], scripts));
125
+ const asked = [...names].filter(([id]) => global.has(id));
126
+ if (asked.length === 0) {
64
127
  return [];
128
+ }
129
+ const answers = project.symbolsAt(asked.map(([, name]) => project.handle(name)));
130
+ const holders = holdersOf(held, scripts);
131
+ const empty = (handle: NodeHandle): boolean => {
132
+ if (project.program.getSourceFileMetadata(handle.path)?.isDefaultLibrary === true) {
133
+ return false;
134
+ }
135
+ const node = project.declarationAt(handle)?.node;
136
+ return node !== undefined && isInterfaceDeclaration(node) && node.members.length === 0;
65
137
  };
138
+ return [
139
+ ...new Set(
140
+ asked.flatMap(([id], at) => {
141
+ const symbol = answers[at];
142
+ if (typeof symbol !== "object") {
143
+ return [];
144
+ }
145
+ const outside = symbol.declarations.filter((handle) => !files.byName.has(handle.path));
146
+ if (outside.length === 0) {
147
+ return [];
148
+ }
149
+ const members = outside.every(empty)
150
+ ? held.symbols.filter((one) => one.parent === id).map((one) => one.id)
151
+ : [];
152
+ return [id, ...(holders?.(id).ids ?? []), ...members];
153
+ }),
154
+ ),
155
+ ];
156
+ }
157
+
158
+ /**
159
+ * For each reference to a declaration an augmentation block, or the global scope of a
160
+ * file in `scripts` (the files that are no module), holds, a read at the same place of
161
+ * every interface and namespace between it and the block or the file, and of the block
162
+ * itself. A read through an interface of `merged`, an ambient global interface merging
163
+ * into a language or library one, is a production read whoever writes it: it types a
164
+ * value the runtime provides, so a test reading a member needs it as code does.
165
+ */
166
+ export function augmentationReferences(
167
+ held: Inventory,
168
+ found: readonly Reference[],
169
+ scripts: ReadonlySet<string> = new Set(),
170
+ merged: ReadonlySet<string> = new Set(),
171
+ ): readonly Reference[] {
172
+ const holders = holdersOf(held, scripts);
173
+ if (holders === undefined) {
174
+ return [];
175
+ }
176
+ const fileOf = new Map(
177
+ held.symbols.filter((one) => one.kind === "file").map((one) => [one.position.path, one.id]),
178
+ );
66
179
  // Two uses at one place of declarations one block holds read the block once there.
67
180
  const made = new Set(found.map(referenceKey));
68
181
  const reads = new Map<string, Reference>();
69
182
  for (const reference of found) {
70
- for (const to of holders(reference.to)) {
183
+ const { ids } = holders(reference.to);
184
+ // A test's read through such an interface is held by the test file, which the test
185
+ // runner loads whichever of its declarations runs.
186
+ const typed = reference.test && ids.some((id) => merged.has(id));
187
+ const from = typed ? (fileOf.get(reference.position.path) ?? reference.from) : reference.from;
188
+ for (const to of ids) {
71
189
  const read: Reference = {
72
- from: reference.from,
190
+ from,
73
191
  to,
74
192
  position: reference.position,
75
193
  use: "read",
76
194
  resolution: reference.resolution,
77
- test: reference.test,
195
+ test: reference.test && !typed,
78
196
  };
79
197
  const key = referenceKey(read);
80
198
  if (!made.has(key) && !reads.has(key)) {