@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/src/resolve.ts CHANGED
@@ -38,6 +38,24 @@ const SEVERITY_KEY = /^DS[0-9]{2}([0-9]{2})?$/u;
38
38
  const EXEMPTION_CLASS = /^[a-z][a-z0-9-]*$/u;
39
39
  const CONTRACT_VERSION_PATTERN = /^[0-9]+\.[0-9]+\.[0-9]+$/u;
40
40
 
41
+ /** The members only a platform build configuration carries. */
42
+ const PLATFORM_MEMBERS = ["os", "arch", "tags"] as const;
43
+
44
+ /** The member only a project build configuration carries. */
45
+ const PROJECT_MEMBER = "project";
46
+
47
+ /**
48
+ * One segment of a project path: not empty, not `.` or `..`, and holding no
49
+ * solidus, backslash or line break.
50
+ */
51
+ const PROJECT_SEGMENT = String.raw`(?:[^/\\.\r\n][^/\\\r\n]*|\.[^/\\.\r\n][^/\\\r\n]*|\.\.[^/\\\r\n]+)`;
52
+
53
+ /**
54
+ * A project path: segments joined by a solidus, so the path is relative, below
55
+ * the root it is read against, and spelled one way on every machine.
56
+ */
57
+ const PROJECT_PATH = new RegExp(`^${PROJECT_SEGMENT}(?:/${PROJECT_SEGMENT})*$`, "u");
58
+
41
59
  const TARGET_KINDS: readonly TargetKind[] = ["application", "library"];
42
60
  const CONFIDENCES: readonly Confidence[] = ["certain", "probable", "possible"];
43
61
  const GENERATED_FILES: readonly GeneratedFiles[] = ["exclude", "include"];
@@ -252,10 +270,75 @@ function requireMember(value: string | undefined, path: string, label: string):
252
270
  return value;
253
271
  }
254
272
 
273
+ /** One build configuration entry as a platform: an identifier, an operating system and an architecture. */
274
+ function readPlatform(
275
+ entry: Record<string, unknown>,
276
+ at: string,
277
+ label: string,
278
+ ): BuildConfiguration {
279
+ return {
280
+ shape: "platform",
281
+ id: requireMember(readString(entry, "id", `${at}.id`, label), `${at}.id`, label),
282
+ os: requireMember(readString(entry, "os", `${at}.os`, label), `${at}.os`, label),
283
+ arch: requireMember(readString(entry, "arch", `${at}.arch`, label), `${at}.arch`, label),
284
+ tags: readStrings(entry, "tags", `${at}.tags`, label, 0) ?? [],
285
+ };
286
+ }
287
+
288
+ /** One build configuration entry as a project: an identifier and a configuration file below the target root. */
289
+ function readProject(
290
+ entry: Record<string, unknown>,
291
+ at: string,
292
+ label: string,
293
+ ): BuildConfiguration {
294
+ const id = requireMember(readString(entry, "id", `${at}.id`, label), `${at}.id`, label);
295
+ const path = `${at}.${PROJECT_MEMBER}`;
296
+ const project = requireMember(readString(entry, PROJECT_MEMBER, path, label), path, label);
297
+ if (!PROJECT_PATH.test(project)) {
298
+ throw malformed(
299
+ label,
300
+ path,
301
+ `${JSON.stringify(project)} is not a path below the target root: segments joined by /, ` +
302
+ "none empty, . or .., and no backslash or line break",
303
+ );
304
+ }
305
+ return { shape: "project", id, project };
306
+ }
307
+
255
308
  /**
256
- * The build matrix: every entry names an identifier, an operating system and an
257
- * architecture.
309
+ * One build configuration entry, read as the one shape its members name: a member
310
+ * only a platform carries makes it a platform, the member only a project carries
311
+ * makes it a project, and an entry naming members of both, or of neither, is
312
+ * refused at the entry, because the two shapes are exclusive.
258
313
  */
