@cplieger/deadset-ts 8.0.0-dev.17 → 8.0.0-dev.18
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 +26 -71
- package/dist/src/augmentations.js +1 -1
- package/dist/src/calls.js +267 -48
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/contract/contract.generated.js +2 -2
- 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/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/published-reach.js +4 -4
- 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 +41 -107
- package/src/augmentations.ts +1 -1
- package/src/calls.ts +326 -52
- package/src/conformance-record.ts +1 -1
- package/src/contract/contract.generated.ts +2 -2
- package/src/exempt.ts +31 -46
- package/src/exposure.ts +492 -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/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/published-reach.ts +9 -4
- package/src/stores.ts +2 -1
- package/src/tests/file-class.ts +19 -2
- 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
|
@@ -13,12 +13,15 @@ export class Sink {
|
|
|
13
13
|
#testFile;
|
|
14
14
|
#consumer;
|
|
15
15
|
#found = [];
|
|
16
|
+
#declaredAt;
|
|
16
17
|
#valued = new Map();
|
|
18
|
+
#named = new Map();
|
|
17
19
|
#held = new Set();
|
|
18
|
-
constructor(root, testFile, consumer) {
|
|
20
|
+
constructor(root, testFile, consumer, declaredAt) {
|
|
19
21
|
this.#root = root;
|
|
20
22
|
this.#testFile = testFile;
|
|
21
23
|
this.#consumer = consumer;
|
|
24
|
+
this.#declaredAt = declaredAt;
|
|
22
25
|
}
|
|
23
26
|
/** Whether a test file wrote one walked file's uses. */
|
|
24
27
|
isTest(file) {
|
|
@@ -47,6 +50,11 @@ export class Sink {
|
|
|
47
50
|
test,
|
|
48
51
|
...(target.guessed === true ? { guessed: true } : {}),
|
|
49
52
|
});
|
|
53
|
+
if (this.#consumer === undefined && !("column" in at)) {
|
|
54
|
+
const named = this.#named.get(file) ?? [];
|
|
55
|
+
named.push({ index: this.#found.length - 1, at, annotated: this.#annotates(at, from) });
|
|
56
|
+
this.#named.set(file, named);
|
|
57
|
+
}
|
|
50
58
|
if (use === "write" && value !== undefined) {
|
|
51
59
|
const valued = this.#valued.get(file) ?? [];
|
|
52
60
|
valued.push({ index: this.#found.length - 1, value });
|
|
@@ -54,6 +62,26 @@ export class Sink {
|
|
|
54
62
|
}
|
|
55
63
|
}
|
|
56
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Whether a name sits in the annotation of `from`'s own declaration. An annotation holds
|
|
67
|
+
* no block, so the walk stops at the first declaration or block above the name.
|
|
68
|
+
*/
|
|
69
|
+
#annotates(name, from) {
|
|
70
|
+
if (name.kind === SyntaxKind.SourceFile) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
let child = name;
|
|
74
|
+
let parent = name.parent;
|
|
75
|
+
while (parent.kind !== SyntaxKind.Block && parent.kind !== SyntaxKind.SourceFile) {
|
|
76
|
+
const id = this.#declaredAt(parent);
|
|
77
|
+
if (id !== undefined) {
|
|
78
|
+
return id === from && parent.type === child;
|
|
79
|
+
}
|
|
80
|
+
child = parent;
|
|
81
|
+
parent = parent.parent;
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
57
85
|
/** Holds declarations an unanswered question could have made live, and the one whose read asked it. */
|
|
58
86
|
hold(ids) {
|
|
59
87
|
for (const id of ids) {
|
|
@@ -83,6 +111,14 @@ export class Sink {
|
|
|
83
111
|
}
|
|
84
112
|
}
|
|
85
113
|
}
|
|
114
|
+
for (const [file, named] of this.#named) {
|
|
115
|
+
for (const { index, carriers } of carriersIn(named, this.#valued.get(file) ?? [], found)) {
|
|
116
|
+
const reference = found[index];
|
|
117
|
+
if (reference !== undefined) {
|
|
118
|
+
found[index] = { ...reference, carriers };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
86
122
|
found.sort((a, b) => byPosition(a.position, b.position) ||
|
|
87
123
|
bytewise(a.to, b.to) ||
|
|
88
124
|
bytewise(a.from, b.from) ||
|
|
@@ -149,3 +185,53 @@ function aloneIn(file, uses, writes, found) {
|
|
|
149
185
|
}
|
|
150
186
|
return result;
|
|
151
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* The carriers of each named record of one file: `from` where its own annotation holds the
|
|
190
|
+
* name, then the target of each write whose stored value holds it and that names another
|
|
191
|
+
* declaration, the innermost value first. Values nest or are disjoint.
|
|
192
|
+
*/
|
|
193
|
+
function carriersIn(named, writes, found) {
|
|
194
|
+
const byStart = [...named].sort((a, b) => a.at.pos - b.at.pos);
|
|
195
|
+
const holding = new Map();
|
|
196
|
+
for (const write of writes) {
|
|
197
|
+
let low = 0;
|
|
198
|
+
let high = byStart.length;
|
|
199
|
+
while (low < high) {
|
|
200
|
+
const middle = (low + high) >>> 1;
|
|
201
|
+
if ((byStart[middle]?.at.pos ?? 0) < write.value.pos) {
|
|
202
|
+
low = middle + 1;
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
high = middle;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
for (let at = low; at < byStart.length; at += 1) {
|
|
209
|
+
const one = byStart[at];
|
|
210
|
+
if (one === undefined || one.at.pos >= write.value.end) {
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
if (one.at.end <= write.value.end) {
|
|
214
|
+
holding.set(one.index, [...(holding.get(one.index) ?? []), write]);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const result = [];
|
|
219
|
+
for (const one of named) {
|
|
220
|
+
const reference = found[one.index];
|
|
221
|
+
if (reference === undefined) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const carriers = one.annotated ? [reference.from] : [];
|
|
225
|
+
const values = [...(holding.get(one.index) ?? [])].sort((a, b) => a.value.end - a.value.pos - (b.value.end - b.value.pos));
|
|
226
|
+
for (const write of values) {
|
|
227
|
+
const to = found[write.index]?.to;
|
|
228
|
+
if (to !== undefined && to !== reference.to && !carriers.includes(to)) {
|
|
229
|
+
carriers.push(to);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (carriers.length > 0) {
|
|
233
|
+
result.push({ index: one.index, carriers });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return result;
|
|
237
|
+
}
|
package/dist/src/references.js
CHANGED
|
@@ -49,10 +49,10 @@ export function references(project, held, targetRoot, options) {
|
|
|
49
49
|
.ownSourceFiles()
|
|
50
50
|
.filter((file) => relativePath(consumer.root, file.fileName) !== undefined &&
|
|
51
51
|
relativePath(targetRoot, file.fileName) === undefined);
|
|
52
|
-
const sink = new Sink(consumer?.root ?? targetRoot, options.testFile, consumer);
|
|
53
52
|
const ownPaths = project.ownPaths();
|
|
54
53
|
const fileIds = new Set(held.symbols.filter((symbol) => symbol.kind === "file").map((symbol) => symbol.id));
|
|
55
54
|
const declaredAt = (node) => declarations.get(nodeKey(node.getSourceFile(), node));
|
|
55
|
+
const sink = new Sink(consumer?.root ?? targetRoot, options.testFile, consumer, declaredAt);
|
|
56
56
|
let moduleBodies;
|
|
57
57
|
/**
|
|
58
58
|
* The declarations one module holds directly: a file's top level, or the body of a
|
|
@@ -182,7 +182,7 @@ function declarationNodes(held, byPath) {
|
|
|
182
182
|
return {
|
|
183
183
|
typeNamesOf: (symbol) => {
|
|
184
184
|
const { byId, nodes } = nodesIn(symbol.position.path);
|
|
185
|
-
return (byId.get(symbol.id) ?? []).flatMap((declaration) => typeNamesOf(declaration, nodes));
|
|
185
|
+
return (byId.get(symbol.id) ?? []).flatMap((declaration) => typeNamesOf(declaration, (nested) => nodes.has(nested)));
|
|
186
186
|
},
|
|
187
187
|
nodesOf: (symbol) => nodesIn(symbol.position.path).byId.get(symbol.id) ?? [],
|
|
188
188
|
};
|
|
@@ -369,12 +369,12 @@ function inferredTypeSymbols(project, declarations) {
|
|
|
369
369
|
/**
|
|
370
370
|
* The names of types one declaration's own syntax writes: in a type reference, a
|
|
371
371
|
* heritage clause or a type query. A function body is not read, and neither is a
|
|
372
|
-
* declaration nested in this one
|
|
372
|
+
* declaration nested in this one that `readAlone` says is read on its own.
|
|
373
373
|
*/
|
|
374
|
-
function typeNamesOf(declaration,
|
|
374
|
+
export function typeNamesOf(declaration, readAlone) {
|
|
375
375
|
const found = [];
|
|
376
376
|
const visit = (node) => {
|
|
377
|
-
if (node !== declaration &&
|
|
377
|
+
if (node !== declaration && readAlone(node)) {
|
|
378
378
|
return;
|
|
379
379
|
}
|
|
380
380
|
if (isTypeReferenceNode(node)) {
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { EXIT_CLEAN, EXIT_USAGE } from "../contract/contract.generated.js";
|
|
2
2
|
import { productionMode, runAnalysis } from "../analysis.js";
|
|
3
|
-
import { listing } from "../components.js";
|
|
4
|
-
import {
|
|
5
|
-
import { referenceKey } from "../matrix.js";
|
|
3
|
+
import { listing } from "../liveness/components.js";
|
|
4
|
+
import { swept as judged } from "../liveness/graph.js";
|
|
6
5
|
import { partialNotes } from "../partial.js";
|
|
7
6
|
import { resolvePath } from "../paths.js";
|
|
8
7
|
import { positionKey } from "../position.js";
|
|
9
|
-
import { madeByTestCode
|
|
8
|
+
import { madeByTestCode } from "../tests/file-class.js";
|
|
10
9
|
import { resolve } from "../resolve.js";
|
|
11
10
|
import {} from "./verb.js";
|
|
12
11
|
import { UsageError } from "../usage.js";
|
|
@@ -80,172 +79,94 @@ function subjectOf(symbols, named) {
|
|
|
80
79
|
.sort(bytewise);
|
|
81
80
|
return { candidates: partial.slice(0, MAX_PARTIAL_MATCHES) };
|
|
82
81
|
}
|
|
83
|
-
/** Every reference of the run once, a reference several configurations make counted once. */
|
|
84
|
-
function referencesOf(configured) {
|
|
85
|
-
const seen = new Set();
|
|
86
|
-
const held = [];
|
|
87
|
-
for (const reference of configured.flatMap((one) => one.references)) {
|
|
88
|
-
const key = referenceKey(reference);
|
|
89
|
-
if (!seen.has(key)) {
|
|
90
|
-
seen.add(key);
|
|
91
|
-
held.push(reference);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
return held;
|
|
95
|
-
}
|
|
96
|
-
/** Every root of the run once per reason, in the order the projects name them. */
|
|
97
|
-
function rootsOf(configured) {
|
|
98
|
-
const seen = new Set();
|
|
99
|
-
return configured
|
|
100
|
-
.flatMap((one) => one.roots)
|
|
101
|
-
.filter((root) => {
|
|
102
|
-
const key = [root.id, root.kind, root.source].join("\u0000");
|
|
103
|
-
if (seen.has(key)) {
|
|
104
|
-
return false;
|
|
105
|
-
}
|
|
106
|
-
seen.add(key);
|
|
107
|
-
return true;
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* The shortest path from a root, a loaded consumer or a test file no project holds to one
|
|
112
|
-
* declaration, walked breadth first from every root at once in the order the run names them,
|
|
113
|
-
* a reference of a consumer or of such a file being the first hop of a path of its own, as
|
|
114
|
-
* the sweep seeds them, and following the references the sweep follows: a
|
|
115
|
-
* reference that reads a module reaches what the module exports, and an evaluation or a root
|
|
116
|
-
* reaches the module alone. Under `production` no reference a test file made is followed,
|
|
117
|
-
* which is the set the report's sweep counts.
|
|
118
|
-
*/
|
|
119
|
-
function shortestPath(analysis, to, mode, support) {
|
|
120
|
-
const graph = analysis.swept.matrix.union;
|
|
121
|
-
const out = new Map();
|
|
122
|
-
const consumed = [];
|
|
123
|
-
for (const reference of referencesOf(analysis.configured)) {
|
|
124
|
-
if (withheldBy(mode, reference, support)) {
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
const { consumer } = reference;
|
|
128
|
-
if (consumer !== undefined) {
|
|
129
|
-
consumed.push({ reference, origin: { k: "consumer", consumer } });
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
132
|
-
if (reference.unheld === true) {
|
|
133
|
-
consumed.push({
|
|
134
|
-
reference,
|
|
135
|
-
origin: { k: "unheld", file: reference.from.replace(/#$/u, "") },
|
|
136
|
-
});
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
const held = out.get(reference.from);
|
|
140
|
-
if (held === undefined) {
|
|
141
|
-
out.set(reference.from, [reference]);
|
|
142
|
-
}
|
|
143
|
-
else {
|
|
144
|
-
held.push(reference);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
const steps = [];
|
|
148
|
-
const seen = new Set();
|
|
149
|
-
const expanded = new Set();
|
|
150
|
-
const enter = (step) => {
|
|
151
|
-
if (!seen.has(step.at)) {
|
|
152
|
-
seen.add(step.at);
|
|
153
|
-
steps.push(step);
|
|
154
|
-
}
|
|
155
|
-
};
|
|
156
|
-
const follow = (reference, previous, origin) => {
|
|
157
|
-
enter({ at: reference.to, hop: { k: "reference", reference }, previous, origin });
|
|
158
|
-
const target = graph.at(reference.to);
|
|
159
|
-
if (reference.use === "evaluation" || target === OUTSIDE || expanded.has(reference.to)) {
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
expanded.add(reference.to);
|
|
163
|
-
for (const exported of graph.exportsOf[target] ?? []) {
|
|
164
|
-
const id = graph.symbols[exported]?.id;
|
|
165
|
-
if (id !== undefined) {
|
|
166
|
-
enter({ at: id, hop: { k: "export", reference, to: id }, previous, origin });
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
};
|
|
170
|
-
for (const root of rootsOf(analysis.configured)) {
|
|
171
|
-
enter({ at: root.id, hop: undefined, previous: -1, origin: { k: "root", root } });
|
|
172
|
-
}
|
|
173
|
-
// A consumed reference is one hop long, so it joins the walk where the roots' first hops do.
|
|
174
|
-
const firstHops = steps.length;
|
|
175
|
-
for (let head = 0;; head += 1) {
|
|
176
|
-
if (head === firstHops) {
|
|
177
|
-
for (const { reference, origin } of consumed) {
|
|
178
|
-
follow(reference, -1, origin);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
const step = steps[head];
|
|
182
|
-
if (step === undefined) {
|
|
183
|
-
break;
|
|
184
|
-
}
|
|
185
|
-
if (step.at === to) {
|
|
186
|
-
const path = [];
|
|
187
|
-
for (let at = step; at?.hop !== undefined; at = steps[at.previous]) {
|
|
188
|
-
path.unshift(at.hop);
|
|
189
|
-
}
|
|
190
|
-
return { origin: step.origin, path };
|
|
191
|
-
}
|
|
192
|
-
for (const reference of out.get(step.at) ?? []) {
|
|
193
|
-
follow(reference, head, step.origin);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
return undefined;
|
|
197
|
-
}
|
|
198
82
|
/** Where a path starts, as a line names it. */
|
|
199
|
-
function originText(origin) {
|
|
83
|
+
function originText(origin, graph) {
|
|
84
|
+
const id = (at) => graph.symbols[at]?.id ?? "";
|
|
200
85
|
switch (origin.k) {
|
|
201
86
|
case "root":
|
|
202
|
-
return `root ${origin.
|
|
203
|
-
case "
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
87
|
+
return `root ${origin.rooted.kind} ${id(origin.rooted.at)}`;
|
|
88
|
+
case "consumed": {
|
|
89
|
+
const reference = graph.references[origin.edge.reference];
|
|
90
|
+
if (reference?.consumer !== undefined) {
|
|
91
|
+
return `consumer ${reference.consumer}`;
|
|
92
|
+
}
|
|
93
|
+
return reference?.unheld === true
|
|
94
|
+
? `test file no project holds ${reference.from.replace(/#$/u, "")}`
|
|
95
|
+
: `declaration no configuration judges ${reference?.from ?? ""}`;
|
|
96
|
+
}
|
|
97
|
+
case "exemption":
|
|
98
|
+
return `exemption ${origin.record.class} ${positionKey(origin.record.site)}`;
|
|
99
|
+
case "mark":
|
|
100
|
+
return `suppression of ${id(origin.at)}`;
|
|
101
|
+
case "unanswered":
|
|
102
|
+
return `unanswered question about ${id(origin.at)}`;
|
|
207
103
|
}
|
|
208
104
|
}
|
|
209
105
|
/** What made a reference: a declaration of the target, or a loaded consumer. */
|
|
210
106
|
function madeBy(reference) {
|
|
211
107
|
return reference.consumer === undefined ? reference.from : `consumer ${reference.consumer}`;
|
|
212
108
|
}
|
|
213
|
-
/**
|
|
214
|
-
|
|
215
|
-
|
|
109
|
+
/**
|
|
110
|
+
* One reference of an answer, marked where test code made it, a test-support file's included,
|
|
111
|
+
* and naming the configurations that make it where they are not every one of the run.
|
|
112
|
+
*/
|
|
113
|
+
function referenceLine(reference, tail, support, configurations) {
|
|
114
|
+
const partial = configurations.making.length < configurations.run.length;
|
|
115
|
+
return ` reference: ${positionKey(reference.position)}\t${reference.use}\t${tail}${madeByTestCode(reference, support) ? "\tfrom a test file" : ""}${partial ? `\tmade in ${configurations.making.join(" ")}` : ""}\n`;
|
|
216
116
|
}
|
|
217
117
|
/** Why a declaration no finding names and no exemption held back is live, or how it fell. */
|
|
218
|
-
function liveness(analysis, subject, cascade
|
|
219
|
-
const { sweep } = analysis.swept;
|
|
118
|
+
function liveness(analysis, subject, cascade) {
|
|
119
|
+
const { matrix, sweep } = analysis.swept;
|
|
120
|
+
const graph = matrix.union;
|
|
220
121
|
const candidate = sweep.candidates.find((one) => one.id === subject.id);
|
|
221
122
|
if (candidate === undefined && !judged(subject.kind)) {
|
|
222
123
|
return `answer: unjudged\n unjudged: neither liveness relation judges a ${subject.kind}\n`;
|
|
223
124
|
}
|
|
224
125
|
let text = `answer: ${candidate === undefined ? "live" : "dead"}\n`;
|
|
225
126
|
text += ` live under: ${listed(sweep.liveUnder.get(subject.id) ?? [])}\n`;
|
|
226
|
-
|
|
227
|
-
|
|
127
|
+
const at = graph.at(subject.id);
|
|
128
|
+
const seen = new Set();
|
|
129
|
+
for (const root of graph.rooted.filter((one) => one.at === at)) {
|
|
130
|
+
const key = `${root.kind}\u0000${root.source}`;
|
|
131
|
+
if (!seen.has(key)) {
|
|
132
|
+
seen.add(key);
|
|
133
|
+
text += ` root: ${root.kind}${root.source === "" ? "" : `\tnamed by ${root.source}`}\n`;
|
|
134
|
+
}
|
|
228
135
|
}
|
|
229
136
|
const support = new Set(analysis.configured.flatMap((one) => one.supportFiles ?? []));
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
137
|
+
const line = (edge, tail) => {
|
|
138
|
+
const reference = graph.references[edge.reference];
|
|
139
|
+
return reference === undefined
|
|
140
|
+
? ""
|
|
141
|
+
: referenceLine(reference, tail(reference), support, {
|
|
142
|
+
making: graph.configurations[edge.reference] ?? [],
|
|
143
|
+
run: matrix.configurations,
|
|
144
|
+
});
|
|
145
|
+
};
|
|
146
|
+
const found = sweep.path(subject.id);
|
|
147
|
+
if (found !== undefined && found.hops.length > 0) {
|
|
148
|
+
text += ` reached from: ${originText(found.origin, graph)}\n`;
|
|
149
|
+
for (const hop of found.hops) {
|
|
150
|
+
text += line(hop.edge, (reference) => `${madeBy(reference)} -> ${reference.to}`);
|
|
237
151
|
if (hop.k === "export") {
|
|
238
|
-
|
|
152
|
+
const reference = graph.references[hop.edge.reference];
|
|
153
|
+
text += ` export: ${reference?.to ?? ""} -> ${graph.symbols[hop.to]?.id ?? ""}\n`;
|
|
239
154
|
}
|
|
240
155
|
}
|
|
241
156
|
}
|
|
242
157
|
else if (found === undefined) {
|
|
243
|
-
const references =
|
|
158
|
+
const references = graph.references.flatMap((one, place) => one.to === subject.id ? [place] : []);
|
|
244
159
|
text +=
|
|
245
160
|
" unreachable: no path of references reaches it from a root, a loaded consumer or a test file no project holds\n";
|
|
246
161
|
text += ` references: ${String(references.length)}\n`;
|
|
247
|
-
for (const
|
|
248
|
-
|
|
162
|
+
for (const place of references) {
|
|
163
|
+
const reference = graph.references[place];
|
|
164
|
+
if (reference !== undefined) {
|
|
165
|
+
text += referenceLine(reference, `from ${madeBy(reference)}`, support, {
|
|
166
|
+
making: graph.configurations[place] ?? [],
|
|
167
|
+
run: matrix.configurations,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
249
170
|
}
|
|
250
171
|
}
|
|
251
172
|
if (candidate !== undefined) {
|
|
@@ -294,7 +215,7 @@ function namedElsewhere(findings, subject) {
|
|
|
294
215
|
.join("");
|
|
295
216
|
}
|
|
296
217
|
/** The one explanation that applies to a declaration, built whole before it is written. */
|
|
297
|
-
function explanation(analysis, subject, cascade
|
|
218
|
+
function explanation(analysis, subject, cascade) {
|
|
298
219
|
const graph = analysis.swept.matrix.union;
|
|
299
220
|
const at = graph.at(subject.id);
|
|
300
221
|
let text = `symbol: ${subject.ref}\n`;
|
|
@@ -302,7 +223,7 @@ function explanation(analysis, subject, cascade, mode) {
|
|
|
302
223
|
text += `position: ${subject.id}\n`;
|
|
303
224
|
text += `configurations: ${listed(analysis.swept.matrix.heldIn[at] ?? [])}\n`;
|
|
304
225
|
const found = analysis.result.findings.find((one) => positionKey(one.position) === subject.id);
|
|
305
|
-
const retained = analysis.swept.retained.filter((one) => one.id === subject.id);
|
|
226
|
+
const retained = analysis.swept.sweep.retained.filter((one) => one.id === subject.id);
|
|
306
227
|
if (found !== undefined) {
|
|
307
228
|
text += reported(found);
|
|
308
229
|
}
|
|
@@ -312,22 +233,22 @@ function explanation(analysis, subject, cascade, mode) {
|
|
|
312
233
|
text += ` class: ${one.class}\t${positionKey(one.site)}\t${one.detail}\n`;
|
|
313
234
|
}
|
|
314
235
|
}
|
|
315
|
-
else if (analysis.swept.
|
|
236
|
+
else if (analysis.swept.sweep.held.includes(subject.id)) {
|
|
316
237
|
text +=
|
|
317
238
|
"answer: held\n held by: a question the checker did not answer, which could have kept it live, so no finding names it\n";
|
|
318
239
|
}
|
|
319
240
|
else {
|
|
320
|
-
text += liveness(analysis, subject, cascade
|
|
241
|
+
text += liveness(analysis, subject, cascade);
|
|
321
242
|
}
|
|
322
243
|
return text + namedElsewhere(analysis.result.findings, subject);
|
|
323
244
|
}
|
|
324
245
|
/**
|
|
325
246
|
* Explains one symbol from the analysis the report is built from: why a reported symbol is
|
|
326
247
|
* reported, which exemption classes held a retained one back, that an unanswered question held
|
|
327
|
-
* a held one, and for any other declaration the relations that hold it live and the
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
248
|
+
* a held one, and for any other declaration the relations that hold it live and the path the
|
|
249
|
+
* sweep's strongest tier reaching it follows, or the references it has where no path reaches
|
|
250
|
+
* it. A request naming no one declaration exits with the usage code and names what partially
|
|
251
|
+
* matches it. Where the analysis was partial, the error stream says so.
|
|
331
252
|
*/
|
|
332
253
|
export const explainVerb = ({ out, err, host, inputs, scope, openClient, option }) => {
|
|
333
254
|
const named = namedSymbol(option);
|
|
@@ -351,7 +272,7 @@ export const explainVerb = ({ out, err, host, inputs, scope, openClient, option
|
|
|
351
272
|
}
|
|
352
273
|
}
|
|
353
274
|
else {
|
|
354
|
-
out.write(explanation(analysis, subject, config.reporters.cascade
|
|
275
|
+
out.write(explanation(analysis, subject, config.reporters.cascade));
|
|
355
276
|
}
|
|
356
277
|
for (const note of notes) {
|
|
357
278
|
diagnose(err, note);
|
|
@@ -20,7 +20,7 @@ export const printRetainedVerb = ({ out, err, host, inputs, scope, openClient })
|
|
|
20
20
|
marked: [],
|
|
21
21
|
mode: productionMode(config),
|
|
22
22
|
});
|
|
23
|
-
for (const line of retainedLines(swept.matrix.union.symbols, swept.retained)) {
|
|
23
|
+
for (const line of retainedLines(swept.matrix.union.symbols, swept.sweep.retained)) {
|
|
24
24
|
out.write(`${line}\n`);
|
|
25
25
|
}
|
|
26
26
|
for (const note of partialNotes({
|
package/jsr.json
CHANGED