@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
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ deadset-ts is a command-line analyzer for TypeScript and JavaScript projects. It
|
|
|
8
8
|
|
|
9
9
|
deadset-ts is report-only. It never edits source, and it refuses any `--fix` flag.
|
|
10
10
|
|
|
11
|
-
**Status: pre-release.** This version implements contract 2.
|
|
11
|
+
**Status: pre-release.** This version implements contract 2.2.0 and ships the analyzer's foundation: one compiler session per run over the projects `analysis.configurations` declares, each a compiler configuration file below the target, or, where it declares none, every `tsconfig` project discovered under the target (the explicit scope, every `tsconfig*.json`, each project's `references`), a fail-closed read of every project's diagnostics, configuration decoding against the contract's closed key list with `print-config` and its provenance, `print-projects`, the report-only guard, the symbol inventory of every declaration a project's own files hold down to its class and type members, the one-pass reference resolution over that inventory, which resolves each file's identifiers in batches ([batch-cap calibration](docs/batch-cap-calibration.md) is the measurement behind the batch size), and the root set `print-roots` prints. A root is a declaration the analysis keeps live without a reference: what a file exports when the manifest names it through `main`, `module`, `types`, `bin` or `exports`, or when `ts.entry_files` matches it; a library target's published API; the configuration, setup and test files of Vitest, Stryker and Playwright, the flat configuration of ESLint, and a worker or service worker a call addresses by a string literal; and every declaration a `roots.patterns` entry names. An entry that names nothing is reported as `DS1704` and fails the run. The entry-point conventions of other tools are not read, and `DECLINED_CONVENTIONS` lists each one with the reason. The analysis is reached through the command, and through `run()` for a caller that runs the command in-process; no other verb reports findings yet, so `analyze`, `explain`, `print-retained` and `describe` exit with a usage message. The one runtime dependency is the `typescript` package at exactly 7.0.2, installed under the `@typescript/native` name until TypeScript 7.1 ships a compiler API the tooling can share.
|
|
12
12
|
|
|
13
13
|
## Install
|
|
14
14
|
|
|
@@ -35,7 +35,7 @@ npx deadset-ts version
|
|
|
35
35
|
| `analyze` | Analyze a project and print the findings |
|
|
36
36
|
| `explain` | Explain one finding: the relation, the roots and the references that decided it |
|
|
37
37
|
| `print-config` | Print the resolved configuration and where each setting came from |
|
|
38
|
-
| `print-projects` | Print the
|
|
38
|
+
| `print-projects` | Print the identifier of every project a run analyzes |
|
|
39
39
|
| `print-roots` | Print every declaration the analysis keeps live without a reference, and why |
|
|
40
40
|
| `print-retained` | Print every symbol an exemption class held back, with the class |
|
|
41
41
|
| `describe` | Describe the analyzer: version, Contract version, supported issue kinds |
|
|
@@ -49,7 +49,7 @@ npx deadset-ts version
|
|
|
49
49
|
|
|
50
50
|
## API
|
|
51
51
|
|
|
52
|
-
The package exports the command line and the
|
|
52
|
+
The package exports the command line, for callers that run it in-process, and the vocabularies and helpers its output is spelled with. The command line reads the platform through a `Host`: the filesystem, the directory relative paths resolve against, and the version of the package the analyzer was installed from. `bin/node-host.ts` is the Node one the command line binds; a caller embedding the analyzer, or running it on another platform, supplies its own and supplies its own version with it.
|
|
53
53
|
|
|
54
54
|
The command line:
|
|
55
55
|
|
|
@@ -62,18 +62,6 @@ The platform:
|
|
|
62
62
|
- `Host`: the filesystem a run reads, the directory it reads relative paths against, and the analyzer's own version.
|
|
63
63
|
- `DirectoryEntry`, `PathKind`: one entry of a directory read, and what one path names.
|
|
64
64
|
|
|
65
|
-
The symbol inventory:
|
|
66
|
-
|
|
67
|
-
- `inventory(project, host, targetRoot)`: every declaration one project's own files hold, in position order, down to class and type members.
|
|
68
|
-
- `nodeKey(file, node)`: the key one declaration is recorded under.
|
|
69
|
-
- `Inventory`, `InventorySymbol`, `InventoryCost`, `SymbolKind`, `Visibility`: one project's inventory, one declaration of it, what reading it cost, and the two vocabularies a declaration is described by.
|
|
70
|
-
|
|
71
|
-
The reference pass:
|
|
72
|
-
|
|
73
|
-
- `references(project, held, targetRoot, options)`: every reference from one project's own files to a declaration of its inventory, in position order, with the rules that classified files as test files.
|
|
74
|
-
- `DEFAULT_BATCH_CAP`: the batch size the reference pass takes when its caller passes none; it is an option of the pass, not a configuration key. [Batch-cap calibration](docs/batch-cap-calibration.md) is the measurement that set it.
|
|
75
|
-
- `Reference`, `References`, `ReferenceOptions`, `ReferenceCost`, `TestFileRule`, `Resolution`, `Use`: one reference, one project's set of them, how a run resolves them, what that cost, one test-file rule, which accessor answered a reference, and whether it reads or writes.
|
|
76
|
-
|
|
77
65
|
Positions:
|
|
78
66
|
|
|
79
67
|
- `renderPosition(file, root, offset)`: one compiler offset as a path, a line and a column, the column counting UTF-16 code units.
|
|
@@ -89,9 +77,12 @@ Stable symbol references:
|
|
|
89
77
|
- `REF_EXPRESSIONS`: one regular expression per form the grammar defines.
|
|
90
78
|
- `Module`, `Component`, `Fragment`, `DependencySection`: the module half of a reference, one component of its fragment, what the fragment names, and the manifest section a dependency was declared in.
|
|
91
79
|
|
|
92
|
-
|
|
80
|
+
Entry points:
|
|
93
81
|
|
|
94
82
|
- `DECLINED_CONVENTIONS`, `DeclinedConvention`: the entry-point conventions of other tools the analysis does not read, one row per tool or convention with the reason.
|
|
83
|
+
|
|
84
|
+
Versions:
|
|
85
|
+
|
|
95
86
|
- `CONTRACT_VERSION`: the contract version this analyzer implements.
|
|
96
87
|
|
|
97
88
|
## Contributing
|
package/dist/src/analysis.js
CHANGED
|
@@ -8,12 +8,15 @@
|
|
|
8
8
|
* project's checker, and that holds only for views the session itself hands out.
|
|
9
9
|
*/
|
|
10
10
|
import { diagnosticErrors, discoverProjects, DiscoveryError } from "./discover.js";
|
|
11
|
+
import { graphOf } from "./graph.js";
|
|
11
12
|
import { inventory } from "./inventory.js";
|
|
12
13
|
import { readManifest } from "./manifest.js";
|
|
13
14
|
import { relativePath, resolvePath } from "./paths.js";
|
|
14
15
|
import { byPosition } from "./position.js";
|
|
16
|
+
import { references } from "./references.js";
|
|
15
17
|
import { roots, unmatchedEverywhere, unmatchedRoots } from "./roots.js";
|
|
16
18
|
import { diagnosticsOf, runSession } from "./session.js";
|
|
19
|
+
import { sweep } from "./sweep.js";
|
|
17
20
|
/** The setting whose source decides where a finding about a configured root sits. */
|
|
18
21
|
const ROOTS_SETTING = "roots.patterns";
|
|
19
22
|
/**
|
|
@@ -38,19 +41,17 @@ function compare(a, b) {
|
|
|
38
41
|
return a < b ? -1 : 1;
|
|
39
42
|
}
|
|
40
43
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* configured string is a finding when it names nothing in every project, and the
|
|
46
|
-
* finding sits in the document that declared the roots.
|
|
44
|
+
* Reads every project the scope discovers, in one snapshot: each is checked for errors
|
|
45
|
+
* first, then enumerated, then rooted, and `stage` reads the rest of what it needs from
|
|
46
|
+
* the project while its view is open. A project carrying an error fails the run with no
|
|
47
|
+
* answer, as every verb that reads declarations does.
|
|
47
48
|
*/
|
|
48
|
-
|
|
49
|
+
function readProjects(engine, host, scope, config, stage) {
|
|
49
50
|
const targetRoot = scope.target.path;
|
|
50
|
-
const discovered = discoverProjects(engine, host, scope);
|
|
51
|
-
const
|
|
51
|
+
const discovered = discoverProjects(engine, host, scope, config.analysis.configurations);
|
|
52
|
+
const ids = new Map(discovered.projects.map((project) => [project.configFile, project.id]));
|
|
52
53
|
const options = {
|
|
53
|
-
manifest,
|
|
54
|
+
manifest: readManifest(host, targetRoot),
|
|
54
55
|
patterns: config.rootPatterns,
|
|
55
56
|
entryFiles: config.ts.entryFiles,
|
|
56
57
|
testFiles: config.ts.testFiles,
|
|
@@ -64,18 +65,30 @@ export function runRoots(engine, host, scope, config, provenance) {
|
|
|
64
65
|
return undefined;
|
|
65
66
|
}
|
|
66
67
|
const held = inventory(project, host, targetRoot);
|
|
67
|
-
|
|
68
|
+
const rooted = roots(project, held, targetRoot, options);
|
|
69
|
+
const configuration = ids.get(project.configFile) ??
|
|
70
|
+
relativePath(targetRoot, project.configFile) ??
|
|
71
|
+
project.configFile;
|
|
72
|
+
return stage(project, { configuration, held, rooted });
|
|
68
73
|
});
|
|
69
74
|
if (failures.length > 0) {
|
|
70
75
|
throw new DiscoveryError(`${String(failures.length)} error(s); no answer was produced`, failures);
|
|
71
76
|
}
|
|
77
|
+
return projects.filter((answer) => answer !== undefined);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The root set of the run the scope and the configuration describe.
|
|
81
|
+
*
|
|
82
|
+
* A configured string is a finding when it names nothing in every project, and the
|
|
83
|
+
* finding sits in the document that declared the roots.
|
|
84
|
+
*/
|
|
85
|
+
export function runRoots(engine, host, scope, config, provenance) {
|
|
86
|
+
const targetRoot = scope.target.path;
|
|
87
|
+
const projects = readProjects(engine, host, scope, config, (_project, read) => read);
|
|
72
88
|
const configurations = [];
|
|
73
89
|
const merged = new Map();
|
|
74
90
|
for (const answer of projects) {
|
|
75
|
-
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
const configuration = relativePath(targetRoot, answer.rooted.configFile) ?? answer.rooted.configFile;
|
|
91
|
+
const configuration = answer.configuration;
|
|
79
92
|
configurations.push(configuration);
|
|
80
93
|
const symbols = new Map(answer.held.symbols.map((symbol) => [symbol.id, symbol]));
|
|
81
94
|
for (const root of answer.rooted.liveUnderReachability) {
|
|
@@ -100,10 +113,30 @@ export function runRoots(engine, host, scope, config, provenance) {
|
|
|
100
113
|
}
|
|
101
114
|
}
|
|
102
115
|
const ordered = [...merged.values()].sort((a, b) => byPosition(a.position, b.position) || compare(a.kind, b.kind) || compare(a.source, b.source));
|
|
103
|
-
const unmatched = unmatchedEverywhere(config.rootPatterns, projects.
|
|
116
|
+
const unmatched = unmatchedEverywhere(config.rootPatterns, projects.map((answer) => answer.rooted));
|
|
104
117
|
return {
|
|
105
118
|
configurations,
|
|
106
119
|
roots: ordered,
|
|
107
120
|
findings: unmatchedRoots(unmatched, documentOf(host, provenance, ROOTS_SETTING, targetRoot)),
|
|
108
121
|
};
|
|
109
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Sweeps every project of the run the scope and the configuration describe, one
|
|
125
|
+
* configuration at a time, in discovery order. Each project's references are read
|
|
126
|
+
* against its own inventory with the configured test-file patterns, and its graph is
|
|
127
|
+
* swept under the input the caller decided once for the run.
|
|
128
|
+
*/
|
|
129
|
+
export function runSweeps(engine, host, scope, config, input) {
|
|
130
|
+
const targetRoot = scope.target.path;
|
|
131
|
+
return readProjects(engine, host, scope, config, (project, read) => {
|
|
132
|
+
const resolved = references(project, read.held, targetRoot, {
|
|
133
|
+
testFiles: config.ts.testFiles,
|
|
134
|
+
});
|
|
135
|
+
const graph = graphOf(read.held.symbols, resolved.references, read.rooted.liveUnderReachability, resolved.testFilePaths);
|
|
136
|
+
return {
|
|
137
|
+
configuration: read.configuration,
|
|
138
|
+
symbols: read.held.symbols,
|
|
139
|
+
sweep: sweep(graph, input),
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
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
|
+
import { OUTSIDE } from "./graph.js";
|
|
7
|
+
/** The name every component identifier this analyzer mints starts with. */
|
|
8
|
+
const ANALYZER = "deadset-ts";
|
|
9
|
+
/** The fewest digits a component identifier's number is written with. */
|
|
10
|
+
const ID_DIGITS = 4;
|
|
11
|
+
/** The index of a declaration the component walk has not reached. */
|
|
12
|
+
const UNVISITED = -1;
|
|
13
|
+
/** What a report carries for one component: its roots and what falls, and its members in full. */
|
|
14
|
+
export function listing(component, cascade) {
|
|
15
|
+
return {
|
|
16
|
+
roots: component.roots,
|
|
17
|
+
members: cascade === "full" ? component.members : [],
|
|
18
|
+
symbolCount: component.falls.length,
|
|
19
|
+
deletableLines: component.deletableLines,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Groups the declarations `dead` marks into components and orders them so each precedes
|
|
24
|
+
* every component it reaches, two that neither reaches ordered by their first member's
|
|
25
|
+
* site. `dead` and `testOfDeadCode` hold one flag per declaration of the graph; the dead
|
|
26
|
+
* set arrives rather than being read from a sweep, so a set several sweeps intersect is
|
|
27
|
+
* grouped over this graph's edges the same way.
|
|
28
|
+
*/
|
|
29
|
+
export function componentsOf(graph, dead, testOfDeadCode) {
|
|
30
|
+
const { at, adjacent } = deadSubgraph(graph, dead, testOfDeadCode);
|
|
31
|
+
if (at.length === 0) {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
const { componentOf, members } = connected(adjacent);
|
|
35
|
+
const { edges, into } = condense(adjacent, componentOf, members.length);
|
|
36
|
+
const falls = fallSets(edges, members, into);
|
|
37
|
+
const outsideReference = externallyReferenced(adjacent, componentOf);
|
|
38
|
+
const idOf = (position) => graph.symbols[at[position] ?? 0]?.id ?? "";
|
|
39
|
+
return order(edges, members).map((group, place) => {
|
|
40
|
+
const own = members[group] ?? [];
|
|
41
|
+
const roots = own.filter((position) => {
|
|
42
|
+
// A dead member of a dead container is never a root: the container is where the
|
|
43
|
+
// deletion starts and the member falls with it.
|
|
44
|
+
const container = graph.parent[at[position] ?? 0] ?? OUTSIDE;
|
|
45
|
+
return outsideReference[position] !== true && dead[container] !== true;
|
|
46
|
+
});
|
|
47
|
+
const fallen = falls[group] ?? [];
|
|
48
|
+
return {
|
|
49
|
+
id: `${ANALYZER}/c-${String(place + 1).padStart(ID_DIGITS, "0")}`,
|
|
50
|
+
members: own.map(idOf),
|
|
51
|
+
roots: roots.map(idOf),
|
|
52
|
+
falls: fallen.map(idOf),
|
|
53
|
+
deletableLines: linesSpanned(fallen.map((position) => graph.symbols[at[position] ?? 0])),
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The distinct source lines a set of declarations spans, each declaration at least the
|
|
59
|
+
* line it starts on. A member written inside its container's span adds no line, because
|
|
60
|
+
* deleting the container removes it.
|
|
61
|
+
*/
|
|
62
|
+
function linesSpanned(symbols) {
|
|
63
|
+
const byPath = new Map();
|
|
64
|
+
for (const symbol of symbols) {
|
|
65
|
+
if (symbol === undefined) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const span = [
|
|
69
|
+
symbol.position.line,
|
|
70
|
+
Math.max(symbol.position.line, symbol.endLine),
|
|
71
|
+
];
|
|
72
|
+
const spans = byPath.get(symbol.position.path);
|
|
73
|
+
if (spans === undefined) {
|
|
74
|
+
byPath.set(symbol.position.path, [span]);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
spans.push(span);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
let total = 0;
|
|
81
|
+
for (const spans of byPath.values()) {
|
|
82
|
+
spans.sort((a, b) => a[0] - b[0]);
|
|
83
|
+
let end = 0;
|
|
84
|
+
for (const [first, last] of spans) {
|
|
85
|
+
total += Math.max(0, last - Math.max(first - 1, end));
|
|
86
|
+
end = Math.max(end, last);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return total;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The dead declarations by their place in the subgraph, and the edges between them: each
|
|
93
|
+
* reference one makes to another, an edge each way between a dead member and its dead
|
|
94
|
+
* container (one direction alone leaves the member a component the container reaches),
|
|
95
|
+
* and an edge back from each production target of an admitted test, which puts the test in
|
|
96
|
+
* the component of the code it exercises because a subject never references its test.
|
|
97
|
+
*/
|
|
98
|
+
function deadSubgraph(graph, dead, testOfDeadCode) {
|
|
99
|
+
const at = [];
|
|
100
|
+
const position = graph.symbols.map((_symbol, index) => {
|
|
101
|
+
if (dead[index] !== true) {
|
|
102
|
+
return UNVISITED;
|
|
103
|
+
}
|
|
104
|
+
at.push(index);
|
|
105
|
+
return at.length - 1;
|
|
106
|
+
});
|
|
107
|
+
const adjacent = at.map(() => []);
|
|
108
|
+
at.forEach((index, from) => {
|
|
109
|
+
for (const edge of graph.out[index] ?? []) {
|
|
110
|
+
const to = position[edge.to] ?? UNVISITED;
|
|
111
|
+
if (to === UNVISITED) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
adjacent[from]?.push(to);
|
|
115
|
+
if (testOfDeadCode[index] === true &&
|
|
116
|
+
graph.test[edge.to] !== true &&
|
|
117
|
+
graph.subject[edge.to] === true) {
|
|
118
|
+
adjacent[to]?.push(from);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const container = position[graph.parent[index] ?? OUTSIDE] ?? UNVISITED;
|
|
122
|
+
if (container !== UNVISITED) {
|
|
123
|
+
adjacent[from]?.push(container);
|
|
124
|
+
adjacent[container]?.push(from);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
return { at, adjacent };
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Each declaration's component and each component's members by site, by Tarjan's
|
|
131
|
+
* algorithm in one pass. The walk keeps its own stack of frames, so a chain of any length
|
|
132
|
+
* is walked without growing the call stack. Components are closed in an order in which
|
|
133
|
+
* each follows every component it reaches.
|
|
134
|
+
*/
|
|
135
|
+
function connected(adjacent) {
|
|
136
|
+
const index = adjacent.map(() => UNVISITED);
|
|
137
|
+
const low = adjacent.map(() => UNVISITED);
|
|
138
|
+
const onStack = adjacent.map(() => false);
|
|
139
|
+
const stack = [];
|
|
140
|
+
const members = [];
|
|
141
|
+
let next = 0;
|
|
142
|
+
const open = (at, frames) => {
|
|
143
|
+
index[at] = next;
|
|
144
|
+
low[at] = next;
|
|
145
|
+
next += 1;
|
|
146
|
+
stack.push(at);
|
|
147
|
+
onStack[at] = true;
|
|
148
|
+
frames.push({ at, next: 0 });
|
|
149
|
+
};
|
|
150
|
+
adjacent.forEach((_edges, start) => {
|
|
151
|
+
if (index[start] !== UNVISITED) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const frames = [];
|
|
155
|
+
open(start, frames);
|
|
156
|
+
for (let frame = frames.at(-1); frame !== undefined; frame = frames.at(-1)) {
|
|
157
|
+
const edges = adjacent[frame.at] ?? [];
|
|
158
|
+
const to = edges[frame.next];
|
|
159
|
+
if (to !== undefined) {
|
|
160
|
+
frame.next += 1;
|
|
161
|
+
if (index[to] === UNVISITED) {
|
|
162
|
+
open(to, frames);
|
|
163
|
+
}
|
|
164
|
+
else if (onStack[to] === true) {
|
|
165
|
+
low[frame.at] = Math.min(low[frame.at] ?? 0, index[to] ?? 0);
|
|
166
|
+
}
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
frames.pop();
|
|
170
|
+
const parent = frames.at(-1);
|
|
171
|
+
if (parent !== undefined) {
|
|
172
|
+
low[parent.at] = Math.min(low[parent.at] ?? 0, low[frame.at] ?? 0);
|
|
173
|
+
}
|
|
174
|
+
if (low[frame.at] !== index[frame.at]) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const group = [];
|
|
178
|
+
for (let top = stack.pop(); top !== undefined; top = stack.pop()) {
|
|
179
|
+
onStack[top] = false;
|
|
180
|
+
group.push(top);
|
|
181
|
+
if (top === frame.at) {
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
members.push(group.sort((a, b) => a - b));
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
const componentOf = adjacent.map(() => 0);
|
|
189
|
+
members.forEach((group, component) => {
|
|
190
|
+
for (const at of group) {
|
|
191
|
+
componentOf[at] = component;
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
return { componentOf, members };
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Per component, the components it reaches in one step, and how many components reach
|
|
198
|
+
* it, each pair counted once. An edge inside a component is not an edge between two, so
|
|
199
|
+
* the result is acyclic.
|
|
200
|
+
*/
|
|
201
|
+
function condense(adjacent, componentOf, count) {
|
|
202
|
+
const edges = Array.from({ length: count }, () => []);
|
|
203
|
+
const into = Array.from({ length: count }, () => 0);
|
|
204
|
+
const seen = new Set();
|
|
205
|
+
adjacent.forEach((targets, from) => {
|
|
206
|
+
for (const to of targets) {
|
|
207
|
+
const a = componentOf[from] ?? 0;
|
|
208
|
+
const b = componentOf[to] ?? 0;
|
|
209
|
+
const pair = `${String(a)}>${String(b)}`;
|
|
210
|
+
if (a === b || seen.has(pair)) {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
seen.add(pair);
|
|
214
|
+
edges[a]?.push(b);
|
|
215
|
+
into[b] = (into[b] ?? 0) + 1;
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
return { edges, into };
|
|
219
|
+
}
|
|
220
|
+
/** Per dead declaration, whether a dead component other than its own references it. */
|
|
221
|
+
function externallyReferenced(adjacent, componentOf) {
|
|
222
|
+
const referenced = adjacent.map(() => false);
|
|
223
|
+
adjacent.forEach((targets, from) => {
|
|
224
|
+
for (const to of targets) {
|
|
225
|
+
if (componentOf[from] !== componentOf[to]) {
|
|
226
|
+
referenced[to] = true;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
return referenced;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* The components in the order a report is worked in. Tarjan closes each component after
|
|
234
|
+
* every one it reaches, so reading the closing order backwards is a topological order,
|
|
235
|
+
* and one pass over it gives each component its depth below the roots.
|
|
236
|
+
*/
|
|
237
|
+
function order(edges, members) {
|
|
238
|
+
const sequence = edges.map((_targets, component) => component).reverse();
|
|
239
|
+
const depth = edges.map(() => 0);
|
|
240
|
+
for (const from of sequence) {
|
|
241
|
+
for (const to of edges[from] ?? []) {
|
|
242
|
+
depth[to] = Math.max(depth[to] ?? 0, (depth[from] ?? 0) + 1);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return sequence.sort((a, b) => (depth[a] ?? 0) - (depth[b] ?? 0) || (members[a]?.[0] ?? 0) - (members[b]?.[0] ?? 0));
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Per component, the dead declarations one deletion of it removes. A root of the
|
|
249
|
+
* condensation carries its members and every component only it reaches; any other
|
|
250
|
+
* component carries its own members alone, because what it reaches is reached through a
|
|
251
|
+
* root above it as well.
|
|
252
|
+
*/
|
|
253
|
+
function fallSets(edges, members, into) {
|
|
254
|
+
// Each root's walk stamps what it reaches with the root's own number, so the walks
|
|
255
|
+
// share one array rather than allocating one per root.
|
|
256
|
+
const stamp = edges.map(() => UNVISITED);
|
|
257
|
+
const reach = new Map();
|
|
258
|
+
const owners = edges.map(() => 0);
|
|
259
|
+
edges.forEach((_targets, root) => {
|
|
260
|
+
if (into[root] !== 0) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const reached = [root];
|
|
264
|
+
stamp[root] = root;
|
|
265
|
+
// An array's iterator reads its length at every step, so what a step pushes is
|
|
266
|
+
// walked in the same loop.
|
|
267
|
+
for (const at of reached) {
|
|
268
|
+
for (const to of edges[at] ?? []) {
|
|
269
|
+
if (stamp[to] !== root) {
|
|
270
|
+
stamp[to] = root;
|
|
271
|
+
reached.push(to);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
reach.set(root, reached);
|
|
276
|
+
for (const component of reached) {
|
|
277
|
+
owners[component] = (owners[component] ?? 0) + 1;
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
return members.map((own, component) => {
|
|
281
|
+
const set = [...own];
|
|
282
|
+
for (const other of reach.get(component) ?? []) {
|
|
283
|
+
if (other !== component && owners[other] === 1) {
|
|
284
|
+
set.push(...(members[other] ?? []));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return set.sort((a, b) => a - b);
|
|
288
|
+
});
|
|
289
|
+
}
|
package/dist/src/discover.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DiagnosticCategory } from "@typescript/native/unstable/sync";
|
|
2
|
-
import { dirnamePath, isAbsolutePath, joinPath, resolvePath } from "./paths.js";
|
|
2
|
+
import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.js";
|
|
3
3
|
/** Directory names discovery does not descend into. */
|
|
4
4
|
const IGNORED_DIRECTORIES = new Set(["node_modules", ".git"]);
|
|
5
5
|
/** A compiler configuration file discovery recognises under a root. */
|
|
@@ -128,24 +128,77 @@ function scopeConfigFiles(host, scope) {
|
|
|
128
128
|
.map((path) => resolvePath(host.workingDirectory(), path));
|
|
129
129
|
}
|
|
130
130
|
/**
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
131
|
+
* The name a derived project carries: its configuration file's path below the target
|
|
132
|
+
* root, or the absolute path of a file outside it.
|
|
133
|
+
*/
|
|
134
|
+
function derivedId(targetRoot, configFile) {
|
|
135
|
+
return relativePath(targetRoot, configFile) ?? configFile;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Resolves one compiler configuration file, or ends the run naming it: a project
|
|
139
|
+
* opened from a configuration the compiler cannot read would be analyzed with no
|
|
140
|
+
* type information.
|
|
141
|
+
*/
|
|
142
|
+
function parseOrRefuse(engine, configFile, named) {
|
|
143
|
+
try {
|
|
144
|
+
engine.parseConfigFile(configFile);
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
throw new DiscoveryError(`${named} cannot be read as a compiler configuration: ` +
|
|
148
|
+
(error instanceof Error ? error.message : String(error)), []);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* The projects a build matrix declares, in the order it lists them. Each entry
|
|
153
|
+
* names a file below the target root, and an entry naming a file that is absent,
|
|
154
|
+
* that is a directory, or that the compiler cannot read ends the run naming it.
|
|
141
155
|
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
156
|
+
* A declared project's `references` are not followed, because the entries are the
|
|
157
|
+
* matrix, and a reference to a configuration the matrix does not name ends the run
|
|
158
|
+
* naming both: the compiler builds a referenced project's sources into the program
|
|
159
|
+
* that references it without reporting their diagnostics, so the project the matrix
|
|
160
|
+
* left out would be analyzed with its errors unread. An entry naming a file an
|
|
161
|
+
* earlier entry named adds nothing, so the earlier identifier stands.
|
|
162
|
+
*/
|
|
163
|
+
function declaredProjects(engine, host, targetRoot, entries) {
|
|
164
|
+
const seen = new Set();
|
|
165
|
+
const projects = [];
|
|
166
|
+
for (const entry of entries) {
|
|
167
|
+
const configFile = joinPath(targetRoot, entry.project);
|
|
168
|
+
const named = `the project ${JSON.stringify(entry.id)} (${configFile})`;
|
|
169
|
+
const kind = host.kindOf(configFile);
|
|
170
|
+
if (kind !== "file") {
|
|
171
|
+
throw new DiscoveryError(kind === "absent"
|
|
172
|
+
? `${named} does not exist`
|
|
173
|
+
: `${named} is a directory, and a project names its compiler configuration file`, []);
|
|
174
|
+
}
|
|
175
|
+
if (seen.has(configFile)) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
seen.add(configFile);
|
|
179
|
+
parseOrRefuse(engine, configFile, named);
|
|
180
|
+
projects.push({ id: entry.id, configFile });
|
|
181
|
+
}
|
|
182
|
+
for (const project of projects) {
|
|
183
|
+
const unnamed = referencesOf(host, project.configFile).find((path) => !seen.has(path));
|
|
184
|
+
if (unnamed !== undefined) {
|
|
185
|
+
throw new DiscoveryError(`the project ${JSON.stringify(project.id)} (${project.configFile}) references ` +
|
|
186
|
+
`${unnamed}, which the build matrix does not name; a referenced project is built ` +
|
|
187
|
+
"into the program that references it, so the matrix names it too", []);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return { projects, configFiles: projects.map((project) => project.configFile), fromScope: [] };
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* The projects a run derives where the matrix declares none, in the order discovery
|
|
194
|
+
* reads them: the paths the scope names, then every compiler configuration under
|
|
195
|
+
* the target root that is not inside an ignored directory, then the configurations
|
|
196
|
+
* each of those references, transitively. Each configuration appears once.
|
|
144
197
|
*/
|
|
145
|
-
|
|
198
|
+
function derivedProjects(engine, host, scope) {
|
|
146
199
|
const fromScope = scopeConfigFiles(host, scope);
|
|
147
200
|
const seen = new Set();
|
|
148
|
-
const
|
|
201
|
+
const projects = [];
|
|
149
202
|
const pending = [...fromScope, ...configFilesUnder(host, scope.target.path)];
|
|
150
203
|
while (pending.length > 0) {
|
|
151
204
|
const configFile = pending.shift();
|
|
@@ -157,15 +210,36 @@ export function discoverProjects(engine, host, scope) {
|
|
|
157
210
|
continue;
|
|
158
211
|
}
|
|
159
212
|
seen.add(path);
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
catch (error) {
|
|
164
|
-
throw new DiscoveryError(`${path} cannot be read as a compiler configuration: ` +
|
|
165
|
-
(error instanceof Error ? error.message : String(error)), []);
|
|
166
|
-
}
|
|
167
|
-
configFiles.push(path);
|
|
213
|
+
parseOrRefuse(engine, path, path);
|
|
214
|
+
projects.push({ id: derivedId(scope.target.path, path), configFile: path });
|
|
168
215
|
pending.push(...referencesOf(host, path));
|
|
169
216
|
}
|
|
170
|
-
return { configFiles, fromScope };
|
|
217
|
+
return { projects, configFiles: projects.map((project) => project.configFile), fromScope };
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Every project one run analyzes.
|
|
221
|
+
*
|
|
222
|
+
* The project entries of the build matrix, where it holds any, are the projects:
|
|
223
|
+
* exactly those, in the order the matrix lists them, each named by its declared
|
|
224
|
+
* identifier, as {@link declaredProjects} reads them. A matrix holding no project
|
|
225
|
+
* entry, the empty one included, leaves discovery to derive the projects from the
|
|
226
|
+
* scope and the target tree. A platform entry names a configuration of another
|
|
227
|
+
* language and plays no part here.
|
|
228
|
+
*
|
|
229
|
+
* A configuration file the compiler cannot read ends the run with the failure code,
|
|
230
|
+
* so a project that would have been analyzed with no type information is never
|
|
231
|
+
* analyzed at all, whether the matrix declared it or discovery derived it. What a
|
|
232
|
+
* configuration's own contents say is refused once its project is open, by
|
|
233
|
+
* {@link diagnosticErrors} over the three sets a program reports, because a
|
|
234
|
+
* resolution carries no diagnostics.
|
|
235
|
+
*
|
|
236
|
+
* Nothing reads a build output: a project is the files its configuration names, so a
|
|
237
|
+
* package whose sources ship as TypeScript is analyzed as it stands.
|
|
238
|
+
*/
|
|
239
|
+
export function discoverProjects(engine, host, scope, configurations = []) {
|
|
240
|
+
const declared = configurations.filter((entry) => entry.shape === "project");
|
|
241
|
+
if (declared.length > 0) {
|
|
242
|
+
return declaredProjects(engine, host, scope.target.path, declared);
|
|
243
|
+
}
|
|
244
|
+
return derivedProjects(engine, host, scope);
|
|
171
245
|
}
|