@cplieger/deadset-ts 2.3.0 → 3.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.
@@ -0,0 +1,372 @@
1
+ /**
2
+ * The dead components of one graph: the strongly connected components of the subgraph
3
+ * the dead declarations induce, ordered so a report is worked from the top down, each
4
+ * with the root members a deletion starts at and what falls with it.
5
+ */
6
+
7
+ import type { Cascade } from "./config.ts";
8
+ import { OUTSIDE, type Graph } from "./graph.ts";
9
+ import type { InventorySymbol } from "./inventory.ts";
10
+
11
+ /** The name every component identifier this analyzer mints starts with. */
12
+ const ANALYZER = "deadset-ts";
13
+
14
+ /** The fewest digits a component identifier's number is written with. */
15
+ const ID_DIGITS = 4;
16
+
17
+ /** The index of a declaration the component walk has not reached. */
18
+ const UNVISITED = -1;
19
+
20
+ /** One dead component and its place in the acyclic graph over the components. */
21
+ export interface Component {
22
+ /** The analyzer's name, a solidus, and `c-` followed by the component's place in the order. */
23
+ readonly id: string;
24
+ /** The component's declarations, by site. A dead member of a dead container is one of them. */
25
+ readonly members: readonly string[];
26
+ /**
27
+ * The members no other dead component references and whose container is not dead,
28
+ * which are where a deletion starts. A cycle has as many as it has members no dead
29
+ * component outside it references.
30
+ */
31
+ readonly roots: readonly string[];
32
+ /**
33
+ * What one deletion of the component removes: its members, and every dead declaration
34
+ * only this component reaches. A dead declaration two components both reach falls with
35
+ * neither and is counted by its own component.
36
+ */
37
+ readonly falls: readonly string[];
38
+ /** The source lines the deletion removes: the distinct lines {@link Component.falls} spans. */
39
+ readonly deletableLines: number;
40
+ }
41
+
42
+ /** What a report carries for one dead component under one cascade mode. */
43
+ export interface Listing {
44
+ readonly roots: readonly string[];
45
+ /** Every member under the full mode, and nothing under the default one. */
46
+ readonly members: readonly string[];
47
+ readonly symbolCount: number;
48
+ readonly deletableLines: number;
49
+ }
50
+
51
+ /** What a report carries for one component: its roots and what falls, and its members in full. */
52
+ export function listing(component: Component, cascade: Cascade): Listing {
53
+ return {
54
+ roots: component.roots,
55
+ members: cascade === "full" ? component.members : [],
56
+ symbolCount: component.falls.length,
57
+ deletableLines: component.deletableLines,
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Groups the declarations `dead` marks into components and orders them so each precedes
63
+ * every component it reaches, two that neither reaches ordered by their first member's
64
+ * site. `dead` and `testOfDeadCode` hold one flag per declaration of the graph; the dead
65
+ * set arrives rather than being read from a sweep, so a set several sweeps intersect is
66
+ * grouped over this graph's edges the same way.
67
+ */
68
+ export function componentsOf(
69
+ graph: Graph,
70
+ dead: readonly boolean[],
71
+ testOfDeadCode: readonly boolean[],
72
+ ): readonly Component[] {
73
+ const { at, adjacent } = deadSubgraph(graph, dead, testOfDeadCode);
74
+ if (at.length === 0) {
75
+ return [];
76
+ }
77
+ const { componentOf, members } = connected(adjacent);
78
+ const { edges, into } = condense(adjacent, componentOf, members.length);
79
+ const falls = fallSets(edges, members, into);
80
+ const outsideReference = externallyReferenced(adjacent, componentOf);
81
+ const idOf = (position: number): string => graph.symbols[at[position] ?? 0]?.id ?? "";
82
+
83
+ return order(edges, members).map((group, place) => {
84
+ const own = members[group] ?? [];
85
+ const roots = own.filter((position) => {
86
+ // A dead member of a dead container is never a root: the container is where the
87
+ // deletion starts and the member falls with it.
88
+ const container = graph.parent[at[position] ?? 0] ?? OUTSIDE;
89
+ return outsideReference[position] !== true && dead[container] !== true;
90
+ });
91
+ const fallen = falls[group] ?? [];
92
+ return {
93
+ id: `${ANALYZER}/c-${String(place + 1).padStart(ID_DIGITS, "0")}`,
94
+ members: own.map(idOf),
95
+ roots: roots.map(idOf),
96
+ falls: fallen.map(idOf),
97
+ deletableLines: linesSpanned(fallen.map((position) => graph.symbols[at[position] ?? 0])),
98
+ };
99
+ });
100
+ }
101
+
102
+ /**
103
+ * The distinct source lines a set of declarations spans, each declaration at least the
104
+ * line it starts on. A member written inside its container's span adds no line, because
105
+ * deleting the container removes it.
106
+ */
107
+ function linesSpanned(symbols: readonly (InventorySymbol | undefined)[]): number {
108
+ const byPath = new Map<string, [number, number][]>();
109
+ for (const symbol of symbols) {
110
+ if (symbol === undefined) {
111
+ continue;
112
+ }
113
+ const span: [number, number] = [
114
+ symbol.position.line,
115
+ Math.max(symbol.position.line, symbol.endLine),
116
+ ];
117
+ const spans = byPath.get(symbol.position.path);
118
+ if (spans === undefined) {
119
+ byPath.set(symbol.position.path, [span]);
120
+ } else {
121
+ spans.push(span);
122
+ }
123
+ }
124
+ let total = 0;
125
+ for (const spans of byPath.values()) {
126
+ spans.sort((a, b) => a[0] - b[0]);
127
+ let end = 0;
128
+ for (const [first, last] of spans) {
129
+ total += Math.max(0, last - Math.max(first - 1, end));
130
+ end = Math.max(end, last);
131
+ }
132
+ }
133
+ return total;
134
+ }
135
+
136
+ /**
137
+ * The dead declarations by their place in the subgraph, and the edges between them: each
138
+ * reference one makes to another, an edge each way between a dead member and its dead
139
+ * container (one direction alone leaves the member a component the container reaches),
140
+ * and an edge back from each production target of an admitted test, which puts the test in
141
+ * the component of the code it exercises because a subject never references its test.
142
+ */
143
+ function deadSubgraph(
144
+ graph: Graph,
145
+ dead: readonly boolean[],
146
+ testOfDeadCode: readonly boolean[],
147
+ ): { readonly at: readonly number[]; readonly adjacent: readonly (readonly number[])[] } {
148
+ const at: number[] = [];
149
+ const position = graph.symbols.map((_symbol, index) => {
150
+ if (dead[index] !== true) {
151
+ return UNVISITED;
152
+ }
153
+ at.push(index);
154
+ return at.length - 1;
155
+ });
156
+ const adjacent: number[][] = at.map(() => []);
157
+ at.forEach((index, from) => {
158
+ for (const edge of graph.out[index] ?? []) {
159
+ const to = position[edge.to] ?? UNVISITED;
160
+ if (to === UNVISITED) {
161
+ continue;
162
+ }
163
+ adjacent[from]?.push(to);
164
+ if (
165
+ testOfDeadCode[index] === true &&
166
+ graph.test[edge.to] !== true &&
167
+ graph.subject[edge.to] === true
168
+ ) {
169
+ adjacent[to]?.push(from);
170
+ }
171
+ }
172
+ const container = position[graph.parent[index] ?? OUTSIDE] ?? UNVISITED;
173
+ if (container !== UNVISITED) {
174
+ adjacent[from]?.push(container);
175
+ adjacent[container]?.push(from);
176
+ }
177
+ });
178
+ return { at, adjacent };
179
+ }
180
+
181
+ /** One frame of the component walk: a declaration, and the next of its edges to follow. */
182
+ interface Frame {
183
+ readonly at: number;
184
+ next: number;
185
+ }
186
+
187
+ /**
188
+ * Each declaration's component and each component's members by site, by Tarjan's
189
+ * algorithm in one pass. The walk keeps its own stack of frames, so a chain of any length
190
+ * is walked without growing the call stack. Components are closed in an order in which
191
+ * each follows every component it reaches.
192
+ */
193
+ function connected(adjacent: readonly (readonly number[])[]): {
194
+ readonly componentOf: readonly number[];
195
+ readonly members: readonly (readonly number[])[];
196
+ } {
197
+ const index = adjacent.map(() => UNVISITED);
198
+ const low = adjacent.map(() => UNVISITED);
199
+ const onStack = adjacent.map(() => false);
200
+ const stack: number[] = [];
201
+ const members: number[][] = [];
202
+ let next = 0;
203
+
204
+ const open = (at: number, frames: Frame[]): void => {
205
+ index[at] = next;
206
+ low[at] = next;
207
+ next += 1;
208
+ stack.push(at);
209
+ onStack[at] = true;
210
+ frames.push({ at, next: 0 });
211
+ };
212
+
213
+ adjacent.forEach((_edges, start) => {
214
+ if (index[start] !== UNVISITED) {
215
+ return;
216
+ }
217
+ const frames: Frame[] = [];
218
+ open(start, frames);
219
+ for (let frame = frames.at(-1); frame !== undefined; frame = frames.at(-1)) {
220
+ const edges = adjacent[frame.at] ?? [];
221
+ const to = edges[frame.next];
222
+ if (to !== undefined) {
223
+ frame.next += 1;
224
+ if (index[to] === UNVISITED) {
225
+ open(to, frames);
226
+ } else if (onStack[to] === true) {
227
+ low[frame.at] = Math.min(low[frame.at] ?? 0, index[to] ?? 0);
228
+ }
229
+ continue;
230
+ }
231
+ frames.pop();
232
+ const parent = frames.at(-1);
233
+ if (parent !== undefined) {
234
+ low[parent.at] = Math.min(low[parent.at] ?? 0, low[frame.at] ?? 0);
235
+ }
236
+ if (low[frame.at] !== index[frame.at]) {
237
+ continue;
238
+ }
239
+ const group: number[] = [];
240
+ for (let top = stack.pop(); top !== undefined; top = stack.pop()) {
241
+ onStack[top] = false;
242
+ group.push(top);
243
+ if (top === frame.at) {
244
+ break;
245
+ }
246
+ }
247
+ members.push(group.sort((a, b) => a - b));
248
+ }
249
+ });
250
+
251
+ const componentOf = adjacent.map(() => 0);
252
+ members.forEach((group, component) => {
253
+ for (const at of group) {
254
+ componentOf[at] = component;
255
+ }
256
+ });
257
+ return { componentOf, members };
258
+ }
259
+
260
+ /**
261
+ * Per component, the components it reaches in one step, and how many components reach
262
+ * it, each pair counted once. An edge inside a component is not an edge between two, so
263
+ * the result is acyclic.
264
+ */
265
+ function condense(
266
+ adjacent: readonly (readonly number[])[],
267
+ componentOf: readonly number[],
268
+ count: number,
269
+ ): { readonly edges: readonly (readonly number[])[]; readonly into: readonly number[] } {
270
+ const edges: number[][] = Array.from({ length: count }, () => []);
271
+ const into = Array.from({ length: count }, () => 0);
272
+ const seen = new Set<string>();
273
+ adjacent.forEach((targets, from) => {
274
+ for (const to of targets) {
275
+ const a = componentOf[from] ?? 0;
276
+ const b = componentOf[to] ?? 0;
277
+ const pair = `${String(a)}>${String(b)}`;
278
+ if (a === b || seen.has(pair)) {
279
+ continue;
280
+ }
281
+ seen.add(pair);
282
+ edges[a]?.push(b);
283
+ into[b] = (into[b] ?? 0) + 1;
284
+ }
285
+ });
286
+ return { edges, into };
287
+ }
288
+
289
+ /** Per dead declaration, whether a dead component other than its own references it. */
290
+ function externallyReferenced(
291
+ adjacent: readonly (readonly number[])[],
292
+ componentOf: readonly number[],
293
+ ): readonly boolean[] {
294
+ const referenced = adjacent.map(() => false);
295
+ adjacent.forEach((targets, from) => {
296
+ for (const to of targets) {
297
+ if (componentOf[from] !== componentOf[to]) {
298
+ referenced[to] = true;
299
+ }
300
+ }
301
+ });
302
+ return referenced;
303
+ }
304
+
305
+ /**
306
+ * The components in the order a report is worked in. Tarjan closes each component after
307
+ * every one it reaches, so reading the closing order backwards is a topological order,
308
+ * and one pass over it gives each component its depth below the roots.
309
+ */
310
+ function order(
311
+ edges: readonly (readonly number[])[],
312
+ members: readonly (readonly number[])[],
313
+ ): readonly number[] {
314
+ const sequence = edges.map((_targets, component) => component).reverse();
315
+ const depth = edges.map(() => 0);
316
+ for (const from of sequence) {
317
+ for (const to of edges[from] ?? []) {
318
+ depth[to] = Math.max(depth[to] ?? 0, (depth[from] ?? 0) + 1);
319
+ }
320
+ }
321
+ return sequence.sort(
322
+ (a, b) => (depth[a] ?? 0) - (depth[b] ?? 0) || (members[a]?.[0] ?? 0) - (members[b]?.[0] ?? 0),
323
+ );
324
+ }
325
+
326
+ /**
327
+ * Per component, the dead declarations one deletion of it removes. A root of the
328
+ * condensation carries its members and every component only it reaches; any other
329
+ * component carries its own members alone, because what it reaches is reached through a
330
+ * root above it as well.
331
+ */
332
+ function fallSets(
333
+ edges: readonly (readonly number[])[],
334
+ members: readonly (readonly number[])[],
335
+ into: readonly number[],
336
+ ): readonly (readonly number[])[] {
337
+ // Each root's walk stamps what it reaches with the root's own number, so the walks
338
+ // share one array rather than allocating one per root.
339
+ const stamp = edges.map(() => UNVISITED);
340
+ const reach = new Map<number, readonly number[]>();
341
+ const owners = edges.map(() => 0);
342
+ edges.forEach((_targets, root) => {
343
+ if (into[root] !== 0) {
344
+ return;
345
+ }
346
+ const reached = [root];
347
+ stamp[root] = root;
348
+ // An array's iterator reads its length at every step, so what a step pushes is
349
+ // walked in the same loop.
350
+ for (const at of reached) {
351
+ for (const to of edges[at] ?? []) {
352
+ if (stamp[to] !== root) {
353
+ stamp[to] = root;
354
+ reached.push(to);
355
+ }
356
+ }
357
+ }
358
+ reach.set(root, reached);
359
+ for (const component of reached) {
360
+ owners[component] = (owners[component] ?? 0) + 1;
361
+ }
362
+ });
363
+ return members.map((own, component) => {
364
+ const set = [...own];
365
+ for (const other of reach.get(component) ?? []) {
366
+ if (other !== component && owners[other] === 1) {
367
+ set.push(...(members[other] ?? []));
368
+ }
369
+ }
370
+ return set.sort((a, b) => a - b);
371
+ });
372
+ }
package/src/config.ts CHANGED
@@ -37,14 +37,35 @@ export type Sort = "position" | "size";
37
37
  /** How much of a dead component a rendering names. */
38
38
  export type Cascade = "roots" | "full";
39
39
 
40
- /** One entry of the build matrix: a configuration the target builds under. */
41
- export interface BuildConfiguration {
40
+ /**
41
+ * A platform configuration: an operating system, an architecture and the build
42
+ * tags in effect. It is the shape a language built under build constraints reads,
43
+ * and this analyzer leaves it to that analysis.
44
+ */
45
+ export interface PlatformConfiguration {
46
+ readonly shape: "platform";
42
47
  readonly id: string;
43
48
  readonly os: string;
44
49
  readonly arch: string;
45
50
  readonly tags: readonly string[];
46
51
  }
47
52
 
53
+ /**
54
+ * A project configuration: one compiler configuration file, as a path below the
55
+ * target root with `/` between segments.
56
+ */
57
+ export interface ProjectConfiguration {
58
+ readonly shape: "project";
59
+ readonly id: string;
60
+ readonly project: string;
61
+ }
62
+
63
+ /**
64
+ * One entry of the build matrix: a configuration the target builds under, in
65
+ * exactly one of the two shapes the matrix admits.
66
+ */
67
+ export type BuildConfiguration = PlatformConfiguration | ProjectConfiguration;
68
+
48
69
  /**
49
70
  * The pair of action delimiters a template is parsed with. Both members are
50
71
  * required once a document names the object, so the pair is one setting rather
package/src/discover.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { DiagnosticCategory } from "@typescript/native/unstable/sync";
2
2
  import type { Diagnostic } from "@typescript/native/unstable/sync";
3
+ import type { BuildConfiguration, ProjectConfiguration } from "./config.ts";
3
4
  import type { Host } from "./host.ts";
4
- import { dirnamePath, isAbsolutePath, joinPath, resolvePath } from "./paths.ts";
5
+ import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.ts";
5
6
  import type { Scope } from "./scope.ts";
6
7
  import type { Engine } from "./session.ts";
7
8
 
@@ -54,11 +55,29 @@ export function diagnosticErrors(sets: {
54
55
  return [...sets.configParsing, ...sets.syntactic, ...sets.semantic].filter(isError);
55
56
  }
56
57
 
57
- /** Every compiler configuration file discovery found, and where each came from. */
58
+ /** One project a run analyzes. */
59
+ export interface DiscoveredProject {
60
+ /**
61
+ * The name a run gives the project: the identifier the build matrix declares for
62
+ * it, or, for a project discovery derived, its configuration file's path below the
63
+ * target root. A derived project whose file is not below the target root is named
64
+ * by the file's absolute path, the one spelling it has.
65
+ */
66
+ readonly id: string;
67
+ /** The compiler configuration file the project is opened from, absolute. */
68
+ readonly configFile: string;
69
+ }
70
+
71
+ /** Every project discovery found, and where each came from. */
58
72
  export interface Discovery {
59
- /** The configuration files, in discovery order, each once. */
73
+ /** The projects, in discovery order, each once. */
74
+ readonly projects: readonly DiscoveredProject[];
75
+ /** The configuration file of each project, in the same order. */
60
76
  readonly configFiles: readonly string[];
61
- /** The files the scope named, which are the ones discovery read first. */
77
+ /**
78
+ * The files the scope named, which are the ones a derivation reads first. A
79
+ * declared matrix reads none of them.
80
+ */
62
81
  readonly fromScope: readonly string[];
63
82
  }
64
83
 
@@ -153,24 +172,93 @@ function scopeConfigFiles(host: Host, scope: Scope): string[] {
153
172
  }
154
173
 
155
174
  /**
156
- * Every project one run analyzes, in the order discovery reads them: the paths the
157
- * scope names, then every compiler configuration under the target root that is not
158
- * inside an ignored directory, then the configurations each of those references,
159
- * transitively. Each configuration appears once.
160
- *
161
- * A configuration file the compiler cannot read ends the run with the failure code,
162
- * so a project that would have been analyzed with no type information is never
163
- * analyzed at all. What a configuration's own contents say is refused once its
164
- * project is open, by {@link diagnosticErrors} over the three sets a program
165
- * reports, because a resolution carries no diagnostics.
175
+ * The name a derived project carries: its configuration file's path below the target
176
+ * root, or the absolute path of a file outside it.
177
+ */
178
+ function derivedId(targetRoot: string, configFile: string): string {
179
+ return relativePath(targetRoot, configFile) ?? configFile;
180
+ }
181
+
182
+ /**
183
+ * Resolves one compiler configuration file, or ends the run naming it: a project
184
+ * opened from a configuration the compiler cannot read would be analyzed with no
185
+ * type information.
186
+ */
187
+ function parseOrRefuse(engine: Engine, configFile: string, named: string): void {
188
+ try {
189
+ engine.parseConfigFile(configFile);
190
+ } catch (error: unknown) {
191
+ throw new DiscoveryError(
192
+ `${named} cannot be read as a compiler configuration: ` +
193
+ (error instanceof Error ? error.message : String(error)),
194
+ [],
195
+ );
196
+ }
197
+ }
198
+
199
+ /**
200
+ * The projects a build matrix declares, in the order it lists them. Each entry
201
+ * names a file below the target root, and an entry naming a file that is absent,
202
+ * that is a directory, or that the compiler cannot read ends the run naming it.
166
203
  *
167
- * Nothing reads a build output: a project is the files its configuration names, so a
168
- * package whose sources ship as TypeScript is analyzed as it stands.
204
+ * A declared project's `references` are not followed, because the entries are the
205
+ * matrix, and a reference to a configuration the matrix does not name ends the run
206
+ * naming both: the compiler builds a referenced project's sources into the program
207
+ * that references it without reporting their diagnostics, so the project the matrix
208
+ * left out would be analyzed with its errors unread. An entry naming a file an
209
+ * earlier entry named adds nothing, so the earlier identifier stands.
169
210
  */
170
- export function discoverProjects(engine: Engine, host: Host, scope: Scope): Discovery {
211
+ function declaredProjects(
212
+ engine: Engine,
213
+ host: Host,
214
+ targetRoot: string,
215
+ entries: readonly ProjectConfiguration[],
216
+ ): Discovery {
217
+ const seen = new Set<string>();
218
+ const projects: DiscoveredProject[] = [];
219
+ for (const entry of entries) {
220
+ const configFile = joinPath(targetRoot, entry.project);
221
+ const named = `the project ${JSON.stringify(entry.id)} (${configFile})`;
222
+ const kind = host.kindOf(configFile);
223
+ if (kind !== "file") {
224
+ throw new DiscoveryError(
225
+ kind === "absent"
226
+ ? `${named} does not exist`
227
+ : `${named} is a directory, and a project names its compiler configuration file`,
228
+ [],
229
+ );
230
+ }
231
+ if (seen.has(configFile)) {
232
+ continue;
233
+ }
234
+ seen.add(configFile);
235
+ parseOrRefuse(engine, configFile, named);
236
+ projects.push({ id: entry.id, configFile });
237
+ }
238
+ for (const project of projects) {
239
+ const unnamed = referencesOf(host, project.configFile).find((path) => !seen.has(path));
240
+ if (unnamed !== undefined) {
241
+ throw new DiscoveryError(
242
+ `the project ${JSON.stringify(project.id)} (${project.configFile}) references ` +
243
+ `${unnamed}, which the build matrix does not name; a referenced project is built ` +
244
+ "into the program that references it, so the matrix names it too",
245
+ [],
246
+ );
247
+ }
248
+ }
249
+ return { projects, configFiles: projects.map((project) => project.configFile), fromScope: [] };
250
+ }
251
+
252
+ /**
253
+ * The projects a run derives where the matrix declares none, in the order discovery
254
+ * reads them: the paths the scope names, then every compiler configuration under
255
+ * the target root that is not inside an ignored directory, then the configurations
256
+ * each of those references, transitively. Each configuration appears once.
257
+ */
258
+ function derivedProjects(engine: Engine, host: Host, scope: Scope): Discovery {
171
259
  const fromScope = scopeConfigFiles(host, scope);
172
260
  const seen = new Set<string>();
173
- const configFiles: string[] = [];
261
+ const projects: DiscoveredProject[] = [];
174
262
  const pending = [...fromScope, ...configFilesUnder(host, scope.target.path)];
175
263
  while (pending.length > 0) {
176
264
  const configFile = pending.shift();
@@ -182,17 +270,44 @@ export function discoverProjects(engine: Engine, host: Host, scope: Scope): Disc
182
270
  continue;
183
271
  }
184
272
  seen.add(path);
185
- try {
186
- engine.parseConfigFile(path);
187
- } catch (error: unknown) {
188
- throw new DiscoveryError(
189
- `${path} cannot be read as a compiler configuration: ` +
190
- (error instanceof Error ? error.message : String(error)),
191
- [],
192
- );
193
- }
194
- configFiles.push(path);
273
+ parseOrRefuse(engine, path, path);
274
+ projects.push({ id: derivedId(scope.target.path, path), configFile: path });
195
275
  pending.push(...referencesOf(host, path));
196
276
  }
197
- return { configFiles, fromScope };
277
+ return { projects, configFiles: projects.map((project) => project.configFile), fromScope };
278
+ }
279
+
280
+ /**
281
+ * Every project one run analyzes.
282
+ *
283
+ * The project entries of the build matrix, where it holds any, are the projects:
284
+ * exactly those, in the order the matrix lists them, each named by its declared
285
+ * identifier, as {@link declaredProjects} reads them. A matrix holding no project
286
+ * entry, the empty one included, leaves discovery to derive the projects from the
287
+ * scope and the target tree. A platform entry names a configuration of another
288
+ * language and plays no part here.
289
+ *
290
+ * A configuration file the compiler cannot read ends the run with the failure code,
291
+ * so a project that would have been analyzed with no type information is never
292
+ * analyzed at all, whether the matrix declared it or discovery derived it. What a
293
+ * configuration's own contents say is refused once its project is open, by
294
+ * {@link diagnosticErrors} over the three sets a program reports, because a
295
+ * resolution carries no diagnostics.
296
+ *
297
+ * Nothing reads a build output: a project is the files its configuration names, so a
298
+ * package whose sources ship as TypeScript is analyzed as it stands.
299
+ */
300
+ export function discoverProjects(
301
+ engine: Engine,
302
+ host: Host,
303
+ scope: Scope,
304
+ configurations: readonly BuildConfiguration[] = [],
305
+ ): Discovery {
306
+ const declared = configurations.filter(
307
+ (entry): entry is ProjectConfiguration => entry.shape === "project",
308
+ );
309
+ if (declared.length > 0) {
310
+ return declaredProjects(engine, host, scope.target.path, declared);
311
+ }
312
+ return derivedProjects(engine, host, scope);
198
313
  }