@cplieger/deadset-ts 3.9.1 → 3.10.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.
@@ -0,0 +1,26 @@
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: "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.",
14
+ },
15
+ {
16
+ fixture: "redundant-export-keyword",
17
+ capability: "DS1104",
18
+ reason: "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.",
19
+ },
20
+ {
21
+ fixture: "unused-exported-consumer",
22
+ capability: "DS1001",
23
+ reason: "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.",
24
+ },
25
+ ],
26
+ };
@@ -1,22 +1,10 @@
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
  */
5
- /** The version of the conformance corpus this analyzer is written against. */
6
- const CORPUS_VERSION = "1.9.0";
7
- /** The SHA-256 of zero bytes, which is the digest of a results document no run wrote. */
8
- const EMPTY_DIGEST = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
9
- /**
10
- * The conformance run this analyzer records, or undefined where it records none. A
11
- * description states exactly this, so a reader that requires a pass refuses an analyzer
12
- * that has not answered the corpus.
13
- */
14
- export const RECORDED_CONFORMANCE = undefined;
15
- /**
16
- * The conformance a report states. A report requires the block, so an analyzer that
17
- * records no run states a failed one over the results document no run wrote, which a merge
18
- * refuses as it refuses any result other than a pass.
19
- */
20
- export function reportedConformance(recorded) {
21
- return recorded ?? { corpusVersion: CORPUS_VERSION, result: "fail", digest: EMPTY_DIGEST };
22
- }
6
+ import { RECORD } from "./conformance-record.js";
7
+ /** The conformance run this analyzer records. */
8
+ export const CONFORMANCE = RECORD.conformance;
9
+ /** Every capability this analyzer declines, in the order `conformance.json` declares them. */
10
+ export const DECLARED_GAPS = RECORD.gaps;
@@ -133,6 +133,15 @@ function canonicalStale(a, b) {
133
133
  compare(a.code, b.code) ||
134
134
  compare(a.symbol, b.symbol));
135
135
  }
136
+ /** One declared gap as a report carries it, its members in the schema's order. */
137
+ function wireGap(gap) {
138
+ return {
139
+ fixture: gap.fixture,
140
+ ...(gap.symbol === undefined ? {} : { symbol: gap.symbol }),
141
+ capability: gap.capability,
142
+ reason: gap.reason,
143
+ };
144
+ }
136
145
  /** Edge evaluations by edge, then side, then their compact encodings. */
