@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.
Files changed (41) hide show
  1. package/README.md +13 -13
  2. package/dist/src/analysis.js +5 -3
  3. package/dist/src/config.js +6 -0
  4. package/dist/src/conformance-record.js +26 -0
  5. package/dist/src/conformance.js +8 -20
  6. package/dist/src/findings-pass.js +3 -5
  7. package/dist/src/report.js +14 -1
  8. package/dist/src/reporters/annotations.js +36 -0
  9. package/dist/src/reporters/fingerprint.js +61 -0
  10. package/dist/src/reporters/reporter.js +7 -1
  11. package/dist/src/reporters/reporters.js +18 -0
  12. package/dist/src/reporters/rules.js +178 -0
  13. package/dist/src/reporters/sarif.js +235 -0
  14. package/dist/src/reporters/sha256.js +101 -0
  15. package/dist/src/reporters/template.js +877 -0
  16. package/dist/src/run.js +4 -10
  17. package/dist/src/verbs/analyze.js +45 -7
  18. package/dist/src/verbs/describe.js +8 -12
  19. package/dist/src/verbs/explain.js +303 -0
  20. package/jsr.json +1 -1
  21. package/package.json +1 -1
  22. package/src/analysis.ts +14 -4
  23. package/src/config.ts +8 -0
  24. package/src/conformance-record.ts +29 -0
  25. package/src/conformance.ts +21 -23
  26. package/src/findings-pass.ts +5 -7
  27. package/src/report.ts +25 -3
  28. package/src/reporters/annotations.ts +67 -0
  29. package/src/reporters/fingerprint.ts +69 -0
  30. package/src/reporters/json.ts +2 -2
  31. package/src/reporters/reporter.ts +21 -2
  32. package/src/reporters/reporters.ts +18 -1
  33. package/src/reporters/rules.ts +195 -0
  34. package/src/reporters/sarif.ts +287 -0
  35. package/src/reporters/sha256.ts +120 -0
  36. package/src/reporters/template.ts +989 -0
  37. package/src/reporters/text.ts +2 -3
  38. package/src/run.ts +5 -11
  39. package/src/verbs/analyze.ts +57 -7
  40. package/src/verbs/describe.ts +9 -13
  41. package/src/verbs/explain.ts +363 -0
package/dist/src/run.js CHANGED
@@ -10,15 +10,13 @@ import { printRetainedVerb } from "./verbs/print-retained.js";
10
10
  import { printRootsVerb } from "./verbs/print-roots.js";
11
11
  import { ANALYZE_OPTIONS, analyzeVerb } from "./verbs/analyze.js";
12
12
  import { describeVerb } from "./verbs/describe.js";
13
+ import { EXPLAIN_OPTIONS, explainVerb } from "./verbs/explain.js";
13
14
  import { EXIT_FAILURE, EXIT_USAGE } from "./verbs/verb.js";
14
15
  import { versionVerb } from "./verbs/version.js";
