@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.
@@ -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
- * Every project one run analyzes, in the order discovery reads them: the paths the
132
- * scope names, then every compiler configuration under the target root that is not
133
- * inside an ignored directory, then the configurations each of those references,
134
- * transitively. Each configuration appears once.
135
- *
136
- * A configuration file the compiler cannot read ends the run with the failure code,
137
- * so a project that would have been analyzed with no type information is never
138
- * analyzed at all. What a configuration's own contents say is refused once its
139
- * project is open, by {@link diagnosticErrors} over the three sets a program
140
- * reports, because a resolution carries no diagnostics.
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
- * Nothing reads a build output: a project is the files its configuration names, so a
143
- * package whose sources ship as TypeScript is analyzed as it stands.
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
- export function discoverProjects(engine, host, scope) {
198
+ function derivedProjects(engine, host, scope) {
146
199
  const fromScope = scopeConfigFiles(host, scope);
147
200
  const seen = new Set();
148
- const configFiles = [];
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
- try {
161
- engine.parseConfigFile(path);
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
  }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * One project's declarations, the references between them and the roots that seed
3
+ * reachability, indexed for the sweep. The graph reads no file and asks the compiler
4
+ * nothing, so a sweep over it is a function of the values it was built from, and the
5
+ * inventory's order is the order every set a sweep answers reads in.
6
+ */
7
+ /** The position of a declaration the inventory does not hold. */
8
+ export const OUTSIDE = -1;
9
+ /**
10
+ * Whether the sweep judges a declaration of each kind. A file is not judged by either
11
+ * relation: whether a file is built and whether anything imports it are the subjects of
12
+ * the file kinds, which read the program rather than ask whether anything references the
13
+ * file. The Contract's finding schema names the kinds that carry no liveness relation,
14
+ * and a test holds this table to that list.
15
+ */
16
+ const SWEPT = {
17
+ file: false,
18
+ function: true,
19
+ class: true,
20
+ interface: true,
21
+ type: true,
22
+ enum: true,
23
+ namespace: true,
24
+ variable: true,
25
+ "export-alias": true,
26
+ method: true,
27
+ "class-member": true,
28
+ "interface-method": true,
29
+ "type-member": true,
30
+ "enum-member": true,
31
+ "type-parameter": true,
32
+ };
33
+ /**
34
+ * Whether a root of each kind names a caller, which holds it live under both relations: a
35
+ * tool reads the configuration it loads, a runtime runs a test file, a setup file and a
36
+ * worker, and a configured root asserts a caller. The other kinds only suppose a consumer
37
+ * that imports what a manifest entry, a binary or an entry file exports, or names a
38
+ * library's published API, so such a root is live under reachability alone.
39
+ */
40
+ const NAMES_A_CALLER = {
41
+ "entry-file": false,
42
+ "manifest-entry": false,
43
+ "manifest-binary": false,
44
+ "published-api": false,
45
+ configured: true,
46
+ pattern: true,
47
+ "test-runner": true,
48
+ "lint-configuration": true,
49
+ "mutation-testing": true,
50
+ "browser-tests": true,
51
+ worker: true,
52
+ };
53
+ /** Whether the sweep judges a declaration of one kind. */
54
+ export function swept(kind) {
55
+ return SWEPT[kind];
56
+ }
57
+ /** Whether a root of one kind names a caller rather than supposing one. */
58
+ export function namesACaller(kind) {
59
+ return NAMES_A_CALLER[kind];
60
+ }
61
+ /**
62
+ * Indexes one project's inventory, references and roots. `testFiles` are the paths the
63
+ * reference pass classified as test files, so a declaration one holds is a test
64
+ * declaration. A reference to a declaration the inventory does not hold is dropped; one
65
+ * from such a declaration still counts toward its target, because reference counting
66
+ * asks whether any declaration names the target. A root naming nothing is dropped.
67
+ */
68
+ export function graphOf(symbols, references, roots, testFiles) {
69
+ const index = new Map(symbols.map((symbol, at) => [symbol.id, at]));
70
+ const at = (id) => index.get(id) ?? OUTSIDE;
71
+ const tests = new Set(testFiles);
72
+ const out = symbols.map(() => []);
73
+ const made = symbols.map(() => ({ production: 0, test: 0 }));
74
+ for (const reference of references) {
75
+ const to = at(reference.to);
76
+ if (to === OUTSIDE) {
77
+ continue;
78
+ }
79
+ const counts = made[to];
80
+ if (counts !== undefined) {
81
+ if (reference.test) {
82
+ counts.test += 1;
83
+ }
84
+ else {
85
+ counts.production += 1;
86
+ }
87
+ }
88
+ out[at(reference.from)]?.push({
89
+ to,
90
+ test: reference.test,
91
+ evaluation: reference.use === "evaluation",
92
+ });
93
+ }
94
+ const parent = symbols.map((symbol) => at(symbol.parent));
95
+ const exportsOf = symbols.map(() => []);
96
+ symbols.forEach((symbol, position) => {
97
+ const container = parent[position] ?? OUTSIDE;
98
+ if (symbol.exported && symbols[container]?.kind === "file") {
99
+ exportsOf[container]?.push(position);
100
+ }
101
+ });
102
+ const rooted = [];
103
+ for (const root of roots) {
104
+ const position = at(root.id);
105
+ if (position !== OUTSIDE) {
106
+ rooted.push({ at: position, kind: root.kind });
107
+ }
108
+ }
109
+ return {
110
+ symbols,
111
+ out,
112
+ made,
113
+ parent,
114
+ test: symbols.map((symbol) => tests.has(symbol.position.path)),
115
+ subject: symbols.map((symbol) => swept(symbol.kind)),
116
+ exportsOf,
117
+ rooted,
118
+ at,
119
+ };
120
+ }
package/dist/src/index.js CHANGED
@@ -1,7 +1,5 @@
1
1
  export { run, SETTING_OPTIONS } from "./run.js";
2
2
  export { DECLINED_CONVENTIONS } from "./entry-point-gaps.js";
3
- export { inventory, nodeKey, } from "./inventory.js";
4
- export { DEFAULT_BATCH_CAP, references, } from "./references.js";
5
3
  export { byPosition, positionKey, PositionError, renderPosition, } from "./position.js";
6
4
  export { computedComponent, isRef, nameComponent, REF_EXPRESSIONS, renderRef, } from "./ref.js";
7
5
  export { CONTRACT_VERSION } from "./version.js";
@@ -0,0 +1,138 @@
1
+ /**
2
+ * One run's projects as one matrix: every declaration any project holds, keyed by its
3
+ * position and carrying the configurations that hold it, swept once per configuration
4
+ * and intersected, with the dead components computed once over every configuration.
5
+ */
6
+ import { componentsOf } from "./components.js";
7
+ import { graphOf } from "./graph.js";
8
+ import { byPosition } from "./position.js";
9
+ import { sweep } from "./sweep.js";
10
+ /** Two strings ordered bytewise. */
11
+ function compare(a, b) {
12
+ if (a === b) {
13
+ return 0;
14
+ }
15
+ return a < b ? -1 : 1;
16
+ }
17
+ /** What makes one reference one reference, whichever configuration made it. */
18
+ function referenceKey(reference) {
19
+ const { path, line, column } = reference.position;
20
+ return [reference.from, reference.to, path, String(line), String(column), reference.use].join("\u0000");
21
+ }
22
+ /**
23
+ * Indexes what the passes answered for each configuration of one run, in the run's order.
24
+ *
25
+ * A declaration is keyed across configurations by its identifier, which is its rendered
26
+ * position and so is the same in every configuration that compiles its file; the fields
27
+ * of the first configuration that holds it are the ones kept. A declaration one
28
+ * configuration alone holds is in the union with that one configuration, so it is
29
+ * judged where it exists rather than skipped.
30
+ *
31
+ * One reference written at one position of a file several configurations compile is seen
32
+ * once per configuration and is one reference, so the union holds it once; two references
33
+ * one configuration makes at one position are two.
34
+ */
35
+ export function matrixOf(per) {
36
+ const symbols = [];
37
+ const holders = new Map();
38
+ const references = [];
39
+ const firstSeen = new Map();
40
+ per.forEach((one, index) => {
41
+ for (const symbol of one.symbols) {
42
+ const held = holders.get(symbol.id);
43
+ if (held === undefined) {
44
+ holders.set(symbol.id, [one.configuration]);
45
+ symbols.push(symbol);
46
+ }
47
+ else if (!held.includes(one.configuration)) {
48
+ held.push(one.configuration);
49
+ }
50
+ }
51
+ for (const reference of one.references) {
52
+ const key = referenceKey(reference);
53
+ const seen = firstSeen.get(key);
54
+ if (seen === undefined) {
55
+ firstSeen.set(key, index);
56
+ }
57
+ else if (seen !== index) {
58
+ continue;
59
+ }
60
+ references.push(reference);
61
+ }
62
+ });
63
+ symbols.sort((a, b) => byPosition(a.position, b.position) || compare(a.ref, b.ref));
64
+ return {
65
+ configurations: per.map((one) => one.configuration),
66
+ graphs: per.map((one) => graphOf(one.symbols, one.references, one.roots, one.testFiles)),
67
+ union: graphOf(symbols, references, per.flatMap((one) => one.roots), [...new Set(per.flatMap((one) => one.testFiles))]),
68
+ heldIn: symbols.map((symbol) => holders.get(symbol.id) ?? []),
69
+ };
70
+ }
71
+ /**
72
+ * Answers which declarations of the run are dead in every configuration that holds them.
73
+ *
74
+ * Liveness is a question about one configuration, because a file another configuration
75
+ * does not compile makes no reference there and a root it does not hold seeds nothing
76
+ * there, so each configuration is swept on its own graph under the run's input. A
77
+ * declaration is a candidate when every configuration holding it judged it one: a
78
+ * reference under one configuration is a reference, so what one project uses is never
79
+ * reported, and a configuration that does not hold a declaration says nothing about it.
80
+ * The candidate's relation is reference counting where every configuration found it so,
81
+ * and reachability otherwise; its counts are the union's, one per reference.
82
+ *
83
+ * The components are computed once, over the union and the intersected dead set, so one
84
+ * dead component is one entry of the run with one identifier.
85
+ */
86
+ export function sweepMatrix(matrix, input) {
87
+ const per = matrix.graphs.map((graph) => sweep(graph, input));
88
+ const byConfiguration = new Map(matrix.configurations.map((configuration, index) => [
89
+ configuration,
90
+ new Map((per[index]?.candidates ?? []).map((candidate) => [candidate.id, candidate])),
91
+ ]));
92
+ const union = matrix.union;
93
+ const dead = union.symbols.map(() => false);
94
+ const testOfDeadCode = union.symbols.map(() => false);
95
+ const candidates = [];
96
+ union.symbols.forEach((symbol, at) => {
97
+ const held = matrix.heldIn[at] ?? [];
98
+ const found = held.map((configuration) => byConfiguration.get(configuration)?.get(symbol.id));
99
+ if (held.length === 0 || found.some((candidate) => candidate === undefined)) {
100
+ return;
101
+ }
102
+ const all = found.filter((candidate) => candidate !== undefined);
103
+ const admitted = all.every((candidate) => candidate.testOfDeadCode);
104
+ dead[at] = true;
105
+ testOfDeadCode[at] = admitted;
106
+ candidates.push({
107
+ id: symbol.id,
108
+ productionRefs: union.made[at]?.production ?? 0,
109
+ testRefs: union.made[at]?.test ?? 0,
110
+ relation: all.every((candidate) => candidate.relation === "reference-counting")
111
+ ? "reference-counting"
112
+ : "reachability",
113
+ testOfDeadCode: admitted,
114
+ configurations: held,
115
+ });
116
+ });
117
+ return {
118
+ liveUnder: liveAnywhere(union.symbols, per),
119
+ candidates,
120
+ components: componentsOf(union, dead, testOfDeadCode),
121
+ };
122
+ }
123
+ /** The relations in the order a declaration's entry lists them. */
124
+ const RELATIONS = ["reference-counting", "reachability"];
125
+ /**
126
+ * Per declaration, in site order, every relation that holds it live in at least one
127
+ * configuration.
128
+ */
129
+ function liveAnywhere(symbols, per) {
130
+ const live = new Map();
131
+ for (const symbol of symbols) {
132
+ const relations = RELATIONS.filter((relation) => per.some((liveness) => (liveness.liveUnder.get(symbol.id) ?? []).includes(relation)));
133
+ if (relations.length > 0) {
134
+ live.set(symbol.id, relations);
135
+ }
136
+ }
137
+ return live;
138
+ }
package/dist/src/print.js CHANGED
@@ -19,12 +19,9 @@ export function printConfig(config, provenance) {
19
19
  min_confidence: config.analysis.minConfidence,
20
20
  generated_files: config.analysis.generatedFiles,
21
21
  consumer_tests: config.analysis.consumerTests,
22
- configurations: config.analysis.configurations.map((entry) => ({
23
- id: entry.id,
24
- os: entry.os,
25
- arch: entry.arch,
26
- tags: entry.tags,
27
- })),
22
+ configurations: config.analysis.configurations.map((entry) => entry.shape === "project"
23
+ ? { id: entry.id, project: entry.project }
24
+ : { id: entry.id, os: entry.os, arch: entry.arch, tags: entry.tags }),
28
25
  matrix: { complete: config.analysis.matrixComplete },
29
26
  template_dirs: config.analysis.templateDirs,
30
27
  template_delimiters: {