@cplieger/deadset-ts 4.0.1 → 5.1.0

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 (138) hide show
  1. package/README.md +4 -4
  2. package/dist/bin/component-mapper.js +5 -0
  3. package/dist/bin/mapper-connection.js +101 -0
  4. package/dist/bin/node-host.js +24 -2
  5. package/dist/src/analysis.js +301 -41
  6. package/dist/src/component-files.js +612 -0
  7. package/dist/src/component-layer.js +132 -0
  8. package/dist/src/config.js +9 -2
  9. package/dist/src/configuration-files.js +290 -0
  10. package/dist/src/conformance-record.js +3 -9
  11. package/dist/src/contextual-uses.js +240 -0
  12. package/dist/src/convention-rows.js +389 -0
  13. package/dist/src/conventions.js +565 -0
  14. package/dist/src/dependencies.js +36 -3
  15. package/dist/src/destructuring.js +6 -6
  16. package/dist/src/discover.js +10 -3
  17. package/dist/src/emit-map.js +72 -0
  18. package/dist/src/entry-point-gaps.js +81 -35
  19. package/dist/src/entry-points.js +50 -32
  20. package/dist/src/exempt-classes.js +6 -1
  21. package/dist/src/exempt.js +32 -20
  22. package/dist/src/findings/completion.js +46 -32
  23. package/dist/src/findings/emitters.js +22 -1
  24. package/dist/src/findings/interfaces.js +6 -2
  25. package/dist/src/findings/reads-and-writes.js +53 -9
  26. package/dist/src/findings/unused-declarations.js +47 -3
  27. package/dist/src/generated-file.js +24 -0
  28. package/dist/src/graph.js +13 -5
  29. package/dist/src/interface-satisfaction.js +2 -68
  30. package/dist/src/inventory.js +8 -3
  31. package/dist/src/kinds.js +6 -5
  32. package/dist/src/manifest.js +37 -11
  33. package/dist/src/matrix.js +21 -13
  34. package/dist/src/partial.js +3 -1
  35. package/dist/src/pnpm-workspace.js +120 -0
  36. package/dist/src/position.js +17 -2
  37. package/dist/src/print.js +2 -0
  38. package/dist/src/query.js +15 -0
  39. package/dist/src/references.js +404 -36
  40. package/dist/src/report.js +26 -6
  41. package/dist/src/reporters/annotations.js +10 -3
  42. package/dist/src/reporters/template.js +3 -0
  43. package/dist/src/resolve.js +37 -11
  44. package/dist/src/roots.js +37 -8
  45. package/dist/src/run.js +13 -3
  46. package/dist/src/schema.js +2 -0
  47. package/dist/src/session.js +141 -34
  48. package/dist/src/setup-failure.js +23 -0
  49. package/dist/src/source-files.js +4 -61
  50. package/dist/src/stores.js +3 -0
  51. package/dist/src/sweep.js +4 -2
  52. package/dist/src/template-field.js +38 -6
  53. package/dist/src/test-support.js +59 -0
  54. package/dist/src/type-errors.js +252 -0
  55. package/dist/src/type-query-alias.js +92 -0
  56. package/dist/src/value-flow.js +74 -0
  57. package/dist/src/verbs/analyze.js +24 -4
  58. package/dist/src/verbs/explain.js +4 -1
  59. package/dist/src/verbs/print-projects.js +17 -56
  60. package/dist/src/verbs/print-retained.js +7 -2
  61. package/dist/src/verbs/print-roots.js +7 -2
  62. package/dist/src/version-range.js +255 -0
  63. package/dist/src/version.js +1 -1
  64. package/dist/src/workspace-manifests.js +68 -0
  65. package/dist/src/workspace-programs.js +342 -0
  66. package/dist/src/workspace-resolution.js +307 -0
  67. package/dist/src/workspace-run.js +75 -0
  68. package/dist/src/workspace.js +358 -0
  69. package/jsr.json +1 -1
  70. package/package.json +3 -2
  71. package/src/analysis.ts +475 -72
  72. package/src/component-files.ts +718 -0
  73. package/src/component-layer.ts +160 -0
  74. package/src/config.ts +11 -2
  75. package/src/configuration-files.ts +370 -0
  76. package/src/conformance-record.ts +3 -10
  77. package/src/contextual-uses.ts +354 -0
  78. package/src/convention-rows.ts +448 -0
  79. package/src/conventions.ts +788 -0
  80. package/src/dependencies.ts +49 -2
  81. package/src/destructuring.ts +16 -6
  82. package/src/discover.ts +10 -6
  83. package/src/emit-map.ts +93 -0
  84. package/src/entry-point-gaps.ts +92 -38
  85. package/src/entry-points.ts +79 -36
  86. package/src/exempt-classes.ts +6 -1
  87. package/src/exempt.ts +69 -20
  88. package/src/file-facts.ts +2 -0
  89. package/src/findings/completion.ts +55 -36
  90. package/src/findings/emitters.ts +32 -1
  91. package/src/findings/interfaces.ts +9 -2
  92. package/src/findings/reads-and-writes.ts +70 -9
  93. package/src/findings/unused-declarations.ts +60 -3
  94. package/src/generated-file.ts +27 -0
  95. package/src/graph.ts +15 -4
  96. package/src/host.ts +23 -0
  97. package/src/index.ts +1 -1
  98. package/src/interface-satisfaction.ts +1 -92
  99. package/src/inventory.ts +17 -3
  100. package/src/kinds.ts +7 -5
  101. package/src/manifest.ts +47 -14
  102. package/src/matrix.ts +43 -15
  103. package/src/partial.ts +6 -1
  104. package/src/pnpm-workspace.ts +135 -0
  105. package/src/position.ts +23 -2
  106. package/src/print.ts +2 -0
  107. package/src/query.ts +26 -0
  108. package/src/references.ts +472 -40
  109. package/src/report.ts +56 -6
  110. package/src/reporters/annotations.ts +12 -3
  111. package/src/reporters/template.ts +3 -0
  112. package/src/resolve.ts +65 -11
  113. package/src/roots.ts +70 -9
  114. package/src/run.ts +15 -5
  115. package/src/schema.ts +2 -0
  116. package/src/session.ts +218 -37
  117. package/src/setup-failure.ts +39 -0
  118. package/src/source-files.ts +4 -66
  119. package/src/stores.ts +9 -0
  120. package/src/sweep.ts +9 -2
  121. package/src/template-field.ts +42 -7
  122. package/src/test-support.ts +77 -0
  123. package/src/type-errors.ts +340 -0
  124. package/src/type-query-alias.ts +133 -0
  125. package/src/value-flow.ts +104 -0
  126. package/src/verbs/analyze.ts +38 -4
  127. package/src/verbs/explain.ts +4 -1
  128. package/src/verbs/print-projects.ts +17 -69
  129. package/src/verbs/print-retained.ts +7 -2
  130. package/src/verbs/print-roots.ts +7 -2
  131. package/src/verbs/verb.ts +6 -3
  132. package/src/version-range.ts +309 -0
  133. package/src/version.ts +1 -1
  134. package/src/workspace-manifests.ts +100 -0
  135. package/src/workspace-programs.ts +486 -0
  136. package/src/workspace-resolution.ts +393 -0
  137. package/src/workspace-run.ts +148 -0
  138. package/src/workspace.ts +463 -0