314
+ function readConfiguration(entry: unknown, at: string, label: string): BuildConfiguration {
315
+ if (!isRecord(entry)) {
316
+ throw malformed(label, at, "is not an object");
317
+ }
318
+ const platform = PLATFORM_MEMBERS.filter((name) => Object.hasOwn(entry, name));
319
+ const project = Object.hasOwn(entry, PROJECT_MEMBER);
320
+ if (platform.length > 0 && project) {
321
+ throw malformed(
322
+ label,
323
+ at,
324
+ `carries ${spell(platform)} of a platform and ${JSON.stringify(PROJECT_MEMBER)} of a ` +
325
+ "project, and an entry is one shape or the other",
326
+ );
327
+ }
328
+ if (project) {
329
+ return readProject(entry, at, label);
330
+ }
331
+ if (platform.length === 0) {
332
+ throw malformed(
333
+ label,
334
+ at,
335
+ `names neither a platform's "os" and "arch" nor a project's ${JSON.stringify(PROJECT_MEMBER)}`,
336
+ );
337
+ }
338
+ return readPlatform(entry, at, label);
339
+ }
340
+
341
+ /** The build matrix, one entry in one shape at a time. */
259
342
  function readConfigurations(
260
343
  analysis: Record<string, unknown> | undefined,
261
344
  label: string,
@@ -267,18 +350,9 @@ function readConfigurations(
267
350
  if (!Array.isArray(value)) {
268
351
  throw malformed(label, "analysis.configurations", "is not an array");
269
352
  }
270
- return (value as unknown[]).map((entry, index) => {
271
- const at = `analysis.configurations[${String(index)}]`;
272
- if (!isRecord(entry)) {
273
- throw malformed(label, at, "is not an object");
274
- }
275
- return {
276
- id: requireMember(readString(entry, "id", `${at}.id`, label), `${at}.id`, label),
277
- os: requireMember(readString(entry, "os", `${at}.os`, label), `${at}.os`, label),
278
- arch: requireMember(readString(entry, "arch", `${at}.arch`, label), `${at}.arch`, label),
279
- tags: readStrings(entry, "tags", `${at}.tags`, label, 0) ?? [],
280
- };
281
- });
353
+ return (value as unknown[]).map((entry, index) =>
354
+ readConfiguration(entry, `analysis.configurations[${String(index)}]`, label),
355
+ );
282
356
  }
283
357
 
284
358
  /**
@@ -642,6 +716,35 @@ function checkLabels(provenance: ReadonlyMap<string, Origin>): void {
642
716
  * the usage exit code.
643
717
  */
644
718
  export function resolve(inputs: Inputs): { config: Config; provenance: Provenance } {
719
+ const { config, provenance } = settleAll(inputs);
720
+ if (config.targetKind === "") {
721
+ throw new ConfigError(
722
+ "missing-target-kind",
723
+ "target.kind",
724
+ "target.kind is not set, it has no default and is never inferred: searched " +
725
+ `${describeSource("the repository configuration", inputs.repository, inputs.repositoryLabel)} and ` +
726
+ describeSource("the central configuration", inputs.central, inputs.centralLabel),
727
+ );
728
+ }
729
+ checkLabels(provenance);
730
+ return { config, provenance };
731
+ }
732
+
733
+ /**
734
+ * The build matrix the configuration documents resolve to, every document read and
735
+ * refused as {@link resolve} refuses it, and nothing else required of them: which
736
+ * projects a run analyzes depends on the matrix alone, so a question about the
737
+ * projects asks no target kind.
738
+ */
739
+ export function resolveMatrix(inputs: Inputs): readonly BuildConfiguration[] {
740
+ return settleAll(inputs).config.analysis.configurations;
741
+ }
742
+
743
+ /**
744
+ * Every setting the documents supply, each from the highest-ranked source carrying
745
+ * it, with the origin of each. The target kind is empty where no source names one.
746
+ */
747
+ function settleAll(inputs: Inputs): { config: Config; provenance: Map<string, Origin> } {
645
748
  const sources = readSources(inputs);
646
749
  const defaults = defaultConfig();
647
750
  const provenance = new Map<string, Origin>();
@@ -718,15 +821,5 @@ export function resolve(inputs: Inputs): { config: Config; provenance: Provenanc
718
821
  },
719
822
  };
720
823
 
721
- if (config.targetKind === "") {
722
- throw new ConfigError(
723
- "missing-target-kind",
724
- "target.kind",
725
- "target.kind is not set, it has no default and is never inferred: searched " +
726
- `${describeSource("the repository configuration", inputs.repository, inputs.repositoryLabel)} and ` +
727
- describeSource("the central configuration", inputs.central, inputs.centralLabel),
728
- );
729
- }
730
- checkLabels(provenance);
731
824
  return { config, provenance };
732
825
  }
package/src/run.ts CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  import type { Host } from "./host.ts";
10
10
  import { joinPath, resolvePath } from "./paths.ts";
11
11
  import { printConfig } from "./print.ts";
12
- import { resolve } from "./resolve.ts";
12
+ import { resolve, resolveMatrix } from "./resolve.ts";
13
13
  import { readScope, ScopeError, scopeForDir, type Scope } from "./scope.ts";
14
14
  import { diagnosticsOf, openEngine, runSession, type Engine } from "./session.ts";
15
15
  import { CONTRACT_VERSION } from "./version.ts";
@@ -245,8 +245,10 @@ function printConfigVerb(args: readonly string[], out: Writer, err: Writer, host
245
245
  }
246
246
 
247
247
  /**
248
- * Writes the compiler configuration of every project one run analyzes, one per
249
- * line, in discovery order, having read each project's diagnostics first.
248
+ * Writes the name of every project one run analyzes, one per line, having read each
249
+ * project's diagnostics first: the identifier the build matrix declares for it, or,
250
+ * where discovery derived the project, its configuration file's path below the
251
+ * target root.
250
252
  */
251
253
  function printProjectsVerb(
252
254
  args: readonly string[],
@@ -258,14 +260,16 @@ function printProjectsVerb(
258
260
  let engine: Engine | undefined;
259
261
  try {
260
262
  const options = readOptions(args);
263
+ const matrix = resolveMatrix(inputsOf(host, options));
261
264
  const scope = scopeOf(host, options);
262
265
  engine = openClient(false);
263
- const discovered = discoverProjects(engine, host, scope);
266
+ const discovered = discoverProjects(engine, host, scope, matrix);
267
+ const ids = new Map(discovered.projects.map((project) => [project.configFile, project.id]));
264
268
  const failures: string[] = [];
265
269
  const { projects } = runSession(engine, discovered.configFiles, (project) => {
266
270
  const errors = diagnosticErrors(diagnosticsOf(project));
267
271
  failures.push(...errors.map(renderDiagnostic));
268
- return project.configFile;
272
+ return ids.get(project.configFile) ?? project.configFile;
269
273
  });
270
274
  if (failures.length > 0) {
271
275
  for (const line of failures) {
@@ -274,8 +278,8 @@ function printProjectsVerb(
274
278
  err.write(`deadset-ts: ${String(failures.length)} error(s); no answer was produced\n`);
275
279
  return EXIT_FAILURE;
276
280
  }
277
- for (const configFile of projects) {
278
- out.write(`${configFile}\n`);
281
+ for (const id of projects) {
282
+ out.write(`${id}\n`);
279
283
  }
280
284
  return EXIT_CLEAN;
281
285
  } catch (error: unknown) {
package/src/schema.ts CHANGED
@@ -8,7 +8,10 @@ import { ConfigError } from "./config.ts";
8
8
  * holds no value of its own.
9
9
  * - `map` is an object whose member names the key list leaves open, its values
10
10
  * scalars: the severity and provenance objects.
11
- * - `list` is an array whose entries are objects with declared members.
11
+ * - `list` is an array whose entries are objects with declared members. Where
12
+ * the key list admits an entry in more than one shape, the members are every
13
+ * shape's together, and which of them one entry may hold is the decoder's
14
+ * refusal, not the walk's.
12
15
  * - `object` is one setting written as an object: its member names are declared
13
16
  * and a source supplies the object whole.
14
17
  */
@@ -44,7 +47,7 @@ export const SCHEMA_ROOT: KeyNode = {
44
47
  consumer_tests: leaf,
45
48
  configurations: {
46
49
  kind: "list",
47
- members: { id: leaf, os: leaf, arch: leaf, tags: leaf },
50
+ members: { id: leaf, os: leaf, arch: leaf, tags: leaf, project: leaf },
48
51
  },
49
52
  matrix: { kind: "section", members: { complete: leaf } },
50
53
  template_dirs: leaf,
package/src/sweep.ts ADDED
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Which declarations of one graph are dead, under which liveness relation, and which
3
+ * dead component each belongs to. Reference counting holds a declaration live when any
4
+ * declaration references it, whatever that one's own liveness, and no root plays a part;
5
+ * reachability holds it live when a path of references reaches it from a root. So a
6
+ * declaration only unreachable ones reference is live under the first, dead under the second.
7
+ */
8
+
9
+ import { componentsOf, type Component } from "./components.ts";
10
+ import { namesACaller, OUTSIDE, type Graph } from "./graph.ts";
11
+
12
+ /** The liveness relation that decided a declaration, as a finding spells it. */
13
+ export type Relation = "reference-counting" | "reachability";
14
+
15
+ /**
16
+ * The run's reference mode, decided once by the composition and read by every stage
17
+ * that counts references.
18
+ */
19
+ export interface Mode {
20
+ /**
21
+ * Counts no reference a test file made, under either relation. Every reference a root
22
+ * in a test file makes is such a reference, so nothing only a test reaches is live.
23
+ */
24
+ readonly production: boolean;
25
+ }
26
+
27
+ /** What one sweep runs over besides the graph. */
28
+ export interface SweepInput {
29
+ /**
30
+ * The declarations a matched suppression names. A mark holds its declaration live
31
+ * under both relations and seeds reachability through every reference it makes, so
32
+ * nothing only a marked declaration references is dead.
33
+ */
34
+ readonly marked: readonly string[];
35
+ readonly mode: Mode;
36
+ }
37
+
38
+ /** One dead declaration, the relation that found it, and the counts a kind reads. */
39
+ export interface Candidate {
40
+ readonly id: string;
41
+ /** The references a production file made to the declaration, whatever the mode counts. */
42
+ readonly productionRefs: number;
43
+ /** The references a test file made to it. */
44
+ readonly testRefs: number;
45
+ /**
46
+ * Reference counting where the mode counts no reference to the declaration, and
47
+ * reachability otherwise, so a declaration dead under both carries the stronger claim.
48
+ */
49
+ readonly relation: Relation;
50
+ /**
51
+ * Whether the test-of-dead-code rule admitted the declaration: a test declaration whose
52
+ * set of referenced production declarations is non-empty and wholly dead.
53
+ */
54
+ readonly testOfDeadCode: boolean;
55
+ }
56
+
57
+ /** One sweep's answer. */
58
+ export interface SweepResult {
59
+ /**
60
+ * Per declaration, the relations that hold it live, reference counting first. A
61
+ * declaration neither holds live has no entry.
62
+ */
63
+ readonly liveUnder: ReadonlyMap<string, readonly Relation[]>;
64
+ /** The dead declarations, in the order the inventory holds them. */
65
+ readonly candidates: readonly Candidate[];
66
+ /** The dead components, each before every component it reaches. */
67
+ readonly components: readonly Component[];
68
+ }
69
+
70
+ /**
71
+ * Answers which declarations of one graph are dead under the input's mode.
72
+ *
73
+ * The order is the order the answers depend on: the callers and the marks are live
74
+ * before either relation runs, the candidate set is every judged declaration at least
75
+ * one relation does not hold live, the tests of dead code join it, and the components
76
+ * are computed over the set that results.
77
+ */
78
+ export function sweep(graph: Graph, input: SweepInput): SweepResult {
79
+ const marked = graph.symbols.map(() => false);
80
+ for (const id of input.marked) {
81
+ const at = graph.at(id);
82
+ if (at !== OUTSIDE) {
83
+ marked[at] = true;
84
+ }
85
+ }
86
+ // Every root and every mark seeds reachability. A root of a kind that names a caller,
87
+ // and a mark, are live under reference counting as well; a root that only supposes a
88
+ // caller is not.
89
+ const called = graph.symbols.map(() => false);
90
+ for (const root of graph.rooted) {
91
+ if (namesACaller(root.kind)) {
92
+ called[root.at] = true;
93
+ }
94
+ }
95
+
96
+ const counted = (at: number): number => {
97
+ const made = graph.made[at];
98
+ if (made === undefined) {
99
+ return 0;
100
+ }
101
+ return input.mode.production ? made.production : made.production + made.test;
102
+ };
103
+ const reached = reachable(graph, marked, input.mode);
104
+ const liveUnder = graph.symbols.map((_symbol, at): readonly Relation[] => {
105
+ const relations: Relation[] = [];
106
+ if (marked[at] === true || called[at] === true || counted(at) > 0) {
107
+ relations.push("reference-counting");
108
+ }
109
+ if (reached[at] === true) {
110
+ relations.push("reachability");
111
+ }
112
+ return relations;
113
+ });
114
+
115
+ const dead = graph.symbols.map(
116
+ (_symbol, at) => graph.subject[at] === true && (liveUnder[at]?.length ?? 0) < 2,
117
+ );
118
+ const testOfDeadCode = admitTestsOfDeadCode(graph, dead, marked);
119
+
120
+ const candidates: Candidate[] = [];
121
+ const live = new Map<string, readonly Relation[]>();
122
+ graph.symbols.forEach((symbol, at) => {
123
+ const relations = liveUnder[at] ?? [];
124
+ if (relations.length > 0) {
125
+ live.set(symbol.id, relations);
126
+ }
127
+ if (dead[at] !== true) {
128
+ return;
129
+ }
130
+ candidates.push({
131
+ id: symbol.id,
132
+ productionRefs: graph.made[at]?.production ?? 0,
133
+ testRefs: graph.made[at]?.test ?? 0,
134
+ relation: counted(at) === 0 ? "reference-counting" : "reachability",
135
+ testOfDeadCode: testOfDeadCode[at] === true,
136
+ });
137
+ });
138
+
139
+ return {
140
+ liveUnder: live,
141
+ candidates,
142
+ components: componentsOf(graph, dead, testOfDeadCode),
143
+ };
144
+ }
145
+
146
+ /**
147
+ * Per declaration, whether a seed reaches it. The two seeds reach through different
148
+ * reference sets: a marked declaration is live by a mechanism the analysis cannot see, so
149
+ * no mode withholds a reference it makes, while a root reaches through the references the
150
+ * mode counts. The marks run first, so what they reach is expanded under the wider rule.
151
+ */
152
+ function reachable(graph: Graph, marked: readonly boolean[], mode: Mode): readonly boolean[] {
153
+ const reached = graph.symbols.map(() => false);
154
+ const expanded = graph.symbols.map(() => false);
155
+ const queue: number[] = [];
156
+
157
+ // A reference to a file is a reference to the module's namespace, which reaches what
158
+ // the module exports as well as its top level. A root does not expand: a file a
159
+ // runtime runs without reading its exports is a root by itself.
160
+ const enter = (at: number, viaReference: boolean): void => {
161
+ if (!reached[at]) {
162
+ reached[at] = true;
163
+ queue.push(at);
164
+ }
165
+ if (!viaReference || expanded[at] === true) {
166
+ return;
167
+ }
168
+ expanded[at] = true;
169
+ for (const exported of graph.exportsOf[at] ?? []) {
170
+ enter(exported, false);
171
+ }
172
+ };
173
+ const walk = (held: boolean): void => {
174
+ for (let at = queue.pop(); at !== undefined; at = queue.pop()) {
175
+ for (const edge of graph.out[at] ?? []) {
176
+ if (!held && mode.production && edge.test) {
177
+ continue;
178
+ }
179
+ enter(edge.to, true);
180
+ }
181
+ }
182
+ };
183
+
184
+ marked.forEach((mark, at) => {
185
+ if (mark) {
186
+ enter(at, false);
187
+ }
188
+ });
189
+ walk(true);
190
+ for (const root of graph.rooted) {
191
+ enter(root.at, false);
192
+ }
193
+ walk(false);
194
+ return reached;
195
+ }
196
+
197
+ /**
198
+ * Admits every test declaration whose set of referenced production declarations is
199
+ * non-empty and wholly dead, and returns which it admitted. The set is read from every
200
+ * reference the test makes, not the ones the mode counts: a production sweep counts none,
201
+ * which is what makes the targets dead. A test referencing one live target is never admitted.
202
+ */
203
+ function admitTestsOfDeadCode(
204
+ graph: Graph,
205
+ dead: boolean[],
206
+ marked: readonly boolean[],
207
+ ): readonly boolean[] {
208
+ const admitted = graph.symbols.map(() => false);
209
+ graph.symbols.forEach((_symbol, at) => {
210
+ if (graph.test[at] !== true || graph.subject[at] !== true || marked[at] === true) {
211
+ return;
212
+ }
213
+ let targets = 0;
214
+ let live = 0;
215
+ for (const edge of graph.out[at] ?? []) {
216
+ if (graph.test[edge.to] === true || graph.subject[edge.to] !== true) {
217
+ continue;
218
+ }
219
+ targets += 1;
220
+ if (dead[edge.to] !== true) {
221
+ live += 1;
222
+ }
223
+ }
224
+ if (targets > 0 && live === 0) {
225
+ dead[at] = true;
226
+ admitted[at] = true;
227
+ }
228
+ });
229
+ return admitted;
230
+ }
package/src/version.ts CHANGED
@@ -4,4 +4,4 @@
4
4
  * independently of the analyzer's own version, which is the one the host answers
5
5
  * from the package the analyzer was installed from.
6
6
  */
7
- export const CONTRACT_VERSION = "2.1.0";
7
+ export const CONTRACT_VERSION = "2.2.0";