137
146
  function byEdge(a, b) {
138
147
  return (compare(a.edge, b.edge) ||
@@ -202,7 +211,11 @@ export function buildReport(input) {
202
211
  findings,
203
212
  edge_evaluations: input.result.edgeEvaluations.map(wireEvaluation).sort(byEdge),
204
213
  stale_suppressions: input.result.staleSuppressions.map(wireStale).sort(canonicalStale),
205
- declared_gaps: [],
214
+ // Every gap shares the one component of the canonical key a gap supplies, the analyzer's
215
+ // name, so the compact encodings alone order them.
216
+ declared_gaps: input.declaredGaps
217
+ .map(wireGap)
218
+ .sort((a, b) => compare(JSON.stringify(a), JSON.stringify(b))),
206
219
  excluded_by_cgo: [],
207
220
  test_file_rules: [...input.testFileRules]
208
221
  .sort((a, b) => compare(a.rule, b.rule) || a.matched - b.matched)
@@ -1,6 +1,6 @@
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
6
  import { normalizePath, relativePath, resolvePath } from "../paths.js";
@@ -129,7 +129,8 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
129
129
  const built = buildReport({
130
130
  contractVersion: CONTRACT_VERSION,
131
131
  version,
132
- conformance: reportedConformance(RECORDED_CONFORMANCE),
132
+ conformance: CONFORMANCE,
133
+ declaredGaps: DECLARED_GAPS,
133
134
  target: { kind, root, identity: identityOf(scoped.target, host) },
134
135
  configurations: configurationsOf(run.projects, targetRoot),
135
136
  unavailable: scoped.consumers.map((consumer) => ({
@@ -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
  };
package/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "3.9.1",
3
+ "version": "3.10.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.10.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Deterministic dead-code analysis for TypeScript, class and type members included",
@@ -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;
package/src/report.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import type { Severity, Sort, TargetKind } from "./config.ts";
8
- import type { Conformance } from "./conformance.ts";
8
+ import type { Conformance, DeclaredGap } from "./conformance.ts";
9
9
  import type { EdgeEvaluation } from "./edges.ts";
10
10
  import type {
11
11
  CompletedFinding,
@@ -105,6 +105,13 @@ export interface WireStaleSuppression {
105
105
  readonly message: string;
106
106
  }
107
107
 
108
+ interface WireDeclaredGap {
109
+ readonly fixture: string;
110
+ readonly symbol?: string;
111
+ readonly capability: string;
112
+ readonly reason: string;
113
+ }
114
+
108
115
  export interface WireTotals {
109
116
  readonly findings: number;
110
117
  readonly by_severity: { readonly allow: number; readonly warn: number; readonly deny: number };
@@ -159,7 +166,7 @@ export interface Report {
159
166
  readonly findings: readonly WireFinding[];
160
167
  readonly edge_evaluations: readonly WireEdgeEvaluation[];
161
168
  readonly stale_suppressions: readonly WireStaleSuppression[];
162
- readonly declared_gaps: readonly never[];
169
+ readonly declared_gaps: readonly WireDeclaredGap[];
163
170
  readonly excluded_by_cgo: readonly never[];
164
171
  readonly test_file_rules: readonly TestFileRule[];
165
172
  readonly totals: WireTotals;
@@ -170,6 +177,7 @@ export interface ReportInput {
170
177
  readonly contractVersion: string;
171
178
  readonly version: string;
172
179
  readonly conformance: Conformance;
180
+ readonly declaredGaps: readonly DeclaredGap[];
173
181
  readonly target: { readonly kind: TargetKind; readonly root: string; readonly identity: string };
174
182
  readonly configurations: readonly ReportConfiguration[];
175
183
  readonly unavailable: readonly UnavailableConsumer[];
@@ -318,6 +326,16 @@ function canonicalStale(a: WireStaleSuppression, b: WireStaleSuppression): numbe
318
326
  );
319
327
  }
320
328
 
329
+ /** One declared gap as a report carries it, its members in the schema's order. */
330
+ function wireGap(gap: DeclaredGap): WireDeclaredGap {
331
+ return {
332
+ fixture: gap.fixture,
333
+ ...(gap.symbol === undefined ? {} : { symbol: gap.symbol }),
334
+ capability: gap.capability,
335
+ reason: gap.reason,
336
+ };
337
+ }
338
+
321
339
  /** Edge evaluations by edge, then side, then their compact encodings. */
322
340
  function byEdge(a: WireEdgeEvaluation, b: WireEdgeEvaluation): number {
323
341
  return (
@@ -392,7 +410,11 @@ export function buildReport(input: ReportInput): Report {
392
410
  findings,
393
411
  edge_evaluations: input.result.edgeEvaluations.map(wireEvaluation).sort(byEdge),
394
412
  stale_suppressions: input.result.staleSuppressions.map(wireStale).sort(canonicalStale),
395
- declared_gaps: [],
413
+ // Every gap shares the one component of the canonical key a gap supplies, the analyzer's
414
+ // name, so the compact encodings alone order them.
415
+ declared_gaps: input.declaredGaps
416
+ .map(wireGap)
417
+ .sort((a, b) => compare(JSON.stringify(a), JSON.stringify(b))),
396
418
  excluded_by_cgo: [],
397
419
  test_file_rules: [...input.testFileRules]
398
420
  .sort((a, b) => compare(a.rule, b.rule) || a.matched - b.matched)
@@ -1,6 +1,6 @@
1
1
  import { runAnalysis, type RunProject } from "../analysis.ts";
2
2
  import { ConfigError, type Format } from "../config.ts";
3
- import { RECORDED_CONFORMANCE, reportedConformance } from "../conformance.ts";
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
6
  import { normalizePath, relativePath, resolvePath } from "../paths.ts";
@@ -175,7 +175,8 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
175
175
  const built = buildReport({
176
176
  contractVersion: CONTRACT_VERSION,
177
177
  version,
178
- conformance: reportedConformance(RECORDED_CONFORMANCE),
178
+ conformance: CONFORMANCE,
179
+ declaredGaps: DECLARED_GAPS,
179
180
  target: { kind, root, identity: identityOf(scoped.target, host) },
180
181
  configurations: configurationsOf(run.projects, targetRoot),
181
182
  unavailable: scoped.consumers.map((consumer) => ({
@@ -1,4 +1,4 @@
1
- import { RECORDED_CONFORMANCE, type Conformance } from "../conformance.ts";
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, absent where it records none.
9
+ * the conformance corpus.
10
10
  */
11
- export function describeDocument(version: string, recorded: Conformance | undefined): string {
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
- ...(recorded === undefined
19
- ? {}
20
- : {
21
- conformance: {
22
- corpus_version: recorded.corpusVersion,
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, RECORDED_CONFORMANCE));
40
+ out.write(describeDocument(version, CONFORMANCE));
45
41
  return EXIT_CLEAN;
46
42
  };