@cplieger/deadset-ts 8.0.0-dev.17 → 8.0.0-dev.19
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/dist/src/analysis.js +30 -72
- package/dist/src/augmentations.js +1 -1
- package/dist/src/calls.js +267 -48
- package/dist/src/commands/held.js +499 -0
- package/dist/src/commands/read.js +16 -10
- package/dist/src/commands/shell.js +109 -15
- package/dist/src/commands/tools.js +1540 -149
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/contract/contract.generated.js +9 -2
- package/dist/src/entries/conventions.js +7 -6
- package/dist/src/entries/test-runners.js +6 -40
- package/dist/src/entries/workflow-steps.js +202 -28
- package/dist/src/exempt.js +0 -26
- package/dist/src/exposure.js +400 -0
- package/dist/src/findings/completion.js +4 -17
- package/dist/src/findings/emitters.js +22 -33
- package/dist/src/findings/intra-function.js +3 -3
- package/dist/src/findings/non-code-artifacts.js +11 -5
- package/dist/src/findings/reads-and-writes.js +71 -15
- package/dist/src/findings/unused-declarations.js +2 -22
- package/dist/src/findings/visibility-narrowing.js +14 -20
- package/dist/src/inventory.js +18 -0
- package/dist/src/{components.js → liveness/components.js} +1 -1
- package/dist/src/{graph.js → liveness/graph.js} +38 -21
- package/dist/src/liveness/matrix.js +99 -0
- package/dist/src/liveness/sweep.js +558 -0
- package/dist/src/query.js +46 -0
- package/dist/src/references/sink.js +87 -1
- package/dist/src/references.js +1 -1
- package/dist/src/roots/manifest.js +47 -4
- package/dist/src/roots/published-reach.js +4 -4
- package/dist/src/roots/root-set.js +14 -16
- package/dist/src/text/named-files.js +257 -34
- package/dist/src/verbs/analyze.js +16 -1
- package/dist/src/verbs/explain.js +73 -152
- package/dist/src/verbs/print-retained.js +1 -1
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +54 -108
- package/src/augmentations.ts +1 -1
- package/src/calls.ts +326 -52
- package/src/commands/held.ts +632 -0
- package/src/commands/read.ts +26 -12
- package/src/commands/shell.ts +117 -15
- package/src/commands/tools.ts +1972 -202
- package/src/conformance-record.ts +1 -1
- package/src/contract/contract.generated.ts +9 -2
- package/src/dependencies.ts +1 -1
- package/src/entries/conventions.ts +8 -12
- package/src/entries/test-runners.ts +12 -45
- package/src/entries/workflow-steps.ts +227 -38
- package/src/exempt.ts +31 -46
- package/src/exposure.ts +492 -0
- package/src/file-facts.ts +5 -0
- package/src/finding.ts +1 -1
- package/src/findings/boundary.ts +7 -0
- package/src/findings/completion.ts +5 -19
- package/src/findings/emitters.ts +23 -41
- package/src/findings/interfaces.ts +3 -3
- package/src/findings/intra-function.ts +3 -3
- package/src/findings/non-code-artifacts.ts +16 -5
- package/src/findings/reads-and-writes.ts +99 -26
- package/src/findings/unused-declarations.ts +4 -28
- package/src/findings/visibility-narrowing.ts +25 -26
- package/src/interface-satisfaction.ts +1 -1
- package/src/inventory.ts +19 -0
- package/src/{components.ts → liveness/components.ts} +3 -3
- package/src/{graph.ts → liveness/graph.ts} +74 -26
- package/src/liveness/matrix.ts +162 -0
- package/src/liveness/sweep.ts +855 -0
- package/src/query.ts +81 -0
- package/src/references/sink.ts +101 -0
- package/src/references.ts +8 -1
- package/src/roots/manifest.ts +53 -8
- package/src/roots/published-reach.ts +9 -4
- package/src/roots/root-set.ts +18 -16
- package/src/stores.ts +2 -1
- package/src/tests/file-class.ts +19 -2
- package/src/text/named-files.ts +333 -36
- package/src/verbs/analyze.ts +16 -1
- package/src/verbs/explain.ts +81 -199
- package/src/verbs/print-retained.ts +1 -1
- package/dist/src/matrix.js +0 -275
- package/dist/src/sweep.js +0 -234
- package/src/matrix.ts +0 -466
- package/src/sweep.ts +0 -384
package/src/verbs/explain.ts
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
import { EXIT_CLEAN, EXIT_USAGE } from "../contract/contract.generated.ts";
|
|
2
2
|
import { productionMode, runAnalysis, type RunAnalysis } from "../analysis.ts";
|
|
3
|
-
import { listing } from "../components.ts";
|
|
3
|
+
import { listing } from "../liveness/components.ts";
|
|
4
4
|
import type { Cascade } from "../config.ts";
|
|
5
5
|
import type { CompletedFinding } from "../finding.ts";
|
|
6
|
-
import {
|
|
6
|
+
import { swept as judged, type Edge, type Graph } from "../liveness/graph.ts";
|
|
7
|
+
import type { Origin } from "../liveness/sweep.ts";
|
|
7
8
|
import type { InventorySymbol } from "../inventory.ts";
|
|
8
|
-
import { referenceKey, type Configured } from "../matrix.ts";
|
|
9
9
|
import { partialNotes } from "../partial.ts";
|
|
10
10
|
import { resolvePath } from "../paths.ts";
|
|
11
11
|
import { positionKey } from "../position.ts";
|
|
12
12
|
import type { Reference } from "../references.ts";
|
|
13
|
-
import
|
|
14
|
-
import { madeByTestCode, withheldBy } from "../tests/file-class.ts";
|
|
13
|
+
import { madeByTestCode } from "../tests/file-class.ts";
|
|
15
14
|
import { resolve } from "../resolve.ts";
|
|
16
|
-
import type { Root } from "../roots/root-set.ts";
|
|
17
15
|
import { type Verb, type VerbOptions } from "./verb.ts";
|
|
18
16
|
import { UsageError } from "../usage.ts";
|
|
19
17
|
import { bytewise } from "../text/compare.ts";
|
|
@@ -106,160 +104,27 @@ function subjectOf(
|
|
|
106
104
|
return { candidates: partial.slice(0, MAX_PARTIAL_MATCHES) };
|
|
107
105
|
}
|
|
108
106
|
|
|
109
|
-
/** Every reference of the run once, a reference several configurations make counted once. */
|
|
110
|
-
function referencesOf(configured: readonly Configured[]): readonly Reference[] {
|
|
111
|
-
const seen = new Set<string>();
|
|
112
|
-
const held: Reference[] = [];
|
|
113
|
-
for (const reference of configured.flatMap((one) => one.references)) {
|
|
114
|
-
const key = referenceKey(reference);
|
|
115
|
-
if (!seen.has(key)) {
|
|
116
|
-
seen.add(key);
|
|
117
|
-
held.push(reference);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
return held;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** Every root of the run once per reason, in the order the projects name them. */
|
|
124
|
-
function rootsOf(configured: readonly Configured[]): readonly Root[] {
|
|
125
|
-
const seen = new Set<string>();
|
|
126
|
-
return configured
|
|
127
|
-
.flatMap((one) => one.roots)
|
|
128
|
-
.filter((root) => {
|
|
129
|
-
const key = [root.id, root.kind, root.source].join("\u0000");
|
|
130
|
-
if (seen.has(key)) {
|
|
131
|
-
return false;
|
|
132
|
-
}
|
|
133
|
-
seen.add(key);
|
|
134
|
-
return true;
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* One step of a path: a reference, or a reference that reads a module and so reaches a
|
|
140
|
-
* declaration the module exports.
|
|
141
|
-
*/
|
|
142
|
-
type Hop =
|
|
143
|
-
| { readonly k: "reference"; readonly reference: Reference }
|
|
144
|
-
| { readonly k: "export"; readonly reference: Reference; readonly to: string };
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Where a path starts: a root of the run, or a reference a loaded consumer or a test file no
|
|
148
|
-
* project holds makes.
|
|
149
|
-
*/
|
|
150
|
-
type Origin =
|
|
151
|
-
| { readonly k: "root"; readonly root: Root }
|
|
152
|
-
| { readonly k: "consumer"; readonly consumer: string }
|
|
153
|
-
| { readonly k: "unheld"; readonly file: string };
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* The shortest path from a root, a loaded consumer or a test file no project holds to one
|
|
157
|
-
* declaration, walked breadth first from every root at once in the order the run names them,
|
|
158
|
-
* a reference of a consumer or of such a file being the first hop of a path of its own, as
|
|
159
|
-
* the sweep seeds them, and following the references the sweep follows: a
|
|
160
|
-
* reference that reads a module reaches what the module exports, and an evaluation or a root
|
|
161
|
-
* reaches the module alone. Under `production` no reference a test file made is followed,
|
|
162
|
-
* which is the set the report's sweep counts.
|
|
163
|
-
*/
|
|
164
|
-
function shortestPath(
|
|
165
|
-
analysis: RunAnalysis,
|
|
166
|
-
to: string,
|
|
167
|
-
mode: Mode,
|
|
168
|
-
support: ReadonlySet<string>,
|
|
169
|
-
): { readonly origin: Origin; readonly path: readonly Hop[] } | undefined {
|
|
170
|
-
const graph = analysis.swept.matrix.union;
|
|
171
|
-
const out = new Map<string, Reference[]>();
|
|
172
|
-
const consumed: { readonly reference: Reference; readonly origin: Origin }[] = [];
|
|
173
|
-
for (const reference of referencesOf(analysis.configured)) {
|
|
174
|
-
if (withheldBy(mode, reference, support)) {
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
const { consumer } = reference;
|
|
178
|
-
if (consumer !== undefined) {
|
|
179
|
-
consumed.push({ reference, origin: { k: "consumer", consumer } });
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
182
|
-
if (reference.unheld === true) {
|
|
183
|
-
consumed.push({
|
|
184
|
-
reference,
|
|
185
|
-
origin: { k: "unheld", file: reference.from.replace(/#$/u, "") },
|
|
186
|
-
});
|
|
187
|
-
continue;
|
|
188
|
-
}
|
|
189
|
-
const held = out.get(reference.from);
|
|
190
|
-
if (held === undefined) {
|
|
191
|
-
out.set(reference.from, [reference]);
|
|
192
|
-
} else {
|
|
193
|
-
held.push(reference);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
interface Step {
|
|
197
|
-
readonly at: string;
|
|
198
|
-
readonly hop: Hop | undefined;
|
|
199
|
-
readonly previous: number;
|
|
200
|
-
readonly origin: Origin;
|
|
201
|
-
}
|
|
202
|
-
const steps: Step[] = [];
|
|
203
|
-
const seen = new Set<string>();
|
|
204
|
-
const expanded = new Set<string>();
|
|
205
|
-
const enter = (step: Step): void => {
|
|
206
|
-
if (!seen.has(step.at)) {
|
|
207
|
-
seen.add(step.at);
|
|
208
|
-
steps.push(step);
|
|
209
|
-
}
|
|
210
|
-
};
|
|
211
|
-
const follow = (reference: Reference, previous: number, origin: Origin): void => {
|
|
212
|
-
enter({ at: reference.to, hop: { k: "reference", reference }, previous, origin });
|
|
213
|
-
const target = graph.at(reference.to);
|
|
214
|
-
if (reference.use === "evaluation" || target === OUTSIDE || expanded.has(reference.to)) {
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
expanded.add(reference.to);
|
|
218
|
-
for (const exported of graph.exportsOf[target] ?? []) {
|
|
219
|
-
const id = graph.symbols[exported]?.id;
|
|
220
|
-
if (id !== undefined) {
|
|
221
|
-
enter({ at: id, hop: { k: "export", reference, to: id }, previous, origin });
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
};
|
|
225
|
-
for (const root of rootsOf(analysis.configured)) {
|
|
226
|
-
enter({ at: root.id, hop: undefined, previous: -1, origin: { k: "root", root } });
|
|
227
|
-
}
|
|
228
|
-
// A consumed reference is one hop long, so it joins the walk where the roots' first hops do.
|
|
229
|
-
const firstHops = steps.length;
|
|
230
|
-
for (let head = 0; ; head += 1) {
|
|
231
|
-
if (head === firstHops) {
|
|
232
|
-
for (const { reference, origin } of consumed) {
|
|
233
|
-
follow(reference, -1, origin);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
const step = steps[head];
|
|
237
|
-
if (step === undefined) {
|
|
238
|
-
break;
|
|
239
|
-
}
|
|
240
|
-
if (step.at === to) {
|
|
241
|
-
const path: Hop[] = [];
|
|
242
|
-
for (let at: Step | undefined = step; at?.hop !== undefined; at = steps[at.previous]) {
|
|
243
|
-
path.unshift(at.hop);
|
|
244
|
-
}
|
|
245
|
-
return { origin: step.origin, path };
|
|
246
|
-
}
|
|
247
|
-
for (const reference of out.get(step.at) ?? []) {
|
|
248
|
-
follow(reference, head, step.origin);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
return undefined;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
107
|
/** Where a path starts, as a line names it. */
|
|
255
|
-
function originText(origin: Origin): string {
|
|
108
|
+
function originText(origin: Origin, graph: Graph): string {
|
|
109
|
+
const id = (at: number): string => graph.symbols[at]?.id ?? "";
|
|
256
110
|
switch (origin.k) {
|
|
257
111
|
case "root":
|
|
258
|
-
return `root ${origin.
|
|
259
|
-
case "
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
112
|
+
return `root ${origin.rooted.kind} ${id(origin.rooted.at)}`;
|
|
113
|
+
case "consumed": {
|
|
114
|
+
const reference = graph.references[origin.edge.reference];
|
|
115
|
+
if (reference?.consumer !== undefined) {
|
|
116
|
+
return `consumer ${reference.consumer}`;
|
|
117
|
+
}
|
|
118
|
+
return reference?.unheld === true
|
|
119
|
+
? `test file no project holds ${reference.from.replace(/#$/u, "")}`
|
|
120
|
+
: `declaration no configuration judges ${reference?.from ?? ""}`;
|
|
121
|
+
}
|
|
122
|
+
case "exemption":
|
|
123
|
+
return `exemption ${origin.record.class} ${positionKey(origin.record.site)}`;
|
|
124
|
+
case "mark":
|
|
125
|
+
return `suppression of ${id(origin.at)}`;
|
|
126
|
+
case "unanswered":
|
|
127
|
+
return `unanswered question about ${id(origin.at)}`;
|
|
263
128
|
}
|
|
264
129
|
}
|
|
265
130
|
|
|
@@ -268,52 +133,74 @@ function madeBy(reference: Reference): string {
|
|
|
268
133
|
return reference.consumer === undefined ? reference.from : `consumer ${reference.consumer}`;
|
|
269
134
|
}
|
|
270
135
|
|
|
271
|
-
/**
|
|
272
|
-
|
|
273
|
-
|
|
136
|
+
/**
|
|
137
|
+
* One reference of an answer, marked where test code made it, a test-support file's included,
|
|
138
|
+
* and naming the configurations that make it where they are not every one of the run.
|
|
139
|
+
*/
|
|
140
|
+
function referenceLine(
|
|
141
|
+
reference: Reference,
|
|
142
|
+
tail: string,
|
|
143
|
+
support: ReadonlySet<string>,
|
|
144
|
+
configurations: { readonly making: readonly string[]; readonly run: readonly string[] },
|
|
145
|
+
): string {
|
|
146
|
+
const partial = configurations.making.length < configurations.run.length;
|
|
147
|
+
return ` reference: ${positionKey(reference.position)}\t${reference.use}\t${tail}${madeByTestCode(reference, support) ? "\tfrom a test file" : ""}${partial ? `\tmade in ${configurations.making.join(" ")}` : ""}\n`;
|
|
274
148
|
}
|
|
275
149
|
|
|
276
150
|
/** Why a declaration no finding names and no exemption held back is live, or how it fell. */
|
|
277
|
-
function liveness(
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
cascade: Cascade,
|
|
281
|
-
mode: Mode,
|
|
282
|
-
): string {
|
|
283
|
-
const { sweep } = analysis.swept;
|
|
151
|
+
function liveness(analysis: RunAnalysis, subject: InventorySymbol, cascade: Cascade): string {
|
|
152
|
+
const { matrix, sweep } = analysis.swept;
|
|
153
|
+
const graph = matrix.union;
|
|
284
154
|
const candidate = sweep.candidates.find((one) => one.id === subject.id);
|
|
285
155
|
if (candidate === undefined && !judged(subject.kind)) {
|
|
286
156
|
return `answer: unjudged\n unjudged: neither liveness relation judges a ${subject.kind}\n`;
|
|
287
157
|
}
|
|
288
158
|
let text = `answer: ${candidate === undefined ? "live" : "dead"}\n`;
|
|
289
159
|
text += ` live under: ${listed(sweep.liveUnder.get(subject.id) ?? [])}\n`;
|
|
290
|
-
|
|
291
|
-
|
|
160
|
+
const at = graph.at(subject.id);
|
|
161
|
+
const seen = new Set<string>();
|
|
162
|
+
for (const root of graph.rooted.filter((one) => one.at === at)) {
|
|
163
|
+
const key = `${root.kind}\u0000${root.source}`;
|
|
164
|
+
if (!seen.has(key)) {
|
|
165
|
+
seen.add(key);
|
|
166
|
+
text += ` root: ${root.kind}${root.source === "" ? "" : `\tnamed by ${root.source}`}\n`;
|
|
167
|
+
}
|
|
292
168
|
}
|
|
293
169
|
const support = new Set(analysis.configured.flatMap((one) => one.supportFiles ?? []));
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
170
|
+
const line = (edge: Edge, tail: (reference: Reference) => string): string => {
|
|
171
|
+
const reference = graph.references[edge.reference];
|
|
172
|
+
return reference === undefined
|
|
173
|
+
? ""
|
|
174
|
+
: referenceLine(reference, tail(reference), support, {
|
|
175
|
+
making: graph.configurations[edge.reference] ?? [],
|
|
176
|
+
run: matrix.configurations,
|
|
177
|
+
});
|
|
178
|
+
};
|
|
179
|
+
const found = sweep.path(subject.id);
|
|
180
|
+
if (found !== undefined && found.hops.length > 0) {
|
|
181
|
+
text += ` reached from: ${originText(found.origin, graph)}\n`;
|
|
182
|
+
for (const hop of found.hops) {
|
|
183
|
+
text += line(hop.edge, (reference) => `${madeBy(reference)} -> ${reference.to}`);
|
|
306
184
|
if (hop.k === "export") {
|
|
307
|
-
|
|
185
|
+
const reference = graph.references[hop.edge.reference];
|
|
186
|
+
text += ` export: ${reference?.to ?? ""} -> ${graph.symbols[hop.to]?.id ?? ""}\n`;
|
|
308
187
|
}
|
|
309
188
|
}
|
|
310
189
|
} else if (found === undefined) {
|
|
311
|
-
const references =
|
|
190
|
+
const references = graph.references.flatMap((one, place) =>
|
|
191
|
+
one.to === subject.id ? [place] : [],
|
|
192
|
+
);
|
|
312
193
|
text +=
|
|
313
194
|
" unreachable: no path of references reaches it from a root, a loaded consumer or a test file no project holds\n";
|
|
314
195
|
text += ` references: ${String(references.length)}\n`;
|
|
315
|
-
for (const
|
|
316
|
-
|
|
196
|
+
for (const place of references) {
|
|
197
|
+
const reference = graph.references[place];
|
|
198
|
+
if (reference !== undefined) {
|
|
199
|
+
text += referenceLine(reference, `from ${madeBy(reference)}`, support, {
|
|
200
|
+
making: graph.configurations[place] ?? [],
|
|
201
|
+
run: matrix.configurations,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
317
204
|
}
|
|
318
205
|
}
|
|
319
206
|
if (candidate !== undefined) {
|
|
@@ -371,12 +258,7 @@ function namedElsewhere(findings: readonly CompletedFinding[], subject: Inventor
|
|
|
371
258
|
}
|
|
372
259
|
|
|
373
260
|
/** The one explanation that applies to a declaration, built whole before it is written. */
|
|
374
|
-
function explanation(
|
|
375
|
-
analysis: RunAnalysis,
|
|
376
|
-
subject: InventorySymbol,
|
|
377
|
-
cascade: Cascade,
|
|
378
|
-
mode: Mode,
|
|
379
|
-
): string {
|
|
261
|
+
function explanation(analysis: RunAnalysis, subject: InventorySymbol, cascade: Cascade): string {
|
|
380
262
|
const graph = analysis.swept.matrix.union;
|
|
381
263
|
const at = graph.at(subject.id);
|
|
382
264
|
let text = `symbol: ${subject.ref}\n`;
|
|
@@ -384,7 +266,7 @@ function explanation(
|
|
|
384
266
|
text += `position: ${subject.id}\n`;
|
|
385
267
|
text += `configurations: ${listed(analysis.swept.matrix.heldIn[at] ?? [])}\n`;
|
|
386
268
|
const found = analysis.result.findings.find((one) => positionKey(one.position) === subject.id);
|
|
387
|
-
const retained = analysis.swept.retained.filter((one) => one.id === subject.id);
|
|
269
|
+
const retained = analysis.swept.sweep.retained.filter((one) => one.id === subject.id);
|
|
388
270
|
if (found !== undefined) {
|
|
389
271
|
text += reported(found);
|
|
390
272
|
} else if (retained.length > 0) {
|
|
@@ -392,11 +274,11 @@ function explanation(
|
|
|
392
274
|
for (const one of retained) {
|
|
393
275
|
text += ` class: ${one.class}\t${positionKey(one.site)}\t${one.detail}\n`;
|
|
394
276
|
}
|
|
395
|
-
} else if (analysis.swept.
|
|
277
|
+
} else if (analysis.swept.sweep.held.includes(subject.id)) {
|
|
396
278
|
text +=
|
|
397
279
|
"answer: held\n held by: a question the checker did not answer, which could have kept it live, so no finding names it\n";
|
|
398
280
|
} else {
|
|
399
|
-
text += liveness(analysis, subject, cascade
|
|
281
|
+
text += liveness(analysis, subject, cascade);
|
|
400
282
|
}
|
|
401
283
|
return text + namedElsewhere(analysis.result.findings, subject);
|
|
402
284
|
}
|
|
@@ -404,10 +286,10 @@ function explanation(
|
|
|
404
286
|
/**
|
|
405
287
|
* Explains one symbol from the analysis the report is built from: why a reported symbol is
|
|
406
288
|
* reported, which exemption classes held a retained one back, that an unanswered question held
|
|
407
|
-
* a held one, and for any other declaration the relations that hold it live and the
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
289
|
+
* a held one, and for any other declaration the relations that hold it live and the path the
|
|
290
|
+
* sweep's strongest tier reaching it follows, or the references it has where no path reaches
|
|
291
|
+
* it. A request naming no one declaration exits with the usage code and names what partially
|
|
292
|
+
* matches it. Where the analysis was partial, the error stream says so.
|
|
411
293
|
*/
|
|
412
294
|
export const explainVerb: Verb = ({ out, err, host, inputs, scope, openClient, option }) => {
|
|
413
295
|
const named = namedSymbol(option);
|
|
@@ -430,7 +312,7 @@ export const explainVerb: Verb = ({ out, err, host, inputs, scope, openClient, o
|
|
|
430
312
|
err.write(" no symbol of the target partially matches it either\n");
|
|
431
313
|
}
|
|
432
314
|
} else {
|
|
433
|
-
out.write(explanation(analysis, subject, config.reporters.cascade
|
|
315
|
+
out.write(explanation(analysis, subject, config.reporters.cascade));
|
|
434
316
|
}
|
|
435
317
|
for (const note of notes) {
|
|
436
318
|
diagnose(err, note);
|
|
@@ -21,7 +21,7 @@ export const printRetainedVerb: Verb = ({ out, err, host, inputs, scope, openCli
|
|
|
21
21
|
marked: [],
|
|
22
22
|
mode: productionMode(config),
|
|
23
23
|
});
|
|
24
|
-
for (const line of retainedLines(swept.matrix.union.symbols, swept.retained)) {
|
|
24
|
+
for (const line of retainedLines(swept.matrix.union.symbols, swept.sweep.retained)) {
|
|
25
25
|
out.write(`${line}\n`);
|
|
26
26
|
}
|
|
27
27
|
for (const note of partialNotes({
|
package/dist/src/matrix.js
DELETED
|
@@ -1,275 +0,0 @@
|
|
|
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, OUTSIDE } from "./graph.js";
|
|
8
|
-
import { byPosition } from "./position.js";
|
|
9
|
-
import { edgeWithheldBy } from "./tests/file-class.js";
|
|
10
|
-
import { reachedWithTheTests, splitRefs, sweep, } from "./sweep.js";
|
|
11
|
-
/**
|
|
12
|
-
* What makes one reference one reference, whichever configuration made it. A consumer's
|
|
13
|
-
* position is rendered against its own root, so the consumer is part of what it names.
|
|
14
|
-
*/
|
|
15
|
-
export function referenceKey(reference) {
|
|
16
|
-
const { path, line, column } = reference.position;
|
|
17
|
-
return [
|
|
18
|
-
reference.consumer ?? "",
|
|
19
|
-
reference.from,
|
|
20
|
-
reference.to,
|
|
21
|
-
path,
|
|
22
|
-
String(line),
|
|
23
|
-
String(column),
|
|
24
|
-
reference.use,
|
|
25
|
-
].join("\u0000");
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Indexes what the passes answered for each configuration of one run, in the run's order.
|
|
29
|
-
* A declaration is keyed by its rendered position, with the fields of the first
|
|
30
|
-
* configuration that judges it; one in a file every holder holds only for its references
|
|
31
|
-
* is judged by none and is not in the union. One reference at one position of a file
|
|
32
|
-
* several configurations compile is one reference; two one configuration makes are two.
|
|
33
|
-
*/
|
|
34
|
-
export function matrixOf(per) {
|
|
35
|
-
const symbols = [];
|
|
36
|
-
const holders = new Map();
|
|
37
|
-
const references = [];
|
|
38
|
-
const firstSeen = new Map();
|
|
39
|
-
const judged = new Set();
|
|
40
|
-
const dependents = new Map();
|
|
41
|
-
per.forEach((one, index) => {
|
|
42
|
-
for (const [id, after] of one.dependents ?? []) {
|
|
43
|
-
if (!dependents.has(id)) {
|
|
44
|
-
dependents.set(id, after);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
const referenceOnly = new Set(one.referenceOnly);
|
|
48
|
-
for (const symbol of one.symbols) {
|
|
49
|
-
const held = holders.get(symbol.id);
|
|
50
|
-
if (held === undefined) {
|
|
51
|
-
holders.set(symbol.id, [one.configuration]);
|
|
52
|
-
}
|
|
53
|
-
else if (!held.includes(one.configuration)) {
|
|
54
|
-
held.push(one.configuration);
|
|
55
|
-
}
|
|
56
|
-
if (!referenceOnly.has(symbol.position.path) && !judged.has(symbol.id)) {
|
|
57
|
-
judged.add(symbol.id);
|
|
58
|
-
symbols.push(symbol);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
for (const reference of one.references) {
|
|
62
|
-
const key = referenceKey(reference);
|
|
63
|
-
const seen = firstSeen.get(key);
|
|
64
|
-
if (seen === undefined) {
|
|
65
|
-
firstSeen.set(key, index);
|
|
66
|
-
}
|
|
67
|
-
else if (seen !== index) {
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
references.push(reference);
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
symbols.sort((a, b) => byPosition(a.position, b.position));
|
|
74
|
-
const testUsed = new Set(per.flatMap((one) => one.testUsedFiles ?? []));
|
|
75
|
-
return {
|
|
76
|
-
configurations: per.map((one) => one.configuration),
|
|
77
|
-
graphs: per.map((one) => graphOf(one.symbols, one.references, one.roots, one.testFiles, one.supportFiles, one.ambient)),
|
|
78
|
-
union: graphOf(symbols, references, per.flatMap((one) => one.roots), [...new Set(per.flatMap((one) => one.testFiles))], [...new Set(per.flatMap((one) => one.supportFiles ?? []))], new Set(per.flatMap((one) => [...(one.ambient ?? [])]))),
|
|
79
|
-
heldIn: symbols.map((symbol) => holders.get(symbol.id) ?? []),
|
|
80
|
-
testUsed: symbols.map((symbol) => testUsed.has(symbol.position.path)),
|
|
81
|
-
dependents,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Which declarations of the run are dead in every configuration that holds them. Each
|
|
86
|
-
* configuration is swept on its own graph, since a file it does not compile references nothing
|
|
87
|
-
* there and a root it does not hold seeds nothing. A declaration is a candidate when every
|
|
88
|
-
* configuration holding it judged it one; its relation is reference counting where every one
|
|
89
|
-
* found it so, else reachability, and its counts are the union's. Components are computed once,
|
|
90
|
-
* over the union and the intersected dead set, so one dead component has one identifier.
|
|
91
|
-
*/
|
|
92
|
-
export function sweepMatrix(matrix, input) {
|
|
93
|
-
const per = matrix.graphs.map((graph) => sweep(graph, input));
|
|
94
|
-
const byConfiguration = new Map(matrix.configurations.map((configuration, index) => [
|
|
95
|
-
configuration,
|
|
96
|
-
new Map((per[index]?.candidates ?? []).map((candidate) => [candidate.id, candidate])),
|
|
97
|
-
]));
|
|
98
|
-
const union = matrix.union;
|
|
99
|
-
const heldBy = union.symbols.map(() => undefined);
|
|
100
|
-
const held = [
|
|
101
|
-
...input.marked,
|
|
102
|
-
...(input.exempt ?? []).map((record) => record.id),
|
|
103
|
-
...(input.unanswered ?? []),
|
|
104
|
-
];
|
|
105
|
-
const testReached = reachedWithTheTests(union, held);
|
|
106
|
-
// A test uses what it names: a file a test reads through a star re-export or a
|
|
107
|
-
// namespace keeps the exports nothing names at their own class.
|
|
108
|
-
const testNamed = reachedWithTheTests(union, held, false);
|
|
109
|
-
const everywhere = union.symbols.map((symbol, at) => {
|
|
110
|
-
const held = matrix.heldIn[at] ?? [];
|
|
111
|
-
const found = held.map((configuration) => byConfiguration.get(configuration)?.get(symbol.id));
|
|
112
|
-
return held.length === 0 || found.some((candidate) => candidate === undefined)
|
|
113
|
-
? undefined
|
|
114
|
-
: found.filter((candidate) => candidate !== undefined);
|
|
115
|
-
});
|
|
116
|
-
const compiled = compiledReferences(matrix, new Set(union.symbols.flatMap((symbol, at) => (everywhere[at] === undefined ? [] : [symbol.id]))), input.mode);
|
|
117
|
-
const judged = everywhere.map((found, at) => compiled.has(union.symbols[at]?.id ?? "") ? undefined : found);
|
|
118
|
-
// A configuration that does not hold a test's subject's caller finds the subject dead,
|
|
119
|
-
// so a test is of dead code only where every production subject it names is dead in
|
|
120
|
-
// every configuration that holds it.
|
|
121
|
-
const subjectsDead = (at) => (union.out[at] ?? []).every((edge) => union.test[edge.to] === true ||
|
|
122
|
-
union.support[edge.to] === true ||
|
|
123
|
-
union.ambient[edge.to] === true ||
|
|
124
|
-
union.subject[edge.to] !== true ||
|
|
125
|
-
judged[edge.to] !== undefined);
|
|
126
|
-
const admittedAt = (at) => judged[at]?.every((candidate) => candidate.testOfDeadCode) === true && subjectsDead(at);
|
|
127
|
-
const kept = keptByTests(union, testReached, admittedAt);
|
|
128
|
-
const testUsed = testUsedReach(union, union.symbols.map((_symbol, at) => matrix.testUsed[at] === true && testNamed[at] === true), judged.map((one) => one !== undefined));
|
|
129
|
-
const candidates = [];
|
|
130
|
-
union.symbols.forEach((symbol, at) => {
|
|
131
|
-
const all = judged[at];
|
|
132
|
-
if (all === undefined) {
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
const held = matrix.heldIn[at] ?? [];
|
|
136
|
-
const admitted = admittedAt(at);
|
|
137
|
-
const split = splitRefs(union.made[at], input.mode);
|
|
138
|
-
const testRefs = split.test;
|
|
139
|
-
const heldByTests = heldBy[union.parent[at] ?? OUTSIDE] ??
|
|
140
|
-
(union.support[at] === true && testRefs > 0 && !admitted ? symbol.id : undefined);
|
|
141
|
-
const unreferenced = union.test[at] === true && !admitted && testReached[at] !== true;
|
|
142
|
-
heldBy[at] = heldByTests;
|
|
143
|
-
const productionRefs = split.production;
|
|
144
|
-
const counted = all.every((candidate) => candidate.relation === "reference-counting");
|
|
145
|
-
candidates.push({
|
|
146
|
-
id: symbol.id,
|
|
147
|
-
productionRefs,
|
|
148
|
-
testRefs,
|
|
149
|
-
// The run that judges a test-file declaration counts its test references.
|
|
150
|
-
relation: (unreferenced ? productionRefs + testRefs === 0 : counted)
|
|
151
|
-
? "reference-counting"
|
|
152
|
-
: "reachability",
|
|
153
|
-
testOfDeadCode: admitted,
|
|
154
|
-
configurations: held,
|
|
155
|
-
heldByTests,
|
|
156
|
-
unreferencedTest: unreferenced,
|
|
157
|
-
testUsed: testUsed[at] === true,
|
|
158
|
-
keptByTests: kept[at] === true,
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
const liveUnder = liveAnywhere(union.symbols, per);
|
|
162
|
-
return {
|
|
163
|
-
liveUnder,
|
|
164
|
-
candidates,
|
|
165
|
-
components: deadComponents(matrix, { liveUnder, candidates }, new Set()),
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
/**
|
|
169
|
-
* Every declaration of `dead`, those dead in every configuration that holds them, that a
|
|
170
|
-
* configuration's graph references from a judged declaration outside `dead` or already
|
|
171
|
-
* kept, until none joins: deleting one would break that configuration's compile. A
|
|
172
|
-
* production run counts no reference a test file made.
|
|
173
|
-
*/
|
|
174
|
-
function compiledReferences(matrix, dead, mode) {
|
|
175
|
-
const kept = new Set();
|
|
176
|
-
for (let changed = true; changed;) {
|
|
177
|
-
changed = false;
|
|
178
|
-
for (const graph of matrix.graphs) {
|
|
179
|
-
graph.out.forEach((edges, from) => {
|
|
180
|
-
const id = graph.symbols[from]?.id ?? "";
|
|
181
|
-
if (graph.subject[from] !== true || (dead.has(id) && !kept.has(id))) {
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
for (const edge of edges) {
|
|
185
|
-
const to = graph.symbols[edge.to]?.id ?? "";
|
|
186
|
-
if (!edgeWithheldBy(mode, edge) && dead.has(to) && !kept.has(to)) {
|
|
187
|
-
kept.add(to);
|
|
188
|
-
changed = true;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
return kept;
|
|
195
|
-
}
|
|
196
|
-
/** Per declaration of the union, whether one of `seeds` is it or reaches it through `dead` declarations. */
|
|
197
|
-
function testUsedReach(union, seeds, dead) {
|
|
198
|
-
// A file is no candidate, and its top level is what reaches what it alone calls.
|
|
199
|
-
const reached = seeds.map((seed, at) => seed && dead[at] === true);
|
|
200
|
-
const queue = seeds.flatMap((one, at) => (one ? [at] : []));
|
|
201
|
-
for (let at = queue.pop(); at !== undefined; at = queue.pop()) {
|
|
202
|
-
for (const edge of union.out[at] ?? []) {
|
|
203
|
-
if (dead[edge.to] === true && reached[edge.to] !== true) {
|
|
204
|
-
reached[edge.to] = true;
|
|
205
|
-
queue.push(edge.to);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
return reached;
|
|
210
|
-
}
|
|
211
|
-
/** Per declaration of the union, whether test code `reached` holds and `admitted` rejects references it. */
|
|
212
|
-
function keptByTests(union, reached, admitted) {
|
|
213
|
-
const kept = union.symbols.map(() => false);
|
|
214
|
-
union.out.forEach((edges, from) => {
|
|
215
|
-
const testCode = union.test[from] === true || union.support[from] === true;
|
|
216
|
-
if (!testCode || reached[from] !== true || admitted(from)) {
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
for (const edge of edges) {
|
|
220
|
-
if (edge.test) {
|
|
221
|
-
kept[edge.to] = true;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
});
|
|
225
|
-
return kept;
|
|
226
|
-
}
|
|
227
|
-
/**
|
|
228
|
-
* The dead components of one sweep over the union, with the file declarations at `files`
|
|
229
|
-
* dead as well: a file that is deleted whole takes the declarations it holds and every
|
|
230
|
-
* dead declaration only its top level references with it.
|
|
231
|
-
*/
|
|
232
|
-
export function deadComponents(matrix, result, files) {
|
|
233
|
-
const union = matrix.union;
|
|
234
|
-
const byId = new Map(result.candidates.map((candidate) => [candidate.id, candidate]));
|
|
235
|
-
const candidateAt = (at) => byId.get(union.symbols[at]?.id ?? "");
|
|
236
|
-
return componentsOf(union, union.symbols.map((_symbol, at) => {
|
|
237
|
-
const candidate = candidateAt(at);
|
|
238
|
-
return files.has(at) || (candidate !== undefined && candidate.heldByTests === undefined);
|
|
239
|
-
}), union.symbols.map((_symbol, at) => candidateAt(at)?.testOfDeadCode === true), union.symbols.map((_symbol, at) => candidateAt(at)?.unreferencedTest === true), positionsOf(union, matrix.dependents, result.liveUnder));
|
|
240
|
-
}
|
|
241
|
-
/**
|
|
242
|
-
* The file declarations at `paths`, below the target root, that one sweep holds live
|
|
243
|
-
* under both relations, the rule a judged declaration is live by.
|
|
244
|
-
*/
|
|
245
|
-
export function liveFilesAt(matrix, result, paths) {
|
|
246
|
-
return matrix.union.symbols.filter((symbol) => symbol.kind === "file" &&
|
|
247
|
-
paths.has(symbol.position.path) &&
|
|
248
|
-
(result.liveUnder.get(symbol.id)?.length ?? 0) === RELATIONS.length);
|
|
249
|
-
}
|
|
250
|
-
/**
|
|
251
|
-
* Each declaration's dependents by its place in the union, and per place whether the
|
|
252
|
-
* declaration is live under reachability in some configuration.
|
|
253
|
-
*/
|
|
254
|
-
function positionsOf(union, dependents, liveUnder) {
|
|
255
|
-
return {
|
|
256
|
-
dependents: union.symbols.map((symbol) => (dependents.get(symbol.id) ?? []).map((id) => union.at(id)).filter((at) => at !== OUTSIDE)),
|
|
257
|
-
live: union.symbols.map((symbol) => liveUnder.get(symbol.id)?.includes("reachability") === true),
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
/** The relations in the order a declaration's entry lists them. */
|
|
261
|
-
const RELATIONS = ["reference-counting", "reachability"];
|
|
262
|
-
/**
|
|
263
|
-
* Per declaration, in site order, every relation that holds it live in at least one
|
|
264
|
-
* configuration.
|
|
265
|
-
*/
|
|
266
|
-
function liveAnywhere(symbols, per) {
|
|
267
|
-
const live = new Map();
|
|
268
|
-
for (const symbol of symbols) {
|
|
269
|
-
const relations = RELATIONS.filter((relation) => per.some((liveness) => (liveness.liveUnder.get(symbol.id) ?? []).includes(relation)));
|
|
270
|
-
if (relations.length > 0) {
|
|
271
|
-
live.set(symbol.id, relations);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
return live;
|
|
275
|
-
}
|