@@ -805,6 +805,9 @@ class Execution {
805
805
  if (head === undefined) {
806
806
  throw new RenderError("empty command");
807
807
  }
808
+ if (head.k === "lit" && head.value === null) {
809
+ throw new RenderError("nil is not a command");
810
+ }
808
811
  if (head.k !== "func") {
809
812
  if (rest.length > 0 || piped !== undefined) {
810
813
  throw new RenderError("cannot give an argument to a non-function");
@@ -1,4 +1,5 @@
1
1
  import { ConfigError, defaultConfig, } from "./config.js";
2
+ import { CONVENTION_ROWS } from "./convention-rows.js";
2
3
  import { FIXED_SEVERITY_CODES, LIVE_CODES } from "./kinds.js";
3
4
  import { checkDocument, malformed, nestFlags, SCHEMA_ROOT, SEVERITY_SECTION, } from "./schema.js";
4
5
  /** Names the command-line flags in a refusal, where a document is named by its path. */
@@ -132,23 +133,44 @@ function readStrings(parent, name, path, label, minItems, allowed) {
132
133
  throw malformed(label, path, `holds ${String(value.length)} entries, want at least ${String(minItems)}`);
133
134
  }
134
135
  const seen = new Set();
135
- for (const entry of value) {
136
+ for (const [index, entry] of value.entries()) {
137
+ const at = `${path}[${String(index)}]`;
136
138
  if (typeof entry !== "string") {
137
- throw malformed(label, path, "holds an entry that is not a string");
139
+ throw malformed(label, at, "is not a string");
138
140
  }
139
141
  if (entry === "") {
140
- throw malformed(label, path, "holds an empty entry");
142
+ throw malformed(label, at, "is empty");
141
143
  }
142
144
  if (seen.has(entry)) {
143
- throw malformed(label, path, `names ${JSON.stringify(entry)} twice`);
145
+ throw malformed(label, at, `names ${JSON.stringify(entry)} again`);
144
146
  }
145
147
  if (allowed !== undefined && !allowed.includes(entry)) {
146
- throw malformed(label, path, `${JSON.stringify(entry)} is not one of ${spell(allowed)}`);
148
+ throw malformed(label, at, `${JSON.stringify(entry)} is not one of ${spell(allowed)}`);
147
149
  }
148
150
  seen.add(entry);
149
151
  }
150
152
  return value;
151
153
  }
154
+ /** A component file's extension: a full stop followed by letters and digits. */
155
+ const COMPONENT_EXTENSION = /^\.[A-Za-z0-9]+$/u;
156
+ function readComponentExtensions(ts, label) {
157
+ const extensions = readStrings(ts, "component_extensions", "ts.component_extensions", label, 0);
158
+ const wrong = extensions?.findIndex((one) => !COMPONENT_EXTENSION.test(one)) ?? -1;
159
+ if (extensions !== undefined && wrong !== -1) {
160
+ throw malformed(label, `ts.component_extensions[${String(wrong)}]`, `${JSON.stringify(extensions[wrong])} is not a full stop followed by letters and digits`);
161
+ }
162
+ return extensions;
163
+ }
164
+ /** The names of the convention rows this analyzer's table carries. */
165
+ const CONVENTION_NAMES = new Set(CONVENTION_ROWS.map((row) => row.name));
166
+ function readDisabledConventions(ts, label) {
167
+ const names = readStrings(ts, "disabled_conventions", "ts.disabled_conventions", label, 0);
168
+ const unknown = names?.findIndex((one) => !CONVENTION_NAMES.has(one)) ?? -1;
169
+ if (names !== undefined && unknown !== -1) {
170
+ throw malformed(label, `ts.disabled_conventions[${String(unknown)}]`, `${JSON.stringify(names[unknown])} names no convention row this analyzer carries`);
171
+ }
172
+ return names;
173
+ }
152
174
  function requireMember(value, path, label) {
153
175
  if (value === undefined || value === "") {
154
176
  throw malformed(label, path, "is required and names nothing");
@@ -507,6 +529,8 @@ function readDoc(value, label) {
507
529
  providers: readProviders(providers, label),
508
530
  testFiles: readStrings(ts, "test_files", "ts.test_files", label, 1),
509
531
  entryFiles: readStrings(ts, "entry_files", "ts.entry_files", label, 0),
532
+ componentExtensions: readComponentExtensions(ts, label),
533
+ disabledConventions: readDisabledConventions(ts, label),
510
534
  injectionRegistrations: readDeclarations(ts, "injection_registrations", "ts.injection_registrations", label),
511
535
  lifecycleContracts: readLifecycleContracts(ts, label),
512
536
  serializers: readDeclarations(ts, "serializers", "ts.serializers", label),
@@ -667,13 +691,13 @@ export function resolve(inputs) {
667
691
  return { config, provenance };
668
692
  }
669
693
  /**
670
- * The build matrix the configuration documents resolve to, every document read and
671
- * refused as {@link resolve} refuses it, and nothing else required of them: which
672
- * projects a run analyzes depends on the matrix alone, so a question about the
673
- * projects asks no target kind.
694
+ * The configuration the documents resolve to, every document read and refused as
695
+ * {@link resolve} refuses it, with no target kind required of them: which projects a
696
+ * run analyzes does not depend on the kind, so a question about the projects asks
697
+ * none. The kind is empty where no document names one.
674
698
  */
675
- export function resolveMatrix(inputs) {
676
- return settleAll(inputs).config.analysis.configurations;
699
+ export function resolveUnkinded(inputs) {
700
+ return settleAll(inputs).config;
677
701
  }
678
702
  /**
679
703
  * Every setting the documents supply, each from the highest-ranked source carrying
@@ -714,6 +738,8 @@ function settleAll(inputs) {
714
738
  ts: {
715
739
  testFiles: at("ts.test_files", defaults.ts.testFiles, (doc) => doc.testFiles),
716
740
  entryFiles: at("ts.entry_files", defaults.ts.entryFiles, (doc) => doc.entryFiles),
741
+ componentExtensions: at("ts.component_extensions", defaults.ts.componentExtensions, (doc) => doc.componentExtensions),
742
+ disabledConventions: at("ts.disabled_conventions", defaults.ts.disabledConventions, (doc) => doc.disabledConventions),
717
743
  injectionRegistrations: at("ts.injection_registrations", defaults.ts.injectionRegistrations, (doc) => doc.injectionRegistrations),
718
744
  lifecycleContracts: at("ts.lifecycle_contracts", defaults.ts.lifecycleContracts, (doc) => doc.lifecycleContracts),
719
745
  serializers: at("ts.serializers", defaults.ts.serializers, (doc) => doc.serializers),
package/dist/src/roots.js CHANGED
@@ -8,12 +8,20 @@
8
8
  * under reachability and still a candidate under reference counting.
9
9
  */
10
10
  import { aliasChains } from "./alias-chain.js";
11
+ import { conventionEntries } from "./conventions.js";
11
12
  import { entryPoints } from "./entry-points.js";
12
13
  import { globExpression } from "./glob.js";
13
14
  import { nodeKey } from "./inventory.js";
14
15
  import { byPosition } from "./position.js";
15
16
  import { UNANSWERED } from "./query.js";
16
17
  import { sourceFilesOf } from "./source-files.js";
18
+ import { typeQueryAliases } from "./type-query-alias.js";
19
+ /** The kind of root each role of a manifest entry makes. */
20
+ const ENTRY_KINDS = {
21
+ import: "manifest-entry",
22
+ run: "manifest-binary",
23
+ script: "script",
24
+ };
17
25
  /** Whether a configured string carries either special character. */
18
26
  function isPattern(text) {
19
27
  return text.includes("*") || text.includes("?");
@@ -117,18 +125,23 @@ export function roots(project, held, targetRoot, options) {
117
125
  entered.push({ file, kind, source, exports });
118
126
  };
119
127
  const published = [];
120
- const publishes = (entry) => options.publishedAPI &&
121
- entry.published &&
122
- (options.manifest.declaresExports ? entry.member.startsWith("exports") : true);
123
- for (const entry of options.manifest.entries) {
124
- if (publishes(entry)) {
128
+ const entries = [
129
+ ...options.manifest.entries.map((entry) => ({
130
+ entry,
131
+ publishes: entry.published &&
132
+ (options.manifest.declaresExports ? entry.member.startsWith("exports") : true),
133
+ })),
134
+ ...(options.workspaceEntries ?? []).map((entry) => ({ entry, publishes: entry.published })),
135
+ ];
136
+ for (const { entry, publishes } of entries) {
137
+ if (options.publishedAPI && publishes) {
125
138
  published.push(...files.named(entry.path));
126
139
  }
127
140
  }
128
141
  const publishedNames = new Set(published.map((file) => file.fileName));
129
- for (const entry of options.manifest.entries) {
142
+ for (const { entry } of entries) {
130
143
  for (const file of files.named(entry.path)) {
131
- const kind = entry.role === "run" ? "manifest-binary" : "manifest-entry";
144
+ const kind = ENTRY_KINDS[entry.role];
132
145
  enter(file, kind, entry.member, !publishedNames.has(file.fileName));
133
146
  }
134
147
  }
@@ -140,9 +153,25 @@ export function roots(project, held, targetRoot, options) {
140
153
  }
141
154
  }
142
155
  }
143
- for (const point of entryPoints(project, files, options.testFiles)) {
156
+ for (const { file, row } of options.conventions === undefined
157
+ ? []
158
+ : conventionEntries(options.conventions, files)) {
159
+ enter(file, "convention", row, true);
160
+ }
161
+ for (const point of entryPoints(project, files, options.testFiles, options.host, targetRoot, options.parse)) {
144
162
  enter(point.file, point.rule, point.source, point.exports);
145
163
  }
164
+ for (const file of files.byPath.values()) {
165
+ for (const alias of typeQueryAliases(file)) {
166
+ const source = alias.type.getText().replace(/\s+/gu, " ");
167
+ for (const node of [alias.declaration, ...alias.containers]) {
168
+ const id = declared.get(nodeKey(file, node));
169
+ if (id !== undefined) {
170
+ add(id, "type-query-alias", source);
171
+ }
172
+ }
173
+ }
174
+ }
146
175
  const { tables: exportsOf, answered } = exportTables(project, held, [
147
176
  ...entered.filter((entry) => entry.exports).map((entry) => entry.file),
148
177
  ...published,
package/dist/src/run.js CHANGED
@@ -3,6 +3,7 @@ import { DiscoveryError, renderDiagnostic } from "./discover.js";
3
3
  import { EMITTERS } from "./findings/emitters.js";
4
4
  import { joinPath, resolvePath } from "./paths.js";
5
5
  import { readScope, ScopeError, scopeForDir } from "./scope.js";
6
+ import { SetupError, setupLine } from "./setup-failure.js";
6
7
  import { openEngine } from "./session.js";
7
8
  import { printConfigVerb } from "./verbs/print-config.js";
8
9
  import { printProjectsVerb } from "./verbs/print-projects.js";
@@ -158,7 +159,7 @@ function scopeOf(host, options) {
158
159
  * `openClient` opens the compiler client; a caller that watches what a run reads
159
160
  * supplies its own of either.
160
161
  */
161
- export function run(args, out, err, host, openClient = (collectTiming) => openEngine({ collectTiming })) {
162
+ export function run(args, out, err, host, openClient = openEngine) {
162
163
  const editing = sourceEditOption(args);
163
164
  if (editing !== undefined) {
164
165
  err.write(`deadset-ts: ${editing} is not supported: deadset-ts reports and never edits a source file\n`);
@@ -193,8 +194,11 @@ function invoke(verb, own, args, streams, openClient) {
193
194
  scope: () => scopeOf(streams.host, read()),
194
195
  option: (name) => read().named.get(name),
195
196
  repeated: (name) => read().repeated.get(name) ?? [],
196
- openClient: (collectTiming) => {
197
- opened = openClient(collectTiming);
197
+ openClient: (config) => {
198
+ opened = openClient({
199
+ collectTiming: false,
200
+ components: { extensions: config.ts.componentExtensions, host: streams.host },
201
+ });
198
202
  return opened;
199
203
  },
200
204
  emitters: EMITTERS,
@@ -219,6 +223,12 @@ function refuse(error, err) {
219
223
  err.write(`deadset-ts: ${error.message}\n`);
220
224
  return EXIT_FAILURE;
221
225
  }
226
+ if (error instanceof SetupError) {
227
+ for (const failure of error.failures) {
228
+ err.write(`${setupLine(failure)}\n`);
229
+ }
230
+ return EXIT_FAILURE;
231
+ }
222
232
  if (error instanceof ScopeError) {
223
233
  err.write(`deadset-ts: ${error.message}\n`);
224
234
  return EXIT_FAILURE;
@@ -71,6 +71,8 @@ export const SCHEMA_ROOT = {
71
71
  members: {
72
72
  test_files: leaf,
73
73
  entry_files: leaf,
74
+ component_extensions: leaf,
75
+ disabled_conventions: leaf,
74
76
  injection_registrations: declarations,
75
77
  lifecycle_contracts: {
76
78
  kind: "list",
@@ -1,5 +1,7 @@
1
1
  import { API, SymbolFlags } from "@typescript/native/unstable/sync";
2
+ import { componentLayer, mapperManifest } from "./component-layer.js";
2
3
  import { DiscoveryError } from "./discover.js";
4
+ import { joinPath } from "./paths.js";
3
5
  import { queriesOf, UnansweredLog, } from "./query.js";
4
6
  /**
5
7
  * Opens the compiler client the analysis reads through.
@@ -9,11 +11,55 @@ import { queriesOf, UnansweredLog, } from "./query.js";
9
11
  * second call has nothing left to do.
10
12
  */
11
13
  export function openEngine(options) {
12
- const api = new API({ collectTiming: options.collectTiming });
14
+ const { components } = options;
15
+ let mapper;
16
+ if (components !== undefined && components.extensions.length > 0) {
17
+ const made = components.host.temporaryDirectory();
18
+ try {
19
+ components.host.writeDocument(joinPath(made.path, "package.json"), [
20
+ mapperManifest(components.host.componentMapperCommand()),
21
+ ]);
22
+ }
23
+ catch (error) {
24
+ made.remove();
25
+ throw error;
26
+ }
27
+ mapper = made;
28
+ }
29
+ const api = new API({
30
+ collectTiming: options.collectTiming,
31
+ ...(mapper === undefined ? {} : { runExternalCode: true }),
32
+ });
13
33
  let closed = false;
14
34
  return {
15
35
  parseConfigFile: (file) => api.parseConfigFile(file),
16
- createSnapshot: (openProjects) => api.createSnapshot({ openProjects: [...openProjects] }),
36
+ parseConfigJson: (json, configFile) => api.parseJsonConfigFileContent(json, { configFileName: configFile }),
37
+ createSnapshot: (openProjects, createPrograms, configurations = new Map()) => api.createSnapshot({
38
+ openProjects: [...openProjects],
39
+ ...(createPrograms === undefined ? {} : { createPrograms: [...createPrograms] }),
40
+ ...(components === undefined
41
+ ? configurations.size === 0
42
+ ? {}
43
+ : { fileSystem: { kind: "layer", files: Object.fromEntries(configurations) } }
44
+ : {
45
+ fileSystem: componentLayer(components.host, openProjects, mapper === undefined
46
+ ? undefined
47
+ : { extensions: components.extensions, packageDirectory: mapper.path }, configurations),
48
+ }),
49
+ }),
50
+ readsComponents: mapper !== undefined,
51
+ createModuleResolver: (options, entries) => api.createModuleResolver(options, {
52
+ moduleResolutions: { fallback: "resolve", entries: [...entries] },
53
+ }),
54
+ parseSourceFile: (fileName, text) => {
55
+ const retained = api.createSourceFile(fileName, text);
56
+ try {
57
+ return retained.sourceFile;
58
+ }
59
+ finally {
60
+ retained.dispose();
61
+ }
62
+ },
17
63
  batch: (questions) => api.batch(...questions),
18
64
  ask: (_accessor, _locations, question) => question(),
19
65
  getTimingInfo: () => api.getTimingInfo(),
@@ -22,10 +68,35 @@ export function openEngine(options) {
22
68
  return;
23
69
  }
24
70
  closed = true;
25
- api.close();
71
+ try {
72
+ api.close();
73
+ }
74
+ finally {
75
+ mapper?.remove();
76
+ }
26
77
  },
27
78
  };
28
79
  }
80
+ /**
81
+ * The source files of one program that `own` admits, in the program's order, none of
82
+ * them one of the compiler's default libraries.
83
+ */
84
+ function filesOwned(program, own) {
85
+ return (program
86
+ .getSourceFileNames()
87
+ // The metadata decides which files belong to the target, and it is read by
88
+ // name. Fetching each file first and filtering afterwards costs one round
89
+ // trip per library file the answer then discards, and a program holds far
90
+ // more library files than target files.
91
+ .filter((name) => {
92
+ const metadata = program.getSourceFileMetadata(name);
93
+ return (metadata !== undefined &&
94
+ !metadata.isDefaultLibrary &&
95
+ own(name, metadata.isFromExternalLibrary));
96
+ })
97
+ .map((name) => program.getSourceFile(name))
98
+ .filter((file) => file !== undefined));
99
+ }
29
100
  function viewOf(project, configFile, engine, log) {
30
101
  const queries = queriesOf(project.checker, configFile, (questions) => engine.batch(questions), log, (accessor, locations, question) => engine.ask(accessor, locations, question));
31
102
  // The snapshot is never updated, so a project's own file set is the same answer
@@ -35,23 +106,13 @@ function viewOf(project, configFile, engine, log) {
35
106
  let ownFiles;
36
107
  let ownKeys;
37
108
  const ownSourceFiles = () => {
38
- ownFiles ??= project.program
39
- .getSourceFileNames()
40
- // The metadata decides which files belong to the target, and it is read by
41
- // name. Fetching each file first and filtering afterwards costs one round
42
- // trip per library file the answer then discards, and a program holds far
43
- // more library files than target files.
44
- .filter((name) => {
45
- const metadata = project.program.getSourceFileMetadata(name);
46
- return (metadata !== undefined && !metadata.isDefaultLibrary && !metadata.isFromExternalLibrary);
47
- })
48
- .map((name) => project.program.getSourceFile(name))
49
- .filter((file) => file !== undefined);
109
+ ownFiles ??= filesOwned(project.program, (_name, fromExternalLibrary) => !fromExternalLibrary);
50
110
  return ownFiles;
51
111
  };
52
112
  return {
53
113
  configFile,
54
114
  program: project.program,
115
+ rootFiles: project.parsedCommandLine.fileNames,
55
116
  queries,
56
117
  ownSourceFiles,
57
118
  ownPaths: () => {
@@ -101,6 +162,26 @@ export function narrowedTo(project, keep) {
101
162
  },
102
163
  };
103
164
  }
165
+ /**
166
+ * The same project, its own source files the ones `own` admits among every file its
167
+ * program holds, given whether the compiler reached the file as a library's.
168
+ */
169
+ export function ownedAs(project, own) {
170
+ let owned;
171
+ let keys;
172
+ const ownSourceFiles = () => {
173
+ owned ??= filesOwned(project.program, own);
174
+ return owned;
175
+ };
176
+ return {
177
+ ...project,
178
+ ownSourceFiles,
179
+ ownPaths: () => {
180
+ keys ??= new Set(ownSourceFiles().map((file) => file.path));
181
+ return keys;
182
+ },
183
+ };
184
+ }
104
185
  /** The three diagnostic sets one project carries, read before any analysis. */
105
186
  export function diagnosticsOf(project) {
106
187
  return {
@@ -119,28 +200,54 @@ export function diagnosticsOf(project) {
119
200
  };
120
201
  }
121
202
  /**
122
- * Runs `visit` over every project the compiler configurations name, inside one
123
- * snapshot of one client, in the order they are named, and answers in that order.
124
- *
125
- * The snapshot is opened once with every configuration, so each becomes a project
126
- * with its own program and checker; it is never updated, because the analysis
127
- * reads and the tree does not change under it. The order the snapshot lists its
128
- * projects in is the compiler's own, so each named configuration is looked up in it
129
- * instead. A configuration the snapshot opened no project for ends the run under
130
- * `"refuse"` and is named in the result under `"record"`. The snapshot is disposed once
131
- * the last project has been visited, and the client is released after it, whether the
132
- * visit completed or threw.
203
+ * The projects one snapshot holds for the configurations named, by configuration file. A
204
+ * project opened from a written configuration is the project of the configuration
205
+ * `written` maps it to.
133
206
  */
134
- export function runSession(engine, configFiles, visit, unopened = "refuse") {
207
+ function projectsOf(snapshot, built, written) {
208
+ const opened = new Map();
209
+ for (const project of snapshot.getProjects()) {
210
+ if (project.configFileName !== undefined) {
211
+ opened.set(written.get(project.configFileName) ?? project.configFileName, project);
212
+ }
213
+ }
214
+ (snapshot.operation.createdPrograms ?? []).forEach((program, at) => {
215
+ const project = snapshot.getProject(program.id);
216
+ const configFile = built[at];
217
+ if (project !== undefined && configFile !== undefined) {
218
+ opened.set(configFile, project);
219
+ }
220
+ });
221
+ return opened;
222
+ }
223
+ /**
224
+ * Runs `visit` over every project the compiler configurations name, inside one never updated
225
+ * snapshot of one client, and answers in their order. A configuration `programs` describes is
226
+ * built as that program, or opened from the configuration it writes, instead of as a project.
227
+ * One the snapshot holds nothing for ends the run under `"refuse"` and is named in the result
228
+ * under `"record"`. The snapshot is disposed after the last visit and the client released
229
+ * after it, whether the visit completed or threw.
230
+ */
231
+ export function runSession(engine, configFiles, visit, unopened = "refuse", programs = new Map()) {
135
232
  try {
136
- const snapshot = engine.createSnapshot(configFiles);
137
- try {
138
- const opened = new Map();
139
- for (const project of snapshot.getProjects()) {
140
- if (project.configFileName !== undefined) {
141
- opened.set(project.configFileName, project);
142
- }
233
+ const built = [];
234
+ const params = [];
235
+ const written = new Map();
236
+ const texts = new Map();
237
+ for (const configFile of configFiles) {
238
+ const program = programs.get(configFile);
239
+ if (program?.kind === "program") {
240
+ built.push(configFile);
241
+ params.push(program.params);
242
+ }
243
+ else if (program?.kind === "configuration") {
244
+ written.set(program.file, configFile);
245
+ texts.set(program.file, program.text);
143
246
  }
247
+ }
248
+ const snapshot = engine.createSnapshot([...configFiles.filter((configFile) => !programs.has(configFile)), ...written.keys()], params.length === 0 ? undefined : params, texts);
249
+ try {
250
+ const opened = projectsOf(snapshot, built, written);
144
251
  const log = new UnansweredLog();
145
252
  const projects = [];
146
253
  const missing = [];
@@ -0,0 +1,23 @@
1
+ /**
2
+ * A setup failure: a file or a component the analysis needs and the project does not
3
+ * provide. Each one names what is missing and the fix, on one line of the error stream
4
+ * that starts with its class.
5
+ */
6
+ /** The failure as the one line of the error stream that names it. */
7
+ export function setupLine(failure) {
8
+ return `setup failure: ${failure.setupClass}: ${failure.detail}`;
9
+ }
10
+ /**
11
+ * A run that met a setup failure where none may be dropped: in a configuration the
12
+ * configuration document or the scope declares, in a declared consumer, or in every
13
+ * configuration discovery derived. It ends the run with the failure code and no finding
14
+ * list.
15
+ */
16
+ export class SetupError extends Error {
17
+ failures;
18
+ constructor(failures) {
19
+ super(failures.map(setupLine).join("\n"));
20
+ this.name = "SetupError";
21
+ this.failures = failures;
22
+ }
23
+ }
@@ -3,45 +3,10 @@
3
3
  * in a document or a string names one: as the file itself, or as a file the
4
4
  * compiler emits from it.
5
5
  */
6
- import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.js";
7
- /** The source extensions the compiler emits each output extension from. */
8
- const EMITTED = [
9
- { emitted: ".d.ts", sources: [".ts", ".tsx"] },
10
- { emitted: ".d.mts", sources: [".mts"] },
11
- { emitted: ".d.cts", sources: [".cts"] },
12
- { emitted: ".js", sources: [".ts", ".tsx"] },
13
- { emitted: ".jsx", sources: [".tsx"] },
14
- { emitted: ".mjs", sources: [".mts"] },
15
- { emitted: ".cjs", sources: [".cts"] },
16
- ];
6
+ import { commonDirectory, emittedFrom } from "./emit-map.js";
7
+ import { dirnamePath, relativePath, resolvePath } from "./paths.js";
17
8
  /** The one character a subpath pattern of a manifest's `exports` holds. */
18
9
  const SUBPATH_WILDCARD = "*";
19
- /** One path with its extension replaced, where it carries the one named. */
20
- function reExtended(path, emitted, source) {
21
- return path.endsWith(emitted) ? path.slice(0, path.length - emitted.length) + source : undefined;
22
- }
23
- /**
24
- * The longest directory every path shares, which is the directory the compiler
25
- * emits from where the configuration declares no root directory.
26
- */
27
- function commonDirectory(paths) {
28
- const first = paths[0];
29
- if (first === undefined) {
30
- return "";
31
- }
32
- let common = dirnamePath(first).split("/");
33
- for (const path of paths.slice(1)) {
34
- const segments = dirnamePath(path).split("/");
35
- let shared = 0;
36
- while (shared < common.length &&
37
- shared < segments.length &&
38
- common[shared] === segments[shared]) {
39
- shared += 1;
40
- }
41
- common = common.slice(0, shared);
42
- }
43
- return common.join("/");
44
- }
45
10
  /**
46
11
  * Whether one path is what a subpath pattern stands for: the text before the
47
12
  * wildcard, any run of characters including the solidus, then the text after it.
@@ -77,31 +42,9 @@ export function sourceFilesOf(project, targetRoot) {
77
42
  const outDir = at(options.outDir);
78
43
  const declarationDir = at(options.declarationDir);
79
44
  const sourceDir = at(options.rootDir) ?? commonDirectory([...byName.keys()]);
80
- /** One path read below the directory the compiler emits from, where it is below the one it emits to. */
81
- const fromEmit = (path, emitted) => {
82
- const emitDir = emitted.startsWith(".d.") ? (declarationDir ?? outDir) : outDir;
83
- if (emitDir === undefined) {
84
- return undefined;
85
- }
86
- const below = relativePath(emitDir, path);
87
- return below === undefined ? undefined : joinPath(sourceDir, below);
88
- };
45
+ const layout = { outDir, declarationDir, sourceDirs: [sourceDir] };
89
46
  /** The source paths one written path may stand for, the path itself first. */
90
- const candidates = (path) => {
91
- const found = [path];
92
- for (const { emitted, sources } of EMITTED) {
93
- const mapped = fromEmit(path, emitted);
94
- for (const source of sources) {
95
- for (const from of [path, mapped]) {
96
- const candidate = from === undefined ? undefined : reExtended(from, emitted, source);
97
- if (candidate !== undefined) {
98
- found.push(candidate);
99
- }
100
- }
101
- }
102
- }
103
- return found;
104
- };
47
+ const candidates = (path) => [path, ...emittedFrom(path, layout)];
105
48
  return {
106
49
  byPath,
107
50
  byName,
@@ -26,6 +26,9 @@ export function accessorsOf(project, held) {
26
26
  export function storesOf(per, exempt, mode) {
27
27
  return {
28
28
  references: per.flatMap((one) => one.references.filter((reference) => !mode.production || !reference.test)),
29
+ readInTests: new Set(per.flatMap((one) => one.references
30
+ .filter((reference) => reference.test && reference.use !== "write")
31
+ .map((reference) => reference.to))),
29
32
  accessors: new Set(per.flatMap((one) => one.accessors)),
30
33
  exempt: new Set(exempt.map((record) => record.id)),
31
34
  };
package/dist/src/sweep.js CHANGED
@@ -153,13 +153,15 @@ function reachable(graph, held, mode) {
153
153
  function admitTestsOfDeadCode(graph, dead, held) {
154
154
  const admitted = graph.symbols.map(() => false);
155
155
  graph.symbols.forEach((_symbol, at) => {
156
- if (graph.test[at] !== true || graph.subject[at] !== true || held[at] === true) {
156
+ const testCode = graph.test[at] === true || graph.support[at] === true;
157
+ if (!testCode || graph.subject[at] !== true || held[at] === true) {
157
158
  return;
158
159
  }
159
160
  let targets = 0;
160
161
  let live = 0;
161
162
  for (const edge of graph.out[at] ?? []) {
162
- if (graph.test[edge.to] === true || graph.subject[edge.to] !== true) {
163
+ const target = graph.test[edge.to] === true || graph.support[edge.to] === true;
164
+ if (target || graph.subject[edge.to] !== true) {
163
165
  continue;
164
166
  }
165
167
  targets += 1;