@cplieger/deadset-ts 2.3.0 → 3.0.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.
- package/README.md +7 -16
- package/dist/src/analysis.js +49 -16
- package/dist/src/components.js +289 -0
- package/dist/src/discover.js +98 -24
- package/dist/src/graph.js +116 -0
- package/dist/src/index.js +0 -2
- package/dist/src/print.js +3 -6
- package/dist/src/references.js +154 -49
- package/dist/src/resolve.js +82 -20
- package/dist/src/run.js +11 -7
- package/dist/src/schema.js +1 -1
- package/dist/src/sweep.js +156 -0
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +91 -24
- package/src/components.ts +372 -0
- package/src/config.ts +23 -2
- package/src/discover.ts +144 -29
- package/src/graph.ts +178 -0
- package/src/index.ts +0 -20
- package/src/print.ts +5 -6
- package/src/references.ts +199 -52
- package/src/resolve.ts +117 -24
- package/src/run.ts +11 -7
- package/src/schema.ts +5 -2
- package/src/sweep.ts +230 -0
- package/src/version.ts +1 -1
|
@@ -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, summed over {@link Component.falls}. */
|
|
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
|
-
/**
|
|
41
|
-
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
157
|
-
*
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
*
|
|
165
|
-
*
|
|
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
|
-
*
|
|
168
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
186
|
-
|
|
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
|
}
|