15
- /**
16
- * Every verb this command answers, in the order the usage text lists them. A verb
17
- * with no `run` is listed, and invoking it is a usage error.
18
- */
16
+ /** Every verb this command answers, in the order the usage text lists them. */
19
17
  const VERBS = [
20
18
  { name: "analyze", run: analyzeVerb, options: ANALYZE_OPTIONS },
21
- { name: "explain" },
19
+ { name: "explain", run: explainVerb, options: EXPLAIN_OPTIONS },
22
20
  { name: "print-config", run: printConfigVerb },
23
21
  { name: "print-projects", run: printProjectsVerb },
24
22
  { name: "print-roots", run: printRootsVerb },
@@ -169,12 +167,8 @@ export function run(args, out, err, host, openClient = (collectTiming) => openEn
169
167
  }
170
168
  const name = args[0];
171
169
  const verb = VERBS.find((listed) => listed.name === name);
172
- if (verb?.run !== undefined) {
173
- return invoke(verb.run, verb.options ?? NO_OPTIONS, args.slice(1), { out, err, host }, openClient);
174
- }
175
170
  if (verb !== undefined) {
176
- err.write(`deadset-ts: ${verb.name} is not implemented\n`);
177
- return EXIT_USAGE;
171
+ return invoke(verb.run, verb.options ?? NO_OPTIONS, args.slice(1), { out, err, host }, openClient);
178
172
  }
179
173
  if (name !== undefined) {
180
174
  err.write(`deadset-ts: unknown verb ${JSON.stringify(name)}\n`);
@@ -1,19 +1,20 @@
1
1
  import { runAnalysis } from "../analysis.js";
2
2
  import { ConfigError } from "../config.js";
3
- import { RECORDED_CONFORMANCE, reportedConformance } from "../conformance.js";
3
+ import { CONFORMANCE, DECLARED_GAPS } from "../conformance.js";
4
4
  import { analyzerProvenance, recordedFindings, verdictOf } from "../findings-pass.js";
5
5
  import { packageScope } from "../inventory.js";
6
- import { normalizePath, relativePath, resolvePath } from "../paths.js";
6
+ import { joinPath, normalizePath, relativePath, resolvePath } from "../paths.js";
7
7
  import { buildReport, capFindings, sortFindings, } from "../report.js";
8
8
  import { json } from "../reporters/json.js";
9
9
  import { RENDERINGS } from "../reporters/reporters.js";
10
+ import { parseTemplate, TemplateError } from "../reporters/template.js";
10
11
  import { resolve } from "../resolve.js";
11
12
  import { writeBaseline } from "../suppress-file.js";
12
13
  import { CONTRACT_VERSION } from "../version.js";
13
14
  import { EXIT_CLEAN, EXIT_FAILURE } from "./verb.js";
14
15
  /** The options `analyze` takes beside the ones every verb takes. */
15
16
  export const ANALYZE_OPTIONS = {
16
- plain: ["report", "baseline-write", "exit-code"],
17
+ plain: ["report", "baseline-write", "exit-code", "template"],
17
18
  repeatable: ["format"],
18
19
  };
19
20
  /** The two values `--exit-code` takes; on, the default, puts the run's verdict in the exit code. */
@@ -80,12 +81,42 @@ function identityOf(module, host) {
80
81
  function counted(n, noun) {
81
82
  return `${String(n)} ${noun}${n === 1 ? "" : "s"}`;
82
83
  }
84
+ /**
85
+ * The template `--template` names, read and parsed before any analysis, so a template that
86
+ * cannot be read or does not parse refuses the invocation. The template format with no
87
+ * template named is refused the same way.
88
+ */
89
+ function templateOf(host, invoked, named, formats) {
90
+ if (named === undefined || named === "") {
91
+ if (formats.includes("template")) {
92
+ throw new ConfigError("malformed", "", "the template format renders the template --template names, and none was named");
93
+ }
94
+ return undefined;
95
+ }
96
+ let source;
97
+ try {
98
+ source = host.readFile(resolvePath(invoked, named));
99
+ }
100
+ catch (error) {
101
+ throw new ConfigError("malformed", "", `--template=${named}: ${error instanceof Error ? error.message : String(error)}`);
102
+ }
103
+ try {
104
+ return parseTemplate(source);
105
+ }
106
+ catch (error) {
107
+ if (!(error instanceof TemplateError)) {
108
+ throw error;
109
+ }
110
+ throw new ConfigError("malformed", "", `--template=${named}: ${error.message}`);
111
+ }
112
+ }
83
113
  /**
84
114
  * Analyzes the scope under the configuration and writes the JSON report to the path
85
115
  * `--report` names, each rendering `--format` names beside it at the report's path with the
86
116
  * format's suffix appended, and a baseline of every finding where `--baseline-write` names a
87
- * path. Nothing is written to the output stream; diagnostics go to the error stream and the
88
- * verdict is the exit code, or 0 under `--exit-code=off`.
117
+ * path. The template format renders the template `--template` names. Nothing is written to
118
+ * the output stream; diagnostics go to the error stream and the verdict is the exit code, or 0
119
+ * under `--exit-code=off`, and a rendering that cannot be written ends the run with 3.
89
120
  */
90
121
  export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repeated }) => {
91
122
  const reportPath = option("report");
@@ -108,6 +139,7 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
108
139
  formats: formatsOf(repeated("format"), config.reporters.formats),
109
140
  baseline: baseline === undefined ? undefined : resolvePath(invoked, baseline),
110
141
  };
142
+ const template = templateOf(host, invoked, option("template"), documents.formats);
111
143
  const scoped = scope();
112
144
  const targetRoot = scoped.target.path;
113
145
  let version;
@@ -129,7 +161,8 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
129
161
  const built = buildReport({
130
162
  contractVersion: CONTRACT_VERSION,
131
163
  version,
132
- conformance: reportedConformance(RECORDED_CONFORMANCE),
164
+ conformance: CONFORMANCE,
165
+ declaredGaps: DECLARED_GAPS,
133
166
  target: { kind, root, identity: identityOf(scoped.target, host) },
134
167
  configurations: configurationsOf(run.projects, targetRoot),
135
168
  unavailable: scoped.consumers.map((consumer) => ({
@@ -154,10 +187,15 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
154
187
  }
155
188
  try {
156
189
  host.writeFile(documents.report, json(report));
190
+ const options = {
191
+ failOn: config.reporters.failOn,
192
+ readSource: (path) => host.readFile(joinPath(targetRoot, path)),
193
+ template,
194
+ };
157
195
  for (const format of documents.formats) {
158
196
  const rendering = RENDERINGS.get(format);
159
197
  if (rendering !== undefined) {
160
- host.writeFile(documents.report + rendering.suffix, rendering.render(report));
198
+ host.writeFile(documents.report + rendering.suffix, rendering.render(report, options));
161
199
  }
162
200
  }
163
201
  if (documents.baseline !== undefined) {
@@ -1,11 +1,11 @@
1
- import { RECORDED_CONFORMANCE } from "../conformance.js";
1
+ import { CONFORMANCE } from "../conformance.js";
2
2
  import { ANALYZER_NAME, LANGUAGE, SCHEMA_VERSION } from "../report.js";
3
3
  import { CONTRACT_VERSION } from "../version.js";
4
4
  import { EXIT_CLEAN, EXIT_FAILURE, EXIT_USAGE } from "./verb.js";
5
5
  /**
6
6
  * The document `describe` writes: the analyzer, the Contract version it implements, the
7
7
  * report schema versions it reads, the languages it claims, and its recorded result over
8
- * the conformance corpus, absent where it records none.
8
+ * the conformance corpus.
9
9
  */
10
10
  export function describeDocument(version, recorded) {
11
11
  const document = {
@@ -14,15 +14,11 @@ export function describeDocument(version, recorded) {
14
14
  contract_version: CONTRACT_VERSION,
15
15
  schema_versions_accepted: [SCHEMA_VERSION],
16
16
  languages: [LANGUAGE],
17
- ...(recorded === undefined
18
- ? {}
19
- : {
20
- conformance: {
21
- corpus_version: recorded.corpusVersion,
22
- result: recorded.result,
23
- digest: recorded.digest,
24
- },
25
- }),
17
+ conformance: {
18
+ corpus_version: recorded.corpusVersion,
19
+ result: recorded.result,
20
+ digest: recorded.digest,
21
+ },
26
22
  };
27
23
  return `${JSON.stringify(document, null, 2)}\n`;
28
24
  }
@@ -40,6 +36,6 @@ export const describeVerb = ({ out, err, host, args }) => {
40
36
  err.write(`deadset-ts: describe: ${error instanceof Error ? error.message : String(error)}\n`);
41
37
  return EXIT_FAILURE;
42
38
  }
43
- out.write(describeDocument(version, RECORDED_CONFORMANCE));
39
+ out.write(describeDocument(version, CONFORMANCE));
44
40
  return EXIT_CLEAN;
45
41
  };
@@ -0,0 +1,303 @@
1
+ import { runAnalysis } from "../analysis.js";
2
+ import { listing } from "../components.js";
3
+ import { ConfigError } from "../config.js";
4
+ import { OUTSIDE, swept as judged } from "../graph.js";
5
+ import { positionKey } from "../position.js";
6
+ import { resolve } from "../resolve.js";
7
+ import { EXIT_CLEAN, EXIT_USAGE } from "./verb.js";
8
+ /**
9
+ * The three spellings of an explanation request, each naming the symbol to explain. They are
10
+ * one request, because exactly one state holds for a symbol and the answer is that state.
11
+ */
12
+ const WHY_OPTIONS = ["why", "why-live", "why-not"];
13
+ /** The options `explain` takes beside the ones every verb takes. */
14
+ export const EXPLAIN_OPTIONS = { plain: [...WHY_OPTIONS], repeatable: [] };
15
+ /** The most partial matches a refused request prints. */
16
+ const MAX_PARTIAL_MATCHES = 20;
17
+ /** A count with its noun, so a line reads for one as well as for several. */
18
+ function counted(n, noun) {
19
+ return `${String(n)} ${noun}${n === 1 ? "" : "s"}`;
20
+ }
21
+ /** A list for a line a maintainer reads, which says none rather than leaving the field blank. */
22
+ function listed(items) {
23
+ return items.length === 0 ? "none" : items.join(" ");
24
+ }
25
+ /** The one symbol a request names, from whichever of the three options names it. */
26
+ function namedSymbol(option) {
27
+ const named = WHY_OPTIONS.flatMap((name) => {
28
+ const value = option(name);
29
+ return value === undefined || value === "" ? [] : [{ name, value }];
30
+ });
31
+ const [only, ...more] = named;
32
+ if (only === undefined) {
33
+ throw new ConfigError("malformed", "", "explain explains the symbol --why, --why-live or --why-not names, and none was named");
34
+ }
35
+ if (more.length > 0) {
36
+ throw new ConfigError("malformed", "", `explain explains one symbol, and ${named.map((one) => `--${one.name}`).join(", ")} each name one`);
37
+ }
38
+ return only.value;
39
+ }
40
+ /** The part of a reference after its separator: the `Type.member` or bare name a maintainer types. */
41
+ function fragmentOf(symbol) {
42
+ const at = symbol.ref.indexOf("#");
43
+ return at < 0 ? "" : symbol.ref.slice(at + 1);
44
+ }
45
+ function described(symbol) {
46
+ return `${symbol.ref}\t${positionKey(symbol.position)}`;
47
+ }
48
+ /**
49
+ * The one declaration a request names, or the candidates where it names none or several.
50
+ * The reference, the fragment of it a maintainer types and the display name are tried in
51
+ * order, each an exact match; a key several declarations answer is no answer.
52
+ */
53
+ function subjectOf(symbols, named) {
54
+ const keys = [
55
+ (symbol) => symbol.ref,
56
+ fragmentOf,
57
+ (symbol) => symbol.name,
58
+ ];
59
+ for (const key of keys) {
60
+ const matched = symbols.filter((symbol) => key(symbol) === named);
61
+ const [only] = matched;
62
+ if (only !== undefined && matched.length === 1) {
63
+ return { subject: only, candidates: [] };
64
+ }
65
+ if (matched.length > 1) {
66
+ return { candidates: matched.map(described) };
67
+ }
68
+ }
69
+ const folded = named.toLowerCase();
70
+ const partial = symbols
71
+ .filter((symbol) => symbol.ref.toLowerCase().includes(folded) || symbol.name.toLowerCase().includes(folded))
72
+ .map(described)
73
+ .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
74
+ return { candidates: partial.slice(0, MAX_PARTIAL_MATCHES) };
75
+ }
76
+ /** Every reference of the run once, a reference several configurations make counted once. */
77
+ function referencesOf(configured) {
78
+ const seen = new Set();
79
+ const held = [];
80
+ for (const reference of configured.flatMap((one) => one.references)) {
81
+ const key = [reference.from, reference.to, positionKey(reference.position), reference.use].join("\u0000");
82
+ if (!seen.has(key)) {
83
+ seen.add(key);
84
+ held.push(reference);
85
+ }
86
+ }
87
+ return held;
88
+ }
89
+ /** Every root of the run once per reason, in the order the projects name them. */
90
+ function rootsOf(configured) {
91
+ const seen = new Set();
92
+ return configured
93
+ .flatMap((one) => one.roots)
94
+ .filter((root) => {
95
+ const key = [root.id, root.kind, root.source].join("\u0000");
96
+ if (seen.has(key)) {
97
+ return false;
98
+ }
99
+ seen.add(key);
100
+ return true;
101
+ });
102
+ }
103
+ /**
104
+ * The shortest path from a root to one declaration, walked breadth first from every root at
105
+ * once in the order the run names them, following the references the sweep follows: a
106
+ * reference that reads a module reaches what the module exports, and an evaluation or a root
107
+ * reaches the module alone. Under `production` no reference a test file made is followed,
108
+ * which is the set the report's sweep counts.
109
+ */
110
+ function shortestPath(analysis, to, production) {
111
+ const graph = analysis.swept.matrix.union;
112
+ const out = new Map();
113
+ for (const reference of referencesOf(analysis.configured)) {
114
+ if (production && reference.test) {
115
+ continue;
116
+ }
117
+ const held = out.get(reference.from);
118
+ if (held === undefined) {
119
+ out.set(reference.from, [reference]);
120
+ }
121
+ else {
122
+ held.push(reference);
123
+ }
124
+ }
125
+ const steps = [];
126
+ const seen = new Set();
127
+ const expanded = new Set();
128
+ const enter = (step) => {
129
+ if (!seen.has(step.at)) {
130
+ seen.add(step.at);
131
+ steps.push(step);
132
+ }
133
+ };
134
+ for (const root of rootsOf(analysis.configured)) {
135
+ enter({ at: root.id, hop: undefined, previous: -1, root });
136
+ }
137
+ for (let head = 0; head < steps.length; head += 1) {
138
+ const step = steps[head];
139
+ if (step === undefined) {
140
+ break;
141
+ }
142
+ if (step.at === to) {
143
+ const path = [];
144
+ for (let at = step; at?.hop !== undefined; at = steps[at.previous]) {
145
+ path.unshift(at.hop);
146
+ }
147
+ return { root: step.root, path };
148
+ }
149
+ for (const reference of out.get(step.at) ?? []) {
150
+ enter({
151
+ at: reference.to,
152
+ hop: { k: "reference", reference },
153
+ previous: head,
154
+ root: step.root,
155
+ });
156
+ const target = graph.at(reference.to);
157
+ if (reference.use === "evaluation" || target === OUTSIDE || expanded.has(reference.to)) {
158
+ continue;
159
+ }
160
+ expanded.add(reference.to);
161
+ for (const exported of graph.exportsOf[target] ?? []) {
162
+ const id = graph.symbols[exported]?.id;
163
+ if (id !== undefined) {
164
+ enter({
165
+ at: id,
166
+ hop: { k: "export", reference, to: id },
167
+ previous: head,
168
+ root: step.root,
169
+ });
170
+ }
171
+ }
172
+ }
173
+ }
174
+ return undefined;
175
+ }
176
+ function referenceLine(reference, tail) {
177
+ return ` reference: ${positionKey(reference.position)}\t${reference.use}\t${tail}${reference.test ? "\tfrom a test file" : ""}\n`;
178
+ }
179
+ /** Why a declaration no finding names and no exemption held back is live, or how it fell. */
180
+ function liveness(analysis, subject, cascade) {
181
+ const { sweep } = analysis.swept;
182
+ const candidate = sweep.candidates.find((one) => one.id === subject.id);
183
+ if (candidate === undefined && !judged(subject.kind)) {
184
+ return `answer: unjudged\n unjudged: neither liveness relation judges a ${subject.kind}\n`;
185
+ }
186
+ let text = `answer: ${candidate === undefined ? "live" : "dead"}\n`;
187
+ text += ` live under: ${listed(sweep.liveUnder.get(subject.id) ?? [])}\n`;
188
+ for (const root of rootsOf(analysis.configured).filter((one) => one.id === subject.id)) {
189
+ text += ` root: ${root.kind}${root.source === "" ? "" : `\tnamed by ${root.source}`}\n`;
190
+ }
191
+ const found = shortestPath(analysis, subject.id, true) ?? shortestPath(analysis, subject.id, false);
192
+ if (found !== undefined && found.path.length > 0) {
193
+ text += ` reached from: root ${found.root.kind} ${found.root.id}\n`;
194
+ for (const hop of found.path) {
195
+ text += referenceLine(hop.reference, `${hop.reference.from} -> ${hop.reference.to}`);
196
+ if (hop.k === "export") {
197
+ text += ` export: ${hop.reference.to} -> ${hop.to}\n`;
198
+ }
199
+ }
200
+ }
201
+ else if (found === undefined) {
202
+ const references = referencesOf(analysis.configured).filter((one) => one.to === subject.id);
203
+ text += " unreachable: no path of references reaches it from a root\n";
204
+ text += ` references: ${String(references.length)}\n`;
205
+ for (const reference of references) {
206
+ text += referenceLine(reference, `from ${reference.from}`);
207
+ }
208
+ }
209
+ if (candidate !== undefined) {
210
+ const component = sweep.components.find((one) => one.members.includes(subject.id));
211
+ if (component !== undefined) {
212
+ const listed = listing(component, cascade);
213
+ text += ` component: ${component.id}, ${counted(listed.symbolCount, "symbol")} and ${counted(listed.deletableLines, "deletable line")} fall with it, listed at cascade ${cascade}\n`;
214
+ for (const root of listed.roots) {
215
+ text += ` component root: ${root}\n`;
216
+ }
217
+ for (const member of listed.members) {
218
+ text += ` component member: ${member}\n`;
219
+ }
220
+ }
221
+ text += ` candidate: dead under ${candidate.relation}, with ${String(candidate.productionRefs)} production and ${String(candidate.testRefs)} test references\n`;
222
+ }
223
+ return text;
224
+ }
225
+ function reported(found) {
226
+ return [
227
+ `answer: reported`,
228
+ ` code: ${found.code} ${found.kind}`,
229
+ ` message: ${found.message}`,
230
+ ` class: ${found.reachabilityClass}`,
231
+ ` confidence: ${found.confidence}`,
232
+ ...(found.livenessRelation === undefined
233
+ ? []
234
+ : [` liveness relation: ${found.livenessRelation}`]),
235
+ ` configurations: ${listed(found.configurations)}`,
236
+ ` loaded consumers: ${listed(found.consumersLoaded)}`,
237
+ ...found.retainedBy.map((one) => ` retained by: ${one}`),
238
+ "",
239
+ ].join("\n");
240
+ }
241
+ /**
242
+ * Every finding of the run that names the declaration by its reference and reports at
243
+ * another position: a part of it, or a suppression record naming it. Each is published under
244
+ * the reference asked about, so an explanation leaving it out would disagree with the report.
245
+ */
246
+ function namedElsewhere(findings, subject) {
247
+ return findings
248
+ .filter((found) => found.symbol.ref === subject.ref && positionKey(found.position) !== subject.id)
249
+ .map((found) => `also reported: ${found.code} ${found.kind}\t${positionKey(found.position)}\t${found.symbol.kind} ${found.symbol.name}\n`)
250
+ .join("");
251
+ }
252
+ /** The one explanation that applies to a declaration, built whole before it is written. */
253
+ function explanation(analysis, subject, cascade) {
254
+ const graph = analysis.swept.matrix.union;
255
+ const at = graph.at(subject.id);
256
+ let text = `symbol: ${subject.ref}\n`;
257
+ text += `declaration: ${subject.kind} ${subject.name}\n`;
258
+ text += `position: ${subject.id}\n`;
259
+ text += `configurations: ${listed(analysis.swept.matrix.heldIn[at] ?? [])}\n`;
260
+ const found = analysis.result.findings.find((one) => positionKey(one.position) === subject.id);
261
+ const retained = analysis.swept.retained.filter((one) => one.id === subject.id);
262
+ if (found !== undefined) {
263
+ text += reported(found);
264
+ }
265
+ else if (retained.length > 0) {
266
+ text += `answer: retained\n held back by: ${counted(retained.length, "exemption class")}\n`;
267
+ for (const one of retained) {
268
+ text += ` class: ${one.class}\t${positionKey(one.site)}\t${one.detail}\n`;
269
+ }
270
+ }
271
+ else {
272
+ text += liveness(analysis, subject, cascade);
273
+ }
274
+ return text + namedElsewhere(analysis.result.findings, subject);
275
+ }
276
+ /**
277
+ * Explains one symbol from the analysis the report is built from, so an explanation cannot
278
+ * disagree with a finding: why a reported symbol is reported, which exemption classes held a
279
+ * retained one back, and for any other declaration the relations that hold it live and the
280
+ * shortest path of references from a root, or the references it has where no path reaches it.
281
+ * A request naming no one declaration exits with the usage code and names what partially
282
+ * matches it.
283
+ */
284
+ export const explainVerb = ({ out, err, host, inputs, scope, openClient, option }) => {
285
+ const named = namedSymbol(option);
286
+ const { config, provenance } = resolve(inputs());
287
+ const analysis = runAnalysis(openClient(false), host, scope(), config, provenance, {
288
+ production: true,
289
+ });
290
+ const { subject, candidates } = subjectOf(analysis.swept.matrix.union.symbols, named);
291
+ if (subject === undefined) {
292
+ err.write(`deadset-ts: ${JSON.stringify(named)} names no one symbol of the target\n`);
293
+ for (const candidate of candidates) {
294
+ err.write(` ${candidate}\n`);
295
+ }
296
+ if (candidates.length === 0) {
297
+ err.write(" no symbol of the target partially matches it either\n");
298
+ }
299
+ return EXIT_USAGE;
300
+ }
301
+ out.write(explanation(analysis, subject, config.reporters.cascade));
302
+ return EXIT_CLEAN;
303
+ };
package/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "3.9.1",
3
+ "version": "3.11.0",
4
4
  "license": "GPL-3.0-or-later",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "3.9.1",
3
+ "version": "3.11.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Deterministic dead-code analysis for TypeScript, class and type members included",
package/src/analysis.ts CHANGED
@@ -483,7 +483,11 @@ function runAnalysisInputs(
483
483
  provenance: Provenance,
484
484
  mode: Mode,
485
485
  detectors: Detectors,
486
- ): { readonly inputs: AnalysisInputs; readonly run: RunFacts } {
486
+ ): {
487
+ readonly inputs: AnalysisInputs;
488
+ readonly run: RunFacts;
489
+ readonly configured: readonly Configured[];
490
+ } {
487
491
  const targetRoot = scope.target.path;
488
492
  const read = readEmitterRun(engine, host, scope, config, mode, detectors);
489
493
  const symbols = read.matrix.union.symbols;
@@ -523,6 +527,7 @@ function runAnalysisInputs(
523
527
  read.projects.flatMap((one) => one.configured.testFiles),
524
528
  ),
525
529
  },
530
+ configured: read.projects.map((one) => one.configured),
526
531
  };
527
532
  }
528
533
 
@@ -545,12 +550,17 @@ export interface RunFacts {
545
550
  export interface RunAnalysis {
546
551
  readonly result: PassResult;
547
552
  readonly run: RunFacts;
553
+ /** The sweep the findings were decided over, under the mark of every bound record. */
554
+ readonly swept: RunSweep;
555
+ /** What each project answered, in the run's order, which the sweep was built from. */
556
+ readonly configured: readonly Configured[];
548
557
  }
549
558
 
550
559
  /**
551
560
  * The analysis of the run the scope and the configuration describe: the run read with its
552
561
  * suppressions bound, every family's findings with the suppressions applied and the
553
- * declared edges evaluated, and the projects and test-file rules the report names.
562
+ * declared edges evaluated, the projects and test-file rules the report names, and the sweep
563
+ * an explanation reads.
554
564
  */
555
565
  export function runAnalysis(
556
566
  engine: Engine,
@@ -561,7 +571,7 @@ export function runAnalysis(
561
571
  mode: Mode,
562
572
  detectors: Detectors = DETECTORS,
563
573
  ): RunAnalysis {
564
- const { inputs, run } = runAnalysisInputs(
574
+ const { inputs, run, configured } = runAnalysisInputs(
565
575
  engine,
566
576
  host,
567
577
  scope,
@@ -570,5 +580,5 @@ export function runAnalysis(
570
580
  mode,
571
581
  detectors,
572
582
  );
573
- return { result: findingsPass(inputs), run };
583
+ return { result: findingsPass(inputs), run, swept: inputs.marked.swept, configured };
574
584
  }
package/src/config.ts CHANGED
@@ -28,6 +28,14 @@ export type Language = "go" | "ts";
28
28
  /** What a finding of one issue kind does to a run. */
29
29
  export type Severity = "allow" | "warn" | "deny";
30
30
 
31
+ /** The severities ranked, the strongest highest. */
32
+ const SEVERITY_RANK: Readonly<Record<Severity, number>> = { allow: 0, warn: 1, deny: 2 };
33
+
34
+ /** Whether a finding at one severity fails a run under the failing severity. */
35
+ export function fails(severity: Severity, failOn: Severity): boolean {
36
+ return SEVERITY_RANK[severity] >= SEVERITY_RANK[failOn];
37
+ }
38
+
31
39
  /** One rendering of the finding list. */
32
40
  export type Format = "text" | "json" | "github" | "sarif" | "template";
33
41
 
@@ -0,0 +1,29 @@
1
+ // Written by the conformance run from conformance.json and conformance-results.json.
2
+ // Regenerate it with `UPDATE_GOLDEN=1 npx vitest --run src/conformance.corpus.test.ts`.
3
+ export const RECORD = {
4
+ conformance: {
5
+ corpusVersion: "1.9.0",
6
+ result: "fail",
7
+ digest: "sha256:8c29f19fc189f4f5d9ea210d556b62d12c3701c221eb7a8adde499b7060bec0d",
8
+ },
9
+ gaps: [
10
+ {
11
+ fixture: "private-member-unread",
12
+ capability: "reflective-lookup",
13
+ reason:
14
+ "The reference pass resolves a string-literal element access to the member it names, so the member is live by that reference and the retained listing names no class for it.",
15
+ },
16
+ {
17
+ fixture: "redundant-export-keyword",
18
+ capability: "DS1104",
19
+ reason:
20
+ "No consumer is loaded beside the target, so the published surface is unknown and no export is narrowed; review an export only the target uses by hand.",
21
+ },
22
+ {
23
+ fixture: "unused-exported-consumer",
24
+ capability: "DS1001",
25
+ reason:
26
+ "No consumer is loaded beside the target, so an export nothing in the target references is reported at the possible class rather than the certain one.",
27
+ },
28
+ ],
29
+ } as const;
@@ -1,34 +1,32 @@
1
1
  /**
2
- * What this analyzer states about its own conformance: the corpus version it is written
3
- * against and its recorded result over that corpus, which `describe` and every report name.
2
+ * What this analyzer states about its own conformance: its recorded result over the
3
+ * conformance corpus and every capability it declines, which `describe` and every report
4
+ * name.
4
5
  */
6
+ import { RECORD } from "./conformance-record.ts";
5
7
 
6
- /** The result one product reached over the conformance corpus. */
8
+ /**
9
+ * The result one product reached over the conformance corpus. A declared gap never lowers
10
+ * it: the result is `pass` when no fixture fails, gaps or not, and `fail` otherwise.
11
+ */
7
12
  export interface Conformance {
8
13
  readonly corpusVersion: string;
9
14
  readonly result: "pass" | "fail";
10
- /** `sha256:` and the lowercase hexadecimal SHA-256 of the results document the runner wrote. */
15
+ /** `sha256:` and the lowercase hexadecimal SHA-256 of the results document the run wrote. */
11
16
  readonly digest: string;
12
17
  }
13
18
 
14
- /** The version of the conformance corpus this analyzer is written against. */
15
- const CORPUS_VERSION = "1.9.0";
19
+ /** One capability this analyzer declines, as `conformance.json` declares it. */
20
+ export interface DeclaredGap {
21
+ readonly fixture: string;
22
+ /** The one expectation the gap covers, where it is narrower than the fixture. */
23
+ readonly symbol?: string;
24
+ readonly capability: string;
25
+ readonly reason: string;
26
+ }
16
27
 
17
- /** The SHA-256 of zero bytes, which is the digest of a results document no run wrote. */
18
- const EMPTY_DIGEST = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
28
+ /** The conformance run this analyzer records. */
29
+ export const CONFORMANCE: Conformance = RECORD.conformance;
19
30
 
20
- /**
21
- * The conformance run this analyzer records, or undefined where it records none. A
22
- * description states exactly this, so a reader that requires a pass refuses an analyzer
23
- * that has not answered the corpus.
24
- */
25
- export const RECORDED_CONFORMANCE: Conformance | undefined = undefined;
26
-
27
- /**
28
- * The conformance a report states. A report requires the block, so an analyzer that
29
- * records no run states a failed one over the results document no run wrote, which a merge
30
- * refuses as it refuses any result other than a pass.
31
- */
32
- export function reportedConformance(recorded: Conformance | undefined): Conformance {
33
- return recorded ?? { corpusVersion: CORPUS_VERSION, result: "fail", digest: EMPTY_DIGEST };
34
- }
31
+ /** Every capability this analyzer declines, in the order `conformance.json` declares them. */
32
+ export const DECLARED_GAPS: readonly DeclaredGap[] = RECORD.gaps;