@cplieger/deadset-ts 3.9.1 → 3.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -13
- package/dist/src/analysis.js +5 -3
- package/dist/src/config.js +6 -0
- package/dist/src/conformance-record.js +26 -0
- package/dist/src/conformance.js +8 -20
- package/dist/src/findings-pass.js +3 -5
- package/dist/src/report.js +14 -1
- package/dist/src/reporters/annotations.js +36 -0
- package/dist/src/reporters/fingerprint.js +61 -0
- package/dist/src/reporters/reporter.js +7 -1
- package/dist/src/reporters/reporters.js +18 -0
- package/dist/src/reporters/rules.js +178 -0
- package/dist/src/reporters/sarif.js +235 -0
- package/dist/src/reporters/sha256.js +101 -0
- package/dist/src/reporters/template.js +877 -0
- package/dist/src/run.js +4 -10
- package/dist/src/verbs/analyze.js +45 -7
- package/dist/src/verbs/describe.js +8 -12
- package/dist/src/verbs/explain.js +303 -0
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +14 -4
- package/src/config.ts +8 -0
- package/src/conformance-record.ts +29 -0
- package/src/conformance.ts +21 -23
- package/src/findings-pass.ts +5 -7
- package/src/report.ts +25 -3
- package/src/reporters/annotations.ts +67 -0
- package/src/reporters/fingerprint.ts +69 -0
- package/src/reporters/json.ts +2 -2
- package/src/reporters/reporter.ts +21 -2
- package/src/reporters/reporters.ts +18 -1
- package/src/reporters/rules.ts +195 -0
- package/src/reporters/sarif.ts +287 -0
- package/src/reporters/sha256.ts +120 -0
- package/src/reporters/template.ts +989 -0
- package/src/reporters/text.ts +2 -3
- package/src/run.ts +5 -11
- package/src/verbs/analyze.ts +57 -7
- package/src/verbs/describe.ts +9 -13
- package/src/verbs/explain.ts +363 -0
package/src/reporters/text.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type { WireTotals } from "../report.ts";
|
|
2
|
-
import type { Reporter } from "./reporter.ts";
|
|
1
|
+
import type { Report, WireTotals } from "../report.ts";
|
|
3
2
|
|
|
4
3
|
/** The kind token and the confidence a stale suppression is rendered with, both fixed. */
|
|
5
4
|
const STALE_KIND = "suppression";
|
|
@@ -30,7 +29,7 @@ function summary(totals: WireTotals): string {
|
|
|
30
29
|
* summary is deliberately not in that shape, so a filter for finding lines yields exactly
|
|
31
30
|
* them. Nothing else is written: no timestamp, duration or host detail.
|
|
32
31
|
*/
|
|
33
|
-
export const text
|
|
32
|
+
export const text = (report: Report): string => {
|
|
34
33
|
const lines = report.findings.map(
|
|
35
34
|
(found) =>
|
|
36
35
|
`${found.position.path}:${String(found.position.line)}:${String(found.position.column)}: ${found.symbol.kind} ${found.symbol.name}: ${found.message} [${found.confidence}] (${found.code})`,
|
package/src/run.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { printRetainedVerb } from "./verbs/print-retained.ts";
|
|
|
11
11
|
import { printRootsVerb } from "./verbs/print-roots.ts";
|
|
12
12
|
import { ANALYZE_OPTIONS, analyzeVerb } from "./verbs/analyze.ts";
|
|
13
13
|
import { describeVerb } from "./verbs/describe.ts";
|
|
14
|
+
import { EXPLAIN_OPTIONS, explainVerb } from "./verbs/explain.ts";
|
|
14
15
|
import { EXIT_FAILURE, EXIT_USAGE, type Verb, type VerbOptions } from "./verbs/verb.ts";
|
|
15
16
|
import { versionVerb } from "./verbs/version.ts";
|
|
16
17
|
|
|
@@ -19,17 +20,14 @@ export interface Writer {
|
|
|
19
20
|
write(text: string): void;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
/**
|
|
23
|
-
* Every verb this command answers, in the order the usage text lists them. A verb
|
|
24
|
-
* with no `run` is listed, and invoking it is a usage error.
|
|
25
|
-
*/
|
|
23
|
+
/** Every verb this command answers, in the order the usage text lists them. */
|
|
26
24
|
const VERBS: readonly {
|
|
27
25
|
readonly name: string;
|
|
28
|
-
readonly run
|
|
26
|
+
readonly run: Verb;
|
|
29
27
|
readonly options?: VerbOptions;
|
|
30
28
|
}[] = [
|
|
31
29
|
{ name: "analyze", run: analyzeVerb, options: ANALYZE_OPTIONS },
|
|
32
|
-
{ name: "explain" },
|
|
30
|
+
{ name: "explain", run: explainVerb, options: EXPLAIN_OPTIONS },
|
|
33
31
|
{ name: "print-config", run: printConfigVerb },
|
|
34
32
|
{ name: "print-projects", run: printProjectsVerb },
|
|
35
33
|
{ name: "print-roots", run: printRootsVerb },
|
|
@@ -219,7 +217,7 @@ export function run(
|
|
|
219
217
|
|
|
220
218
|
const name = args[0];
|
|
221
219
|
const verb = VERBS.find((listed) => listed.name === name);
|
|
222
|
-
if (verb
|
|
220
|
+
if (verb !== undefined) {
|
|
223
221
|
return invoke(
|
|
224
222
|
verb.run,
|
|
225
223
|
verb.options ?? NO_OPTIONS,
|
|
@@ -228,10 +226,6 @@ export function run(
|
|
|
228
226
|
openClient,
|
|
229
227
|
);
|
|
230
228
|
}
|
|
231
|
-
if (verb !== undefined) {
|
|
232
|
-
err.write(`deadset-ts: ${verb.name} is not implemented\n`);
|
|
233
|
-
return EXIT_USAGE;
|
|
234
|
-
}
|
|
235
229
|
if (name !== undefined) {
|
|
236
230
|
err.write(`deadset-ts: unknown verb ${JSON.stringify(name)}\n`);
|
|
237
231
|
}
|
package/src/verbs/analyze.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { runAnalysis, type RunProject } from "../analysis.ts";
|
|
2
2
|
import { ConfigError, type Format } from "../config.ts";
|
|
3
|
-
import {
|
|
3
|
+
import { CONFORMANCE, DECLARED_GAPS } from "../conformance.ts";
|
|
4
4
|
import { analyzerProvenance, recordedFindings, verdictOf } from "../findings-pass.ts";
|
|
5
5
|
import { packageScope } from "../inventory.ts";
|
|
6
|
-
import { normalizePath, relativePath, resolvePath } from "../paths.ts";
|
|
6
|
+
import { joinPath, normalizePath, relativePath, resolvePath } from "../paths.ts";
|
|
7
7
|
import type { Host } from "../host.ts";
|
|
8
8
|
import {
|
|
9
9
|
buildReport,
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from "../report.ts";
|
|
15
15
|
import { json } from "../reporters/json.ts";
|
|
16
16
|
import { RENDERINGS } from "../reporters/reporters.ts";
|
|
17
|
+
import { parseTemplate, TemplateError, type Template } from "../reporters/template.ts";
|
|
17
18
|
import { resolve } from "../resolve.ts";
|
|
18
19
|
import type { Module } from "../scope.ts";
|
|
19
20
|
import { writeBaseline, type Recorded } from "../suppress-file.ts";
|
|
@@ -22,7 +23,7 @@ import { EXIT_CLEAN, EXIT_FAILURE, type Verb, type VerbOptions } from "./verb.ts
|
|
|
22
23
|
|
|
23
24
|
/** The options `analyze` takes beside the ones every verb takes. */
|
|
24
25
|
export const ANALYZE_OPTIONS: VerbOptions = {
|
|
25
|
-
plain: ["report", "baseline-write", "exit-code"],
|
|
26
|
+
plain: ["report", "baseline-write", "exit-code", "template"],
|
|
26
27
|
repeatable: ["format"],
|
|
27
28
|
};
|
|
28
29
|
|
|
@@ -110,6 +111,47 @@ function counted(n: number, noun: string): string {
|
|
|
110
111
|
return `${String(n)} ${noun}${n === 1 ? "" : "s"}`;
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
/**
|
|
115
|
+
* The template `--template` names, read and parsed before any analysis, so a template that
|
|
116
|
+
* cannot be read or does not parse refuses the invocation. The template format with no
|
|
117
|
+
* template named is refused the same way.
|
|
118
|
+
*/
|
|
119
|
+
function templateOf(
|
|
120
|
+
host: Host,
|
|
121
|
+
invoked: string,
|
|
122
|
+
named: string | undefined,
|
|
123
|
+
formats: readonly Format[],
|
|
124
|
+
): Template | undefined {
|
|
125
|
+
if (named === undefined || named === "") {
|
|
126
|
+
if (formats.includes("template")) {
|
|
127
|
+
throw new ConfigError(
|
|
128
|
+
"malformed",
|
|
129
|
+
"",
|
|
130
|
+
"the template format renders the template --template names, and none was named",
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
let source: string;
|
|
136
|
+
try {
|
|
137
|
+
source = host.readFile(resolvePath(invoked, named));
|
|
138
|
+
} catch (error: unknown) {
|
|
139
|
+
throw new ConfigError(
|
|
140
|
+
"malformed",
|
|
141
|
+
"",
|
|
142
|
+
`--template=${named}: ${error instanceof Error ? error.message : String(error)}`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
return parseTemplate(source);
|
|
147
|
+
} catch (error: unknown) {
|
|
148
|
+
if (!(error instanceof TemplateError)) {
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
throw new ConfigError("malformed", "", `--template=${named}: ${error.message}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
113
155
|
/** Every document one invocation asked for: the report, each rendering beside it, a baseline. */
|
|
114
156
|
interface Documents {
|
|
115
157
|
readonly report: string;
|
|
@@ -121,8 +163,9 @@ interface Documents {
|
|
|
121
163
|
* Analyzes the scope under the configuration and writes the JSON report to the path
|
|
122
164
|
* `--report` names, each rendering `--format` names beside it at the report's path with the
|
|
123
165
|
* format's suffix appended, and a baseline of every finding where `--baseline-write` names a
|
|
124
|
-
* path.
|
|
125
|
-
* verdict is the exit code, or 0
|
|
166
|
+
* path. The template format renders the template `--template` names. Nothing is written to
|
|
167
|
+
* the output stream; diagnostics go to the error stream and the verdict is the exit code, or 0
|
|
168
|
+
* under `--exit-code=off`, and a rendering that cannot be written ends the run with 3.
|
|
126
169
|
*/
|
|
127
170
|
export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option, repeated }) => {
|
|
128
171
|
const reportPath = option("report");
|
|
@@ -153,6 +196,7 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
|
|
|
153
196
|
formats: formatsOf(repeated("format"), config.reporters.formats),
|
|
154
197
|
baseline: baseline === undefined ? undefined : resolvePath(invoked, baseline),
|
|
155
198
|
};
|
|
199
|
+
const template = templateOf(host, invoked, option("template"), documents.formats);
|
|
156
200
|
const scoped = scope();
|
|
157
201
|
const targetRoot = scoped.target.path;
|
|
158
202
|
|
|
@@ -175,7 +219,8 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
|
|
|
175
219
|
const built = buildReport({
|
|
176
220
|
contractVersion: CONTRACT_VERSION,
|
|
177
221
|
version,
|
|
178
|
-
conformance:
|
|
222
|
+
conformance: CONFORMANCE,
|
|
223
|
+
declaredGaps: DECLARED_GAPS,
|
|
179
224
|
target: { kind, root, identity: identityOf(scoped.target, host) },
|
|
180
225
|
configurations: configurationsOf(run.projects, targetRoot),
|
|
181
226
|
unavailable: scoped.consumers.map((consumer) => ({
|
|
@@ -200,10 +245,15 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
|
|
|
200
245
|
|
|
201
246
|
try {
|
|
202
247
|
host.writeFile(documents.report, json(report));
|
|
248
|
+
const options = {
|
|
249
|
+
failOn: config.reporters.failOn,
|
|
250
|
+
readSource: (path: string) => host.readFile(joinPath(targetRoot, path)),
|
|
251
|
+
template,
|
|
252
|
+
};
|
|
203
253
|
for (const format of documents.formats) {
|
|
204
254
|
const rendering = RENDERINGS.get(format);
|
|
205
255
|
if (rendering !== undefined) {
|
|
206
|
-
host.writeFile(documents.report + rendering.suffix, rendering.render(report));
|
|
256
|
+
host.writeFile(documents.report + rendering.suffix, rendering.render(report, options));
|
|
207
257
|
}
|
|
208
258
|
}
|
|
209
259
|
if (documents.baseline !== undefined) {
|
package/src/verbs/describe.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CONFORMANCE, type Conformance } from "../conformance.ts";
|
|
2
2
|
import { ANALYZER_NAME, LANGUAGE, SCHEMA_VERSION } from "../report.ts";
|
|
3
3
|
import { CONTRACT_VERSION } from "../version.ts";
|
|
4
4
|
import { EXIT_CLEAN, EXIT_FAILURE, EXIT_USAGE, type Verb } from "./verb.ts";
|
|
@@ -6,24 +6,20 @@ import { EXIT_CLEAN, EXIT_FAILURE, EXIT_USAGE, type Verb } from "./verb.ts";
|
|
|
6
6
|
/**
|
|
7
7
|
* The document `describe` writes: the analyzer, the Contract version it implements, the
|
|
8
8
|
* report schema versions it reads, the languages it claims, and its recorded result over
|
|
9
|
-
* the conformance corpus
|
|
9
|
+
* the conformance corpus.
|
|
10
10
|
*/
|
|
11
|
-
export function describeDocument(version: string, recorded: Conformance
|
|
11
|
+
export function describeDocument(version: string, recorded: Conformance): string {
|
|
12
12
|
const document = {
|
|
13
13
|
name: ANALYZER_NAME,
|
|
14
14
|
version,
|
|
15
15
|
contract_version: CONTRACT_VERSION,
|
|
16
16
|
schema_versions_accepted: [SCHEMA_VERSION],
|
|
17
17
|
languages: [LANGUAGE],
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
:
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
result: recorded.result,
|
|
24
|
-
digest: recorded.digest,
|
|
25
|
-
},
|
|
26
|
-
}),
|
|
18
|
+
conformance: {
|
|
19
|
+
corpus_version: recorded.corpusVersion,
|
|
20
|
+
result: recorded.result,
|
|
21
|
+
digest: recorded.digest,
|
|
22
|
+
},
|
|
27
23
|
};
|
|
28
24
|
return `${JSON.stringify(document, null, 2)}\n`;
|
|
29
25
|
}
|
|
@@ -41,6 +37,6 @@ export const describeVerb: Verb = ({ out, err, host, args }) => {
|
|
|
41
37
|
err.write(`deadset-ts: describe: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
42
38
|
return EXIT_FAILURE;
|
|
43
39
|
}
|
|
44
|
-
out.write(describeDocument(version,
|
|
40
|
+
out.write(describeDocument(version, CONFORMANCE));
|
|
45
41
|
return EXIT_CLEAN;
|
|
46
42
|
};
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { runAnalysis, type RunAnalysis } from "../analysis.ts";
|
|
2
|
+
import { listing } from "../components.ts";
|
|
3
|
+
import type { Cascade } from "../config.ts";
|
|
4
|
+
import { ConfigError } from "../config.ts";
|
|
5
|
+
import type { CompletedFinding } from "../finding.ts";
|
|
6
|
+
import { OUTSIDE, swept as judged } from "../graph.ts";
|
|
7
|
+
import type { InventorySymbol } from "../inventory.ts";
|
|
8
|
+
import type { Configured } from "../matrix.ts";
|
|
9
|
+
import { positionKey } from "../position.ts";
|
|
10
|
+
import type { Reference } from "../references.ts";
|
|
11
|
+
import { resolve } from "../resolve.ts";
|
|
12
|
+
import type { Root } from "../roots.ts";
|
|
13
|
+
import { EXIT_CLEAN, EXIT_USAGE, type Verb, type VerbOptions } from "./verb.ts";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The three spellings of an explanation request, each naming the symbol to explain. They are
|
|
17
|
+
* one request, because exactly one state holds for a symbol and the answer is that state.
|
|
18
|
+
*/
|
|
19
|
+
const WHY_OPTIONS = ["why", "why-live", "why-not"] as const;
|
|
20
|
+
|
|
21
|
+
/** The options `explain` takes beside the ones every verb takes. */
|
|
22
|
+
export const EXPLAIN_OPTIONS: VerbOptions = { plain: [...WHY_OPTIONS], repeatable: [] };
|
|
23
|
+
|
|
24
|
+
/** The most partial matches a refused request prints. */
|
|
25
|
+
const MAX_PARTIAL_MATCHES = 20;
|
|
26
|
+
|
|
27
|
+
/** A count with its noun, so a line reads for one as well as for several. */
|
|
28
|
+
function counted(n: number, noun: string): string {
|
|
29
|
+
return `${String(n)} ${noun}${n === 1 ? "" : "s"}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A list for a line a maintainer reads, which says none rather than leaving the field blank. */
|
|
33
|
+
function listed(items: readonly string[]): string {
|
|
34
|
+
return items.length === 0 ? "none" : items.join(" ");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The one symbol a request names, from whichever of the three options names it. */
|
|
38
|
+
function namedSymbol(option: (name: string) => string | undefined): string {
|
|
39
|
+
const named = WHY_OPTIONS.flatMap((name) => {
|
|
40
|
+
const value = option(name);
|
|
41
|
+
return value === undefined || value === "" ? [] : [{ name, value }];
|
|
42
|
+
});
|
|
43
|
+
const [only, ...more] = named;
|
|
44
|
+
if (only === undefined) {
|
|
45
|
+
throw new ConfigError(
|
|
46
|
+
"malformed",
|
|
47
|
+
"",
|
|
48
|
+
"explain explains the symbol --why, --why-live or --why-not names, and none was named",
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
if (more.length > 0) {
|
|
52
|
+
throw new ConfigError(
|
|
53
|
+
"malformed",
|
|
54
|
+
"",
|
|
55
|
+
`explain explains one symbol, and ${named.map((one) => `--${one.name}`).join(", ")} each name one`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return only.value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The part of a reference after its separator: the `Type.member` or bare name a maintainer types. */
|
|
62
|
+
function fragmentOf(symbol: InventorySymbol): string {
|
|
63
|
+
const at = symbol.ref.indexOf("#");
|
|
64
|
+
return at < 0 ? "" : symbol.ref.slice(at + 1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function described(symbol: InventorySymbol): string {
|
|
68
|
+
return `${symbol.ref}\t${positionKey(symbol.position)}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The one declaration a request names, or the candidates where it names none or several.
|
|
73
|
+
* The reference, the fragment of it a maintainer types and the display name are tried in
|
|
74
|
+
* order, each an exact match; a key several declarations answer is no answer.
|
|
75
|
+
*/
|
|
76
|
+
function subjectOf(
|
|
77
|
+
symbols: readonly InventorySymbol[],
|
|
78
|
+
named: string,
|
|
79
|
+
): { readonly subject?: InventorySymbol; readonly candidates: readonly string[] } {
|
|
80
|
+
const keys: ((symbol: InventorySymbol) => string)[] = [
|
|
81
|
+
(symbol) => symbol.ref,
|
|
82
|
+
fragmentOf,
|
|
83
|
+
(symbol) => symbol.name,
|
|
84
|
+
];
|
|
85
|
+
for (const key of keys) {
|
|
86
|
+
const matched = symbols.filter((symbol) => key(symbol) === named);
|
|
87
|
+
const [only] = matched;
|
|
88
|
+
if (only !== undefined && matched.length === 1) {
|
|
89
|
+
return { subject: only, candidates: [] };
|
|
90
|
+
}
|
|
91
|
+
if (matched.length > 1) {
|
|
92
|
+
return { candidates: matched.map(described) };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const folded = named.toLowerCase();
|
|
96
|
+
const partial = symbols
|
|
97
|
+
.filter(
|
|
98
|
+
(symbol) =>
|
|
99
|
+
symbol.ref.toLowerCase().includes(folded) || symbol.name.toLowerCase().includes(folded),
|
|
100
|
+
)
|
|
101
|
+
.map(described)
|
|
102
|
+
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
103
|
+
return { candidates: partial.slice(0, MAX_PARTIAL_MATCHES) };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Every reference of the run once, a reference several configurations make counted once. */
|
|
107
|
+
function referencesOf(configured: readonly Configured[]): readonly Reference[] {
|
|
108
|
+
const seen = new Set<string>();
|
|
109
|
+
const held: Reference[] = [];
|
|
110
|
+
for (const reference of configured.flatMap((one) => one.references)) {
|
|
111
|
+
const key = [reference.from, reference.to, positionKey(reference.position), reference.use].join(
|
|
112
|
+
"\u0000",
|
|
113
|
+
);
|
|
114
|
+
if (!seen.has(key)) {
|
|
115
|
+
seen.add(key);
|
|
116
|
+
held.push(reference);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return held;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Every root of the run once per reason, in the order the projects name them. */
|
|
123
|
+
function rootsOf(configured: readonly Configured[]): readonly Root[] {
|
|
124
|
+
const seen = new Set<string>();
|
|
125
|
+
return configured
|
|
126
|
+
.flatMap((one) => one.roots)
|
|
127
|
+
.filter((root) => {
|
|
128
|
+
const key = [root.id, root.kind, root.source].join("\u0000");
|
|
129
|
+
if (seen.has(key)) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
seen.add(key);
|
|
133
|
+
return true;
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* One step of a path: a reference, or a reference that reads a module and so reaches a
|
|
139
|
+
* declaration the module exports.
|
|
140
|
+
*/
|
|
141
|
+
type Hop =
|
|
142
|
+
| { readonly k: "reference"; readonly reference: Reference }
|
|
143
|
+
| { readonly k: "export"; readonly reference: Reference; readonly to: string };
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The shortest path from a root to one declaration, walked breadth first from every root at
|
|
147
|
+
* once in the order the run names them, following the references the sweep follows: a
|
|
148
|
+
* reference that reads a module reaches what the module exports, and an evaluation or a root
|
|
149
|
+
* reaches the module alone. Under `production` no reference a test file made is followed,
|
|
150
|
+
* which is the set the report's sweep counts.
|
|
151
|
+
*/
|
|
152
|
+
function shortestPath(
|
|
153
|
+
analysis: RunAnalysis,
|
|
154
|
+
to: string,
|
|
155
|
+
production: boolean,
|
|
156
|
+
): { readonly root: Root; readonly path: readonly Hop[] } | undefined {
|
|
157
|
+
const graph = analysis.swept.matrix.union;
|
|
158
|
+
const out = new Map<string, Reference[]>();
|
|
159
|
+
for (const reference of referencesOf(analysis.configured)) {
|
|
160
|
+
if (production && reference.test) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const held = out.get(reference.from);
|
|
164
|
+
if (held === undefined) {
|
|
165
|
+
out.set(reference.from, [reference]);
|
|
166
|
+
} else {
|
|
167
|
+
held.push(reference);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
interface Step {
|
|
171
|
+
readonly at: string;
|
|
172
|
+
readonly hop: Hop | undefined;
|
|
173
|
+
readonly previous: number;
|
|
174
|
+
readonly root: Root;
|
|
175
|
+
}
|
|
176
|
+
const steps: Step[] = [];
|
|
177
|
+
const seen = new Set<string>();
|
|
178
|
+
const expanded = new Set<string>();
|
|
179
|
+
const enter = (step: Step): void => {
|
|
180
|
+
if (!seen.has(step.at)) {
|
|
181
|
+
seen.add(step.at);
|
|
182
|
+
steps.push(step);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
for (const root of rootsOf(analysis.configured)) {
|
|
186
|
+
enter({ at: root.id, hop: undefined, previous: -1, root });
|
|
187
|
+
}
|
|
188
|
+
for (let head = 0; head < steps.length; head += 1) {
|
|
189
|
+
const step = steps[head];
|
|
190
|
+
if (step === undefined) {
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
if (step.at === to) {
|
|
194
|
+
const path: Hop[] = [];
|
|
195
|
+
for (let at: Step | undefined = step; at?.hop !== undefined; at = steps[at.previous]) {
|
|
196
|
+
path.unshift(at.hop);
|
|
197
|
+
}
|
|
198
|
+
return { root: step.root, path };
|
|
199
|
+
}
|
|
200
|
+
for (const reference of out.get(step.at) ?? []) {
|
|
201
|
+
enter({
|
|
202
|
+
at: reference.to,
|
|
203
|
+
hop: { k: "reference", reference },
|
|
204
|
+
previous: head,
|
|
205
|
+
root: step.root,
|
|
206
|
+
});
|
|
207
|
+
const target = graph.at(reference.to);
|
|
208
|
+
if (reference.use === "evaluation" || target === OUTSIDE || expanded.has(reference.to)) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
expanded.add(reference.to);
|
|
212
|
+
for (const exported of graph.exportsOf[target] ?? []) {
|
|
213
|
+
const id = graph.symbols[exported]?.id;
|
|
214
|
+
if (id !== undefined) {
|
|
215
|
+
enter({
|
|
216
|
+
at: id,
|
|
217
|
+
hop: { k: "export", reference, to: id },
|
|
218
|
+
previous: head,
|
|
219
|
+
root: step.root,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function referenceLine(reference: Reference, tail: string): string {
|
|
229
|
+
return ` reference: ${positionKey(reference.position)}\t${reference.use}\t${tail}${reference.test ? "\tfrom a test file" : ""}\n`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Why a declaration no finding names and no exemption held back is live, or how it fell. */
|
|
233
|
+
function liveness(analysis: RunAnalysis, subject: InventorySymbol, cascade: Cascade): string {
|
|
234
|
+
const { sweep } = analysis.swept;
|
|
235
|
+
const candidate = sweep.candidates.find((one) => one.id === subject.id);
|
|
236
|
+
if (candidate === undefined && !judged(subject.kind)) {
|
|
237
|
+
return `answer: unjudged\n unjudged: neither liveness relation judges a ${subject.kind}\n`;
|
|
238
|
+
}
|
|
239
|
+
let text = `answer: ${candidate === undefined ? "live" : "dead"}\n`;
|
|
240
|
+
text += ` live under: ${listed(sweep.liveUnder.get(subject.id) ?? [])}\n`;
|
|
241
|
+
for (const root of rootsOf(analysis.configured).filter((one) => one.id === subject.id)) {
|
|
242
|
+
text += ` root: ${root.kind}${root.source === "" ? "" : `\tnamed by ${root.source}`}\n`;
|
|
243
|
+
}
|
|
244
|
+
const found =
|
|
245
|
+
shortestPath(analysis, subject.id, true) ?? shortestPath(analysis, subject.id, false);
|
|
246
|
+
if (found !== undefined && found.path.length > 0) {
|
|
247
|
+
text += ` reached from: root ${found.root.kind} ${found.root.id}\n`;
|
|
248
|
+
for (const hop of found.path) {
|
|
249
|
+
text += referenceLine(hop.reference, `${hop.reference.from} -> ${hop.reference.to}`);
|
|
250
|
+
if (hop.k === "export") {
|
|
251
|
+
text += ` export: ${hop.reference.to} -> ${hop.to}\n`;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
} else if (found === undefined) {
|
|
255
|
+
const references = referencesOf(analysis.configured).filter((one) => one.to === subject.id);
|
|
256
|
+
text += " unreachable: no path of references reaches it from a root\n";
|
|
257
|
+
text += ` references: ${String(references.length)}\n`;
|
|
258
|
+
for (const reference of references) {
|
|
259
|
+
text += referenceLine(reference, `from ${reference.from}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (candidate !== undefined) {
|
|
263
|
+
const component = sweep.components.find((one) => one.members.includes(subject.id));
|
|
264
|
+
if (component !== undefined) {
|
|
265
|
+
const listed = listing(component, cascade);
|
|
266
|
+
text += ` component: ${component.id}, ${counted(listed.symbolCount, "symbol")} and ${counted(listed.deletableLines, "deletable line")} fall with it, listed at cascade ${cascade}\n`;
|
|
267
|
+
for (const root of listed.roots) {
|
|
268
|
+
text += ` component root: ${root}\n`;
|
|
269
|
+
}
|
|
270
|
+
for (const member of listed.members) {
|
|
271
|
+
text += ` component member: ${member}\n`;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
text += ` candidate: dead under ${candidate.relation}, with ${String(candidate.productionRefs)} production and ${String(candidate.testRefs)} test references\n`;
|
|
275
|
+
}
|
|
276
|
+
return text;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function reported(found: CompletedFinding): string {
|
|
280
|
+
return [
|
|
281
|
+
`answer: reported`,
|
|
282
|
+
` code: ${found.code} ${found.kind}`,
|
|
283
|
+
` message: ${found.message}`,
|
|
284
|
+
` class: ${found.reachabilityClass}`,
|
|
285
|
+
` confidence: ${found.confidence}`,
|
|
286
|
+
...(found.livenessRelation === undefined
|
|
287
|
+
? []
|
|
288
|
+
: [` liveness relation: ${found.livenessRelation}`]),
|
|
289
|
+
` configurations: ${listed(found.configurations)}`,
|
|
290
|
+
` loaded consumers: ${listed(found.consumersLoaded)}`,
|
|
291
|
+
...found.retainedBy.map((one) => ` retained by: ${one}`),
|
|
292
|
+
"",
|
|
293
|
+
].join("\n");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Every finding of the run that names the declaration by its reference and reports at
|
|
298
|
+
* another position: a part of it, or a suppression record naming it. Each is published under
|
|
299
|
+
* the reference asked about, so an explanation leaving it out would disagree with the report.
|
|
300
|
+
*/
|
|
301
|
+
function namedElsewhere(findings: readonly CompletedFinding[], subject: InventorySymbol): string {
|
|
302
|
+
return findings
|
|
303
|
+
.filter(
|
|
304
|
+
(found) => found.symbol.ref === subject.ref && positionKey(found.position) !== subject.id,
|
|
305
|
+
)
|
|
306
|
+
.map(
|
|
307
|
+
(found) =>
|
|
308
|
+
`also reported: ${found.code} ${found.kind}\t${positionKey(found.position)}\t${found.symbol.kind} ${found.symbol.name}\n`,
|
|
309
|
+
)
|
|
310
|
+
.join("");
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** The one explanation that applies to a declaration, built whole before it is written. */
|
|
314
|
+
function explanation(analysis: RunAnalysis, subject: InventorySymbol, cascade: Cascade): string {
|
|
315
|
+
const graph = analysis.swept.matrix.union;
|
|
316
|
+
const at = graph.at(subject.id);
|
|
317
|
+
let text = `symbol: ${subject.ref}\n`;
|
|
318
|
+
text += `declaration: ${subject.kind} ${subject.name}\n`;
|
|
319
|
+
text += `position: ${subject.id}\n`;
|
|
320
|
+
text += `configurations: ${listed(analysis.swept.matrix.heldIn[at] ?? [])}\n`;
|
|
321
|
+
const found = analysis.result.findings.find((one) => positionKey(one.position) === subject.id);
|
|
322
|
+
const retained = analysis.swept.retained.filter((one) => one.id === subject.id);
|
|
323
|
+
if (found !== undefined) {
|
|
324
|
+
text += reported(found);
|
|
325
|
+
} else if (retained.length > 0) {
|
|
326
|
+
text += `answer: retained\n held back by: ${counted(retained.length, "exemption class")}\n`;
|
|
327
|
+
for (const one of retained) {
|
|
328
|
+
text += ` class: ${one.class}\t${positionKey(one.site)}\t${one.detail}\n`;
|
|
329
|
+
}
|
|
330
|
+
} else {
|
|
331
|
+
text += liveness(analysis, subject, cascade);
|
|
332
|
+
}
|
|
333
|
+
return text + namedElsewhere(analysis.result.findings, subject);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Explains one symbol from the analysis the report is built from, so an explanation cannot
|
|
338
|
+
* disagree with a finding: why a reported symbol is reported, which exemption classes held a
|
|
339
|
+
* retained one back, and for any other declaration the relations that hold it live and the
|
|
340
|
+
* shortest path of references from a root, or the references it has where no path reaches it.
|
|
341
|
+
* A request naming no one declaration exits with the usage code and names what partially
|
|
342
|
+
* matches it.
|
|
343
|
+
*/
|
|
344
|
+
export const explainVerb: Verb = ({ out, err, host, inputs, scope, openClient, option }) => {
|
|
345
|
+
const named = namedSymbol(option);
|
|
346
|
+
const { config, provenance } = resolve(inputs());
|
|
347
|
+
const analysis = runAnalysis(openClient(false), host, scope(), config, provenance, {
|
|
348
|
+
production: true,
|
|
349
|
+
});
|
|
350
|
+
const { subject, candidates } = subjectOf(analysis.swept.matrix.union.symbols, named);
|
|
351
|
+
if (subject === undefined) {
|
|
352
|
+
err.write(`deadset-ts: ${JSON.stringify(named)} names no one symbol of the target\n`);
|
|
353
|
+
for (const candidate of candidates) {
|
|
354
|
+
err.write(` ${candidate}\n`);
|
|
355
|
+
}
|
|
356
|
+
if (candidates.length === 0) {
|
|
357
|
+
err.write(" no symbol of the target partially matches it either\n");
|
|
358
|
+
}
|
|
359
|
+
return EXIT_USAGE;
|
|
360
|
+
}
|
|
361
|
+
out.write(explanation(analysis, subject, config.reporters.cascade));
|
|
362
|
+
return EXIT_CLEAN;
|
|
363
|
+
};
|