@cplieger/deadset-ts 5.2.3 → 6.0.1

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 (84) hide show
  1. package/README.md +1 -1
  2. package/dist/src/analysis.js +28 -5
  3. package/dist/src/augmentations.js +50 -0
  4. package/dist/src/components.js +15 -15
  5. package/dist/src/conformance-record.js +2 -2
  6. package/dist/src/convention-rows.js +17 -2
  7. package/dist/src/conventions.js +82 -1
  8. package/dist/src/dependencies.js +42 -26
  9. package/dist/src/entry-points.js +28 -14
  10. package/dist/src/exempt.js +1 -0
  11. package/dist/src/findings/completion.js +9 -4
  12. package/dist/src/findings/dependencies-and-module-machinery.js +2 -2
  13. package/dist/src/findings/emitters.js +64 -25
  14. package/dist/src/findings/interfaces.js +4 -3
  15. package/dist/src/findings/intra-function.js +15 -2
  16. package/dist/src/findings/reads-and-writes.js +3 -2
  17. package/dist/src/findings/unused-declarations.js +13 -1
  18. package/dist/src/findings/visibility-narrowing.js +2 -1
  19. package/dist/src/findings-pass.js +1 -0
  20. package/dist/src/graph.js +6 -6
  21. package/dist/src/implementations.js +1 -1
  22. package/dist/src/indexed-access.js +215 -0
  23. package/dist/src/interface-satisfaction.js +85 -23
  24. package/dist/src/intra-function-parts.js +139 -30
  25. package/dist/src/inventory.js +6 -1
  26. package/dist/src/kinds.js +13 -12
  27. package/dist/src/matrix.js +34 -9
  28. package/dist/src/module-declarations.js +180 -0
  29. package/dist/src/outside-tests.js +17 -15
  30. package/dist/src/overrides.js +70 -9
  31. package/dist/src/query.js +7 -0
  32. package/dist/src/ref.js +9 -0
  33. package/dist/src/references.js +6 -0
  34. package/dist/src/report.js +6 -1
  35. package/dist/src/reporters/sarif.js +6 -2
  36. package/dist/src/reporters/text.js +23 -3
  37. package/dist/src/roots.js +50 -10
  38. package/dist/src/suppress-file.js +3 -5
  39. package/dist/src/version.js +1 -1
  40. package/dist/src/workflow-steps.js +82 -0
  41. package/dist/src/workspace-programs.js +6 -31
  42. package/jsr.json +1 -1
  43. package/package.json +8 -2
  44. package/src/analysis.ts +40 -4
  45. package/src/augmentations.ts +72 -0
  46. package/src/components.ts +15 -13
  47. package/src/conformance-record.ts +2 -2
  48. package/src/convention-rows.ts +25 -3
  49. package/src/conventions.ts +106 -2
  50. package/src/dependencies.ts +51 -31
  51. package/src/entry-points.ts +32 -19
  52. package/src/exempt.ts +4 -1
  53. package/src/findings/completion.ts +11 -4
  54. package/src/findings/dependencies-and-module-machinery.ts +2 -2
  55. package/src/findings/emitters.ts +89 -27
  56. package/src/findings/interfaces.ts +7 -6
  57. package/src/findings/intra-function.ts +20 -2
  58. package/src/findings/reads-and-writes.ts +7 -2
  59. package/src/findings/unused-declarations.ts +16 -1
  60. package/src/findings/visibility-narrowing.ts +2 -1
  61. package/src/findings-pass.ts +4 -1
  62. package/src/graph.ts +6 -6
  63. package/src/implementations.ts +1 -1
  64. package/src/indexed-access.ts +295 -0
  65. package/src/interface-satisfaction.ts +118 -27
  66. package/src/intra-function-parts.ts +196 -35
  67. package/src/inventory.ts +8 -0
  68. package/src/kinds.ts +14 -12
  69. package/src/matrix.ts +51 -8
  70. package/src/module-declarations.ts +241 -0
  71. package/src/outside-tests.ts +18 -16
  72. package/src/overrides.ts +80 -14
  73. package/src/query.ts +28 -0
  74. package/src/ref.ts +10 -0
  75. package/src/references.ts +6 -0
  76. package/src/report.ts +11 -1
  77. package/src/reporters/sarif.ts +9 -2
  78. package/src/reporters/text.ts +25 -3
  79. package/src/roots.ts +56 -7
  80. package/src/suppress-file.ts +3 -6
  81. package/src/sweep.ts +12 -0
  82. package/src/version.ts +1 -1
  83. package/src/workflow-steps.ts +104 -0
  84. package/src/workspace-programs.ts +6 -35
@@ -1,6 +1,8 @@
1
+ import type { Confidence } from "../config.ts";
1
2
  import { evaluateEdges, type EdgeEvaluation } from "../edges.ts";
2
3
  import type { CompletedFinding } from "../finding.ts";
3
4
  import { liveFilesAt } from "../matrix.ts";
5
+ import { declarationKey } from "../ref.ts";
4
6
  import { ledgerOf, type Dials, type Ledger, type SuppressionRecord } from "../suppress.ts";
5
7
  import { insideSkipped } from "../type-errors.ts";
6
8
  import { completed, dialsOf, reportable } from "./completion.ts";
@@ -27,10 +29,10 @@ export const EMITTERS: Emitters = new Map<string, Emitter>([
27
29
  ]);
28
30
 
29
31
  /**
30
- * The references of every declaration an unanswered question could have kept live, which
31
- * the run reports nothing about: a finding about one would be computed from the gap.
32
+ * The keys of every declaration an unanswered question could have kept live, which the run
33
+ * reports nothing about: a finding about one would be computed from the gap.
32
34
  */
33
- function heldRefs(input: EmitterInput): ReadonlySet<string> {
35
+ function heldKeys(input: EmitterInput): ReadonlySet<string> {
34
36
  const held = new Set(input.swept.heldByUnanswered);
35
37
  if (held.size === 0) {
36
38
  return held;
@@ -38,16 +40,16 @@ function heldRefs(input: EmitterInput): ReadonlySet<string> {
38
40
  return new Set(
39
41
  input.swept.matrix.union.symbols
40
42
  .filter((symbol) => held.has(symbol.id))
41
- .map((symbol) => symbol.ref),
43
+ .map((symbol) => declarationKey(symbol.ref, symbol.position.path)),
42
44
  );
43
45
  }
44
46
 
45
47
  /**
46
- * The references of every top-level declaration of a live component file. The file's
48
+ * The keys of every top-level declaration of a live component file. The file's
47
49
  * markup may use each of them, which no analysis of the markup can rule out, so the run
48
50
  * reports nothing about one while the file is live.
49
51
  */
50
- function markupRefs(input: EmitterInput): ReadonlySet<string> {
52
+ function markupKeys(input: EmitterInput): ReadonlySet<string> {
51
53
  const { swept } = input;
52
54
  if (swept.componentFiles.length === 0) {
53
55
  return new Set();
@@ -60,7 +62,7 @@ function markupRefs(input: EmitterInput): ReadonlySet<string> {
60
62
  return new Set(
61
63
  swept.matrix.union.symbols
62
64
  .filter((symbol) => live.has(symbol.parent))
63
- .map((symbol) => symbol.ref),
65
+ .map((symbol) => declarationKey(symbol.ref, symbol.position.path)),
64
66
  );
65
67
  }
66
68
 
@@ -82,8 +84,13 @@ export interface DecidedFindings {
82
84
  readonly ledger: Ledger<CompletedFinding>;
83
85
  /** Every finding a record could name: the run's with no mark, then the run's as swept. */
84
86
  readonly would: readonly CompletedFinding[];
87
+ /** The findings the minimum confidence withheld, by confidence. */
88
+ readonly withheld: Withheld;
85
89
  }
86
90
 
91
+ /** How many findings the minimum confidence withheld at each confidence. */
92
+ export type Withheld = Readonly<Record<Confidence, number>>;
93
+
87
94
  /** Every family's findings over one sweep, and those of any further emitter, completed by one step. */
88
95
  function completedOver(
89
96
  input: EmitterInput,
@@ -116,12 +123,12 @@ function dialsOver(
116
123
  }
117
124
 
118
125
  /**
119
- * Every finding of one run swept under its suppressions' marks, decided in one pass: every
120
- * family's findings completed by one step, so every component number is minted once, a
121
- * pending finding's among them; the finding each record claims withheld, whatever its
122
- * kind; the rest withheld by the dials, so a root one family reports withholds what
123
- * another family reports in its component; and every declared edge evaluated over what
124
- * remains, so a finding it holds pending is in its evaluation and nowhere else.
126
+ * Every finding of one run swept under its suppressions' marks, decided in one pass: each
127
+ * family's findings completed by one step, so every component number is minted once, the
128
+ * finding each record claims withheld, the rest withheld by the dials, so a root one family
129
+ * reports withholds what another reports in its component, and every declared edge
130
+ * evaluated over what remains, so a pending finding is in its evaluation alone. What the
131
+ * minimum confidence withheld is what the same decision at the lowest minimum adds.
125
132
  */
126
133
  export function decidedFindings(
127
134
  input: EmitterInput,
@@ -131,26 +138,81 @@ export function decidedFindings(
131
138
  const unmarked =
132
139
  suppressed.records.length === 0 ? [] : completedOver(suppressed.unmarked, fallenMembers);
133
140
  const would = [...unmarked, ...found];
141
+ const decided = dialledOver(input, suppressed, found, unmarked);
142
+ const shown =
143
+ input.config.analysis.minConfidence === "possible"
144
+ ? decided
145
+ : dialledOver(
146
+ atPossible(input),
147
+ {
148
+ unmarked: atPossible(suppressed.unmarked),
149
+ records: suppressed.records,
150
+ },
151
+ found,
152
+ unmarked,
153
+ );
154
+ return { ...decided, would, withheld: withheldOf(decided.findings, shown.findings) };
155
+ }
156
+
157
+ /** The run's input with the minimum confidence at its lowest, which withholds nothing. */
158
+ function atPossible(input: EmitterInput): EmitterInput {
159
+ const { config } = input;
160
+ return {
161
+ ...input,
162
+ config: { ...config, analysis: { ...config.analysis, minConfidence: "possible" } },
163
+ };
164
+ }
165
+
166
+ /**
167
+ * The run's completed findings decided under its dials: the finding each record claims
168
+ * withheld, the rest withheld by the dials, and every declared edge evaluated over what
169
+ * remains.
170
+ */
171
+ function dialledOver(
172
+ input: EmitterInput,
173
+ suppressed: Suppressed,
174
+ found: readonly CompletedFinding[],
175
+ unmarked: readonly CompletedFinding[],
176
+ ): Omit<DecidedFindings, "would" | "withheld"> {
177
+ const would = [...unmarked, ...found];
134
178
  const ledger = ledgerOf(suppressed.records, would, dialsOver(input, found, suppressed, unmarked));
135
- const unanswered = heldRefs(input);
136
- const markup = markupRefs(input);
179
+ const unanswered = heldKeys(input);
180
+ const markup = markupKeys(input);
137
181
  const kept = reportable(
138
182
  input,
139
- found.filter(
140
- (finding) =>
183
+ found.filter((finding) => {
184
+ const key = declarationKey(finding.symbol.ref, finding.position.path);
185
+ return (
141
186
  !ledger.withheld(finding) &&
142
- !unanswered.has(finding.symbol.ref) &&
143
- !markup.has(finding.symbol.ref) &&
144
- !insideSkipped(input.swept.skipped, finding.position),
145
- ),
187
+ !unanswered.has(key) &&
188
+ !markup.has(key) &&
189
+ !insideSkipped(input.swept.skipped, finding.position)
190
+ );
191
+ }),
146
192
  );
147
193
  const evaluated = evaluateEdges(kept, input.boundary.edges, input.swept.matrix.union.symbols);
148
- return {
149
- findings: evaluated.findings,
150
- evaluations: evaluated.evaluations,
151
- ledger,
152
- would,
153
- };
194
+ return { findings: evaluated.findings, evaluations: evaluated.evaluations, ledger };
195
+ }
196
+
197
+ /** The findings the lowest minimum would report that the run does not, counted by confidence. */
198
+ function withheldOf(
199
+ reported: readonly CompletedFinding[],
200
+ shown: readonly CompletedFinding[],
201
+ ): Withheld {
202
+ const kept = new Set(reported.map(findingKey));
203
+ const counts = { certain: 0, probable: 0, possible: 0 };
204
+ for (const finding of shown) {
205
+ if (!kept.has(findingKey(finding))) {
206
+ counts[finding.confidence] += 1;
207
+ }
208
+ }
209
+ return counts;
210
+ }
211
+
212
+ /** A finding's identity across two decisions of one run: its position, code and subject. */
213
+ function findingKey(finding: CompletedFinding): string {
214
+ const { path, line, column } = finding.position;
215
+ return JSON.stringify([path, line, column, finding.code, finding.symbol.ref]);
154
216
  }
155
217
 
156
218
  /** Every finding the run reports, each family's in table order. */
@@ -1,7 +1,7 @@
1
1
  import type { Finding, FindingPosition, PositionedSymbol } from "../finding.ts";
2
2
  import type { InventorySymbol } from "../inventory.ts";
3
3
  import { byPosition } from "../position.ts";
4
- import type { Candidate } from "../sweep.ts";
4
+ import type { MatrixCandidate } from "../matrix.ts";
5
5
  import type { Emitter, EmitterInput } from "./emitter.ts";
6
6
 
7
7
  const UNUSED_INTERFACE = "DS1201";
@@ -31,7 +31,7 @@ function compare(a: string, b: string): number {
31
31
  function indexed(input: EmitterInput): {
32
32
  readonly symbolOf: (id: string) => InventorySymbol | undefined;
33
33
  readonly inTest: (id: string) => boolean;
34
- readonly candidateOf: (id: string) => Candidate | undefined;
34
+ readonly candidateOf: (id: string) => MatrixCandidate | undefined;
35
35
  readonly implementationsOf: (id: string) => readonly PositionedSymbol[];
36
36
  } {
37
37
  const union = input.swept.matrix.union;
@@ -79,15 +79,16 @@ function findingOf(
79
79
  * The findings of the interfaces family, `DS1200` to `DS1299`: `DS1201`, a dead
80
80
  * interface, whose members fall with it unreported; and `DS1203`, a method of a live
81
81
  * interface no reference names, unless it is a marker, a method whose every
82
- * implementation has an empty body. A test file's declaration is neither kind's
83
- * subject, because a production sweep counts none of the references it can have.
82
+ * implementation has an empty body. A test file's declaration is neither kind's subject,
83
+ * because a production sweep counts none of the references it can have, save an interface
84
+ * the run that counts test references does not reach, which is `DS1201`'s.
84
85
  */
85
86
  export const interfaces: Emitter = (input) => {
86
87
  const run = indexed(input);
87
88
  const found: Finding[] = [];
88
89
  for (const candidate of input.swept.sweep.candidates) {
89
90
  const symbol = run.symbolOf(candidate.id);
90
- if (symbol === undefined || run.inTest(candidate.id)) {
91
+ if (symbol === undefined || (run.inTest(candidate.id) && !candidate.unreferencedTest)) {
91
92
  continue;
92
93
  }
93
94
  if (symbol.kind === "interface") {
@@ -122,7 +123,7 @@ export const interfaces: Emitter = (input) => {
122
123
  };
123
124
 
124
125
  /** What one unused interface's finding says, by the references the sweep counted. */
125
- function unusedMessage(candidate: Candidate): string {
126
+ function unusedMessage(candidate: MatrixCandidate): string {
126
127
  if (candidate.relation === "reachability") {
127
128
  return UNUSED_DEAD_MESSAGE;
128
129
  }
@@ -19,6 +19,24 @@ function overlapClause(overlap: readonly string[]): string {
19
19
  : `also reported by ${spelled(overlap)}`;
20
20
  }
21
21
 
22
+ /**
23
+ * Whether one parameter's name is reported: every unread one of a function whose
24
+ * signature is free, and of a function used as a value only one no read parameter follows,
25
+ * because a caller may pass such a function more arguments than it declares.
26
+ */
27
+ function parameterReported(
28
+ part: PartFact,
29
+ free: ReadonlySet<string>,
30
+ freeButValued: ReadonlySet<string>,
31
+ ): boolean {
32
+ const place = part.parameter;
33
+ if (place?.inValue !== true && free.has(part.declaration)) {
34
+ return true;
35
+ }
36
+ const valued = place?.inValue === true || freeButValued.has(part.declaration);
37
+ return valued && place?.beforeARead !== true;
38
+ }
39
+
22
40
  /**
23
41
  * Whether one part is reported. A parameter needs its function's signature free to
24
42
  * change; a result needs that, every call in the loaded program to discard it, and every
@@ -34,10 +52,10 @@ function reported(
34
52
  if (part.used) {
35
53
  return false;
36
54
  }
37
- const { free, discardedEverywhere } = input.intraFunction;
55
+ const { free, freeButValued, discardedEverywhere } = input.intraFunction;
38
56
  switch (part.code) {
39
57
  case UNUSED_PARAMETER:
40
- return free.has(part.declaration);
58
+ return parameterReported(part, free, freeButValued);
41
59
  case UNUSED_RESULT:
42
60
  return (
43
61
  free.has(part.declaration) &&
@@ -2,6 +2,7 @@ import type { Finding, FindingDetails, FindingPosition } from "../finding.ts";
2
2
  import type { InventorySymbol } from "../inventory.ts";
3
3
  import type { MatrixCandidate } from "../matrix.ts";
4
4
  import { positionKey } from "../position.ts";
5
+ import { declarationKey } from "../ref.ts";
5
6
  import type { Emitter, EmitterInput } from "./emitter.ts";
6
7
 
7
8
  /** The codes of this family. */
@@ -242,12 +243,16 @@ function unusedTypeParameters(run: Run): readonly Finding[] {
242
243
  }
243
244
 
244
245
  /**
245
- * The references of the declarations the write-only kind reports, whatever the
246
+ * The keys of the declarations the write-only kind reports, whatever the
246
247
  * severity: a narrowing kind yields each to it, since deleting the declaration
247
248
  * supersedes narrowing it.
248
249
  */
249
250
  export function writeOnlyDeclarations(input: EmitterInput): ReadonlySet<string> {
250
- return new Set(writeOnlySymbols(indexed(input)).map((finding) => finding.symbol.ref));
251
+ return new Set(
252
+ writeOnlySymbols(indexed(input)).map((finding) =>
253
+ declarationKey(finding.symbol.ref, finding.position.path),
254
+ ),
255
+ );
251
256
  }
252
257
 
253
258
  /**
@@ -71,9 +71,16 @@ function codeOf(judged: Judged): string | undefined {
71
71
  if (candidate.testOfDeadCode) {
72
72
  return "DS1005";
73
73
  }
74
- if (judged.test || judged.containerDead || NOT_THIS_FAMILY.has(symbol.kind)) {
74
+ if (
75
+ (judged.test && !candidate.unreferencedTest) ||
76
+ judged.containerDead ||
77
+ NOT_THIS_FAMILY.has(symbol.kind)
78
+ ) {
75
79
  return undefined;
76
80
  }
81
+ if (candidate.unreferencedTest) {
82
+ return unreferencedCode(symbol);
83
+ }
77
84
  const referenced = candidate.productionRefs + candidate.testRefs > 0;
78
85
  if (symbol.kind === "enum-member" && !referenced) {
79
86
  return undefined;
@@ -96,6 +103,14 @@ function codeOf(judged: Judged): string | undefined {
96
103
  return judged.unreachable ? undefined : "DS1001";
97
104
  }
98
105
 
106
+ /** The code a declaration nothing references carries, by its shape and its visibility. */
107
+ function unreferencedCode(symbol: InventorySymbol): string {
108
+ if (MEMBERS.has(symbol.kind)) {
109
+ return "DS1003";
110
+ }
111
+ return symbol.exported ? "DS1001" : "DS1002";
112
+ }
113
+
99
114
  /** The visibility a member's message names, where it narrows the member's reach. */
100
115
  function visibilityWord(symbol: InventorySymbol): string {
101
116
  switch (symbol.visibility) {
@@ -2,6 +2,7 @@ import type { Finding } from "../finding.ts";
2
2
  import { OUTSIDE, type Graph } from "../graph.ts";
3
3
  import type { InventorySymbol, SymbolKind } from "../inventory.ts";
4
4
  import type { MatrixCandidate } from "../matrix.ts";
5
+ import { declarationKey } from "../ref.ts";
5
6
  import type { RootKind } from "../roots.ts";
6
7
  import type { Emitter, EmitterInput } from "./emitter.ts";
7
8
  import { writeOnlyDeclarations } from "./reads-and-writes.ts";
@@ -159,7 +160,7 @@ function narrowing(
159
160
  const file = facts.fileOf[at] ?? OUTSIDE;
160
161
  if (
161
162
  !topLevelExport(facts, at, symbol) ||
162
- world.writeOnly.has(symbol.ref) ||
163
+ world.writeOnly.has(declarationKey(symbol.ref, symbol.position.path)) ||
163
164
  symbol.kind === "export-alias" ||
164
165
  entersByName(facts.rootedBy[at]) ||
165
166
  entersByName(facts.rootedBy[file]) ||
@@ -8,7 +8,7 @@ import type { AnalysisInputs } from "./analysis.ts";
8
8
  import { fails, type Config } from "./config.ts";
9
9
  import type { EdgeEvaluation } from "./edges.ts";
10
10
  import type { CompletedFinding, Finding } from "./finding.ts";
11
- import { decidedFindings } from "./findings/emitters.ts";
11
+ import { decidedFindings, type Withheld } from "./findings/emitters.ts";
12
12
  import {
13
13
  fileRefsOf,
14
14
  staleSuppressions,
@@ -26,6 +26,8 @@ export interface PassTotals {
26
26
  readonly staleSuppressions: number;
27
27
  /** The evaluations that hold a finding pending. */
28
28
  readonly pending: number;
29
+ /** The findings the minimum confidence withheld, by confidence. */
30
+ readonly withheld: Withheld;
29
31
  }
30
32
 
31
33
  /** What the pass answers about one run. */
@@ -98,6 +100,7 @@ export function findingsPass(inputs: AnalysisInputs): PassResult {
98
100
  reasonsRecorded: counts.reasonsRecorded,
99
101
  staleSuppressions: records.length,
100
102
  pending: decided.evaluations.filter((one) => one.finding !== undefined).length,
103
+ withheld: decided.withheld,
101
104
  },
102
105
  ledger,
103
106
  };
package/src/graph.ts CHANGED
@@ -39,11 +39,11 @@ const SWEPT: Readonly<Record<SymbolKind, boolean>> = {
39
39
 
40
40
  /**
41
41
  * Whether a root of each kind names a caller, which holds it live under both relations: a
42
- * tool reads the configuration it loads and the files its strings name, a runtime runs a
43
- * test file, a setup file, a worker, a page's module script, a command and a script, the
44
- * manifest names what enters the package, an alias global stands for the export it names,
45
- * and an entry file, a configured root and a pattern assert a caller. A library's
46
- * published API only supposes an unseen consumer, so it is live under reachability alone.
42
+ * tool reads what it loads and what its strings name, a runtime runs a test, setup or
43
+ * worker file, a page's module script, a command, a script or a workflow step, the manifest
44
+ * names what enters the package, an alias global stands for its export, and an entry file,
45
+ * a configured root and a pattern assert a caller. A library's published API only supposes
46
+ * an unseen consumer, so it is live under reachability alone.
47
47
  */
48
48
  const NAMES_A_CALLER: Readonly<Record<RootKind, boolean>> = {
49
49
  "entry-file": true,
@@ -59,10 +59,10 @@ const NAMES_A_CALLER: Readonly<Record<RootKind, boolean>> = {
59
59
  "configuration-file": true,
60
60
  "configuration-string": true,
61
61
  "test-runner": true,
62
- "mutation-testing": true,
63
62
  "browser-tests": true,
64
63
  worker: true,
65
64
  "html-entry": true,
65
+ "workflow-step": true,
66
66
  };
67
67
 
68
68
  /** Whether the sweep judges a declaration of one kind. */
@@ -118,7 +118,7 @@ function typesAt<Brand>(project: ProjectView<Brand>, nodes: readonly Node[]): Ma
118
118
  }
119
119
 
120
120
  /** Whether one class member is a method whose every written body holds no statement. */
121
- function emptyImplementation<Brand>(project: ProjectView<Brand>, member: TSSymbol): boolean {
121
+ export function emptyImplementation<Brand>(project: ProjectView<Brand>, member: TSSymbol): boolean {
122
122
  const own = project.ownPaths();
123
123
  let bodies = 0;
124
124
  for (const handle of member.declarations) {