@cplieger/deadset-ts 5.2.2 → 6.0.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 (75) 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/dependencies-and-module-machinery.js +2 -2
  12. package/dist/src/findings/emitters.js +47 -12
  13. package/dist/src/findings/interfaces.js +4 -3
  14. package/dist/src/findings/intra-function.js +15 -2
  15. package/dist/src/findings/unused-declarations.js +13 -1
  16. package/dist/src/findings-pass.js +1 -0
  17. package/dist/src/graph.js +6 -6
  18. package/dist/src/implementations.js +1 -1
  19. package/dist/src/indexed-access.js +215 -0
  20. package/dist/src/interface-satisfaction.js +85 -23
  21. package/dist/src/intra-function-parts.js +139 -30
  22. package/dist/src/inventory.js +6 -1
  23. package/dist/src/kinds.js +13 -12
  24. package/dist/src/matrix.js +34 -9
  25. package/dist/src/module-declarations.js +180 -0
  26. package/dist/src/outside-tests.js +17 -15
  27. package/dist/src/overrides.js +70 -9
  28. package/dist/src/query.js +7 -0
  29. package/dist/src/ref.js +1 -0
  30. package/dist/src/references.js +6 -0
  31. package/dist/src/report.js +6 -1
  32. package/dist/src/reporters/sarif.js +3 -1
  33. package/dist/src/reporters/text.js +23 -3
  34. package/dist/src/roots.js +50 -10
  35. package/dist/src/version.js +1 -1
  36. package/dist/src/workflow-steps.js +82 -0
  37. package/jsr.json +1 -1
  38. package/package.json +8 -2
  39. package/src/analysis.ts +40 -4
  40. package/src/augmentations.ts +72 -0
  41. package/src/components.ts +15 -13
  42. package/src/conformance-record.ts +2 -2
  43. package/src/convention-rows.ts +25 -3
  44. package/src/conventions.ts +106 -2
  45. package/src/dependencies.ts +51 -31
  46. package/src/entry-points.ts +32 -19
  47. package/src/exempt.ts +4 -1
  48. package/src/findings/dependencies-and-module-machinery.ts +2 -2
  49. package/src/findings/emitters.ts +71 -12
  50. package/src/findings/interfaces.ts +7 -6
  51. package/src/findings/intra-function.ts +20 -2
  52. package/src/findings/unused-declarations.ts +16 -1
  53. package/src/findings-pass.ts +4 -1
  54. package/src/graph.ts +6 -6
  55. package/src/implementations.ts +1 -1
  56. package/src/indexed-access.ts +295 -0
  57. package/src/interface-satisfaction.ts +118 -27
  58. package/src/intra-function-parts.ts +196 -35
  59. package/src/inventory.ts +8 -0
  60. package/src/kinds.ts +14 -12
  61. package/src/matrix.ts +51 -8
  62. package/src/module-declarations.ts +241 -0
  63. package/src/outside-tests.ts +18 -16
  64. package/src/overrides.ts +80 -14
  65. package/src/query.ts +28 -0
  66. package/src/ref.ts +1 -0
  67. package/src/references.ts +6 -0
  68. package/src/report.ts +11 -1
  69. package/src/reporters/sarif.ts +4 -1
  70. package/src/reporters/text.ts +25 -3
  71. package/src/roots.ts +56 -7
  72. package/src/sweep.ts +12 -0
  73. package/src/version.ts +1 -1
  74. package/src/workflow-steps.ts +104 -0
  75. package/src/workspace-run.ts +3 -4
@@ -1,10 +1,10 @@
1
1
  /**
2
- * The dependencies the target's manifest declares, and what the projects' own files
3
- * need from outside the target. A file needs a package it imports, re-exports from,
4
- * augments or references the types of, by name or through the file the specifier
5
- * resolves to; a compiler configuration needs one it names, and a configuration file a
6
- * tool loads needs each dependency its strings spell. A package one dependency pulls in
7
- * for itself is that dependency's need, not the target's.
2
+ * The dependencies the target's manifest and its workspace members' manifests declare, and
3
+ * what the projects' own files need from outside the target. A file needs a package it
4
+ * imports, re-exports from, augments or references the types of, by name or through the
5
+ * file the specifier resolves to; a compiler configuration needs one it names, and a
6
+ * configuration file a tool loads needs each dependency its strings spell. A package one
7
+ * dependency pulls in for itself is that dependency's need, not the target's.
8
8
  */
9
9
 
10
10
  import {
@@ -32,7 +32,7 @@ import {
32
32
  import type { FindingPosition } from "./finding.ts";
33
33
  import type { Host } from "./host.ts";
34
34
  import { nodeKey, packageScope, type Inventory } from "./inventory.ts";
35
- import { dirnamePath, isAbsolutePath, joinPath, resolvePath } from "./paths.ts";
35
+ import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.ts";
36
36
  import type { DependencySection, Module } from "./ref.ts";
37
37
  import { UNANSWERED } from "./query.ts";
38
38
  import type { Handle, ProjectView } from "./session.ts";
@@ -53,12 +53,14 @@ const SECTIONS: readonly (readonly [string, DependencySection])[] = [
53
53
  /** The owner of a use no declaration of the inventory holds: a configuration's. */
54
54
  const PROJECT_OWNER = "";
55
55
 
56
- /** One dependency the target's manifest declares. */
56
+ /** One dependency a manifest the run reads declares. */
57
57
  export interface DeclaredDependency {
58
58
  readonly name: string;
59
59
  readonly section: DependencySection;
60
60
  /** Where the manifest writes the dependency's key, below the target root. */
61
61
  readonly position: FindingPosition;
62
+ /** The scope the dependency's reference is written in: its manifest's. */
63
+ readonly manifest: Module;
62
64
  }
63
65
 
64
66
  /** What a configuration's own reading needs: packages no deletion of a declaration removes. */
@@ -97,9 +99,7 @@ export interface ProjectNeeds {
97
99
 
98
100
  /** The run's dependency evidence. */
99
101
  export interface Dependencies {
100
- /** The scope every dependency's reference is written in: the target's manifest. */
101
- readonly manifest: Module;
102
- /** Every declared dependency, in the order the manifest writes them. */
102
+ /** Every declared dependency, manifest by manifest, in the order each writes them. */
103
103
  readonly declared: readonly DeclaredDependency[];
104
104
  /** The installed copy of each declared dependency the run found, by name. */
105
105
  readonly installed: ReadonlyMap<string, InstalledDependency>;
@@ -608,8 +608,13 @@ function installedOf(host: Host, root: string, name: string): InstalledDependenc
608
608
  }
609
609
  }
610
610
 
611
- /** Every dependency the manifest text declares, in the order the text writes them. */
612
- function declaredIn(text: string, manifest: Record<string, unknown>): DeclaredDependency[] {
611
+ /** Every dependency the manifest text at `path` declares, in the order the text writes them. */
612
+ function declaredIn(
613
+ text: string,
614
+ manifest: Record<string, unknown>,
615
+ path: string,
616
+ scope: Module,
617
+ ): DeclaredDependency[] {
613
618
  const sections = membersAt(text, skipWhitespace(text, 0));
614
619
  const declared: { readonly dependency: DeclaredDependency; readonly at: number }[] = [];
615
620
  for (const [member, section] of SECTIONS) {
@@ -621,7 +626,12 @@ function declaredIn(text: string, manifest: Record<string, unknown>): DeclaredDe
621
626
  for (const [name, { key }] of membersAt(text, held.value)) {
622
627
  const { line, column } = lineAndColumn(text, key);
623
628
  declared.push({
624
- dependency: { name, section, position: { path: MANIFEST, line, column, endLine: line } },
629
+ dependency: {
630
+ name,
631
+ section,
632
+ position: { path, line, column, endLine: line },
633
+ manifest: scope,
634
+ },
625
635
  at: key,
626
636
  });
627
637
  }
@@ -630,11 +640,10 @@ function declaredIn(text: string, manifest: Record<string, unknown>): DeclaredDe
630
640
  }
631
641
 
632
642
  /**
633
- * The run's dependency evidence: the target's manifest read with the position of every
634
- * dependency it declares, the installed copy of each, what the projects need, and the
635
- * last uses each deletion candidate holds.
636
- *
637
- * Only the manifest at the target root is read, as for entry points. A manifest the
643
+ * The run's dependency evidence: the target's manifest and each workspace member's in
644
+ * `members` below the target root, read with the position of every dependency each
645
+ * declares, the installed copy of each found from its manifest's directory, what the
646
+ * projects need, and the last uses each deletion candidate holds. A manifest the
638
647
  * program cannot read declares nothing.
639
648
  */
640
649
  export function dependenciesOf(
@@ -642,20 +651,31 @@ export function dependenciesOf(
642
651
  targetRoot: string,
643
652
  perProject: readonly ProjectNeeds[],
644
653
  candidates: readonly string[],
654
+ members: readonly string[] = [],
645
655
  ): Dependencies {
646
- const read = readJSON(host, joinPath(targetRoot, MANIFEST));
647
- const manifest = isRecord(read?.value) ? read.value : undefined;
648
- const scope = packageScope(host, targetRoot)(MANIFEST);
649
- const declared =
650
- read !== undefined && manifest !== undefined ? declaredIn(read.text, manifest) : [];
651
-
652
656
  const root = resolvePath(host.workingDirectory(), targetRoot);
657
+ const scopeOf = packageScope(host, targetRoot);
658
+ const manifests = new Map<string, string>([[root, MANIFEST]]);
659
+ for (const dir of members) {
660
+ const below = relativePath(root, dir);
661
+ if (below !== undefined && !manifests.has(dir)) {
662
+ manifests.set(dir, `${below}/${MANIFEST}`);
663
+ }
664
+ }
665
+ const declared: DeclaredDependency[] = [];
653
666
  const installed = new Map<string, InstalledDependency>();
654
- for (const dependency of declared) {
655
- if (!installed.has(dependency.name)) {
656
- const found = installedOf(host, root, dependency.name);
657
- if (found !== undefined) {
658
- installed.set(dependency.name, found);
667
+ for (const [dir, path] of manifests) {
668
+ const read = readJSON(host, joinPath(dir, MANIFEST));
669
+ if (read === undefined || !isRecord(read.value)) {
670
+ continue;
671
+ }
672
+ for (const dependency of declaredIn(read.text, read.value, path, scopeOf(path))) {
673
+ declared.push(dependency);
674
+ if (!installed.has(dependency.name)) {
675
+ const found = installedOf(host, dir, dependency.name);
676
+ if (found !== undefined) {
677
+ installed.set(dependency.name, found);
678
+ }
659
679
  }
660
680
  }
661
681
  }
@@ -691,5 +711,5 @@ export function dependenciesOf(
691
711
  );
692
712
  }
693
713
  }
694
- return { manifest: scope, declared, installed, needed, lastUses };
714
+ return { declared, installed, needed, lastUses };
695
715
  }
@@ -36,6 +36,7 @@ import {
36
36
  moduleStrings,
37
37
  type ParseFile,
38
38
  } from "./configuration-files.ts";
39
+ import type { Selected } from "./conventions.ts";
39
40
  import { globExpression } from "./glob.ts";
40
41
  import type { Host } from "./host.ts";
41
42
  import { htmlEntries } from "./html-entries.ts";
@@ -43,6 +44,7 @@ import { dirnamePath, relativePath, resolvePath } from "./paths.ts";
43
44
  import { UNANSWERED } from "./query.ts";
44
45
  import type { ProjectView } from "./session.ts";
45
46
  import type { SourceFiles } from "./source-files.ts";
47
+ import { workflowEntries } from "./workflow-steps.ts";
46
48
 
47
49
  /** Which rule entered a file. */
48
50
  export type EntryRule =
@@ -52,14 +54,14 @@ export type EntryRule =
52
54
  | "configuration-string"
53
55
  /** The test runner: its workspace configuration, its setup files and the test files. */
54
56
  | "test-runner"
55
- /** The mutation tester's configuration under a name the configuration form does not cover. */
56
- | "mutation-testing"
57
57
  /** The test files below the browser-test runner's test directory. */
58
58
  | "browser-tests"
59
59
  /** A worker or service worker a call addresses by a string literal. */
60
60
  | "worker"
61
61
  /** A file a module script of an HTML file below the target root loads. */
62
- | "html-entry";
62
+ | "html-entry"
63
+ /** A file a `run:` step of a workflow in the target root's `.github/workflows` names. */
64
+ | "workflow-step";
63
65
 
64
66
  /** One file a rule enters. */
65
67
  export interface EntryPoint {
@@ -90,9 +92,6 @@ interface Convention {
90
92
  /** The extensions the test runner and the browser-test runner load a configuration from. */
91
93
  const MODULE_EXTENSIONS = ["js", "mjs", "cjs", "ts", "mts", "cts"] as const;
92
94
 
93
- /** The extensions the mutation tester's configuration is loaded from. */
94
- const SCRIPT_EXTENSIONS = ["js", "mjs", "cjs"] as const;
95
-
96
95
  /** The tool file names the configuration form does not cover. */
97
96
  const CONVENTIONS: readonly Convention[] = [
98
97
  {
@@ -101,12 +100,6 @@ const CONVENTIONS: readonly Convention[] = [
101
100
  extensions: MODULE_EXTENSIONS,
102
101
  testRunner: true,
103
102
  },
104
- {
105
- rule: "mutation-testing",
106
- stems: ["stryker.conf", ".stryker.config", ".stryker.conf"],
107
- extensions: SCRIPT_EXTENSIONS,
108
- testRunner: false,
109
- },
110
103
  ];
111
104
 
112
105
  /** The stem of the browser-test runner's configuration, whose test directory is read. */
@@ -262,16 +255,35 @@ function isBrowserTestConfig(name: string): boolean {
262
255
  /**
263
256
  * The files the configuration files of one project enter: each configuration module,
264
257
  * with what it exports, and each own file a relative string of a configuration module
265
- * or a JSON configuration file names, read against that file's directory.
258
+ * or a JSON configuration file names, read against that file's directory. A string at a
259
+ * selection key of an applied convention row names no file.
266
260
  */
267
261
  function configurationEntries(
268
262
  host: Host,
269
263
  targetRoot: string,
270
264
  files: SourceFiles,
271
265
  parse: ParseFile | undefined,
266
+ selected: ReadonlyMap<string, Selected>,
272
267
  ): readonly EntryPoint[] {
273
268
  const { modules, documents, outside } = configurationFiles(host, targetRoot, files, parse);
274
269
  const found: EntryPoint[] = [];
270
+ const unselected = (file: SourceFile): readonly string[] => {
271
+ const starts = selected.get(file.fileName)?.starts;
272
+ return moduleStrings(file)
273
+ .filter((one) => starts?.has(one.node.getStart()) !== true)
274
+ .map((one) => one.text);
275
+ };
276
+ const unselectedIn = (path: string, texts: readonly string[]): readonly string[] => {
277
+ const left = [...(selected.get(path)?.texts ?? [])];
278
+ return texts.filter((text) => {
279
+ const at = left.indexOf(text);
280
+ if (at < 0) {
281
+ return true;
282
+ }
283
+ left.splice(at, 1);
284
+ return false;
285
+ });
286
+ };
275
287
  const strings = (dir: string, texts: readonly string[]): void => {
276
288
  for (const text of texts) {
277
289
  for (const file of filesNamedBy(files, dir, text)) {
@@ -281,13 +293,10 @@ function configurationEntries(
281
293
  };
282
294
  for (const { file, form } of modules) {
283
295
  found.push({ file, rule: "configuration-file", source: form, exports: true });
284
- strings(
285
- dirnamePath(file.fileName),
286
- moduleStrings(file).map((one) => one.text),
287
- );
296
+ strings(dirnamePath(file.fileName), unselected(file));
288
297
  }
289
298
  for (const document of documents) {
290
- strings(dirnamePath(document.path), document.strings);
299
+ strings(dirnamePath(document.path), unselectedIn(document.path, document.strings));
291
300
  }
292
301
  for (const { file } of outside) {
293
302
  strings(
@@ -523,8 +532,9 @@ export function entryPoints<Brand>(
523
532
  host: Host,
524
533
  targetRoot: string,
525
534
  parse?: ParseFile,
535
+ selected: ReadonlyMap<string, Selected> = new Map(),
526
536
  ): readonly EntryPoint[] {
527
- const found: EntryPoint[] = [...configurationEntries(host, targetRoot, files, parse)];
537
+ const found: EntryPoint[] = [...configurationEntries(host, targetRoot, files, parse, selected)];
528
538
  const tests = testFiles.map((pattern) => ({ pattern, expression: globExpression(pattern) }));
529
539
  for (const [path, file] of files.byPath) {
530
540
  const name = basename(path);
@@ -548,5 +558,8 @@ export function entryPoints<Brand>(
548
558
  for (const { file, source } of htmlEntries(host, targetRoot, files, parse)) {
549
559
  found.push({ file, rule: "html-entry", source, exports: true });
550
560
  }
561
+ for (const { file, source } of workflowEntries(host, targetRoot, files)) {
562
+ found.push({ file, rule: "workflow-step", source, exports: true });
563
+ }
551
564
  return found;
552
565
  }
package/src/exempt.ts CHANGED
@@ -24,7 +24,7 @@ import { byPosition, positionKey, type Position } from "./position.ts";
24
24
  import { Unanswerable } from "./query.ts";
25
25
  import type { ProjectView } from "./session.ts";
26
26
  import type { Templates } from "./template-field.ts";
27
- import { sweep, type Exemption, type Mode, type SweepInput } from "./sweep.ts";
27
+ import { sweep, type Exemption, type Implementing, type Mode, type SweepInput } from "./sweep.ts";
28
28
 
29
29
  /** One declaration a detector holds back, and where and why. */
30
30
  export interface Evidence {
@@ -35,6 +35,8 @@ export interface Evidence {
35
35
  readonly site: Position;
36
36
  /** The component file, below the target root, whose markup holds the evidence. */
37
37
  readonly whileLive?: string;
38
+ /** Where the evidence is an implementation of interface methods, which and how. */
39
+ readonly implementing?: Implementing;
38
40
  }
39
41
 
40
42
  /** What a detector reads of one project, while the project's view is open. */
@@ -214,6 +216,7 @@ export function evidenceRecords<Brand>(
214
216
  detail: evidence.detail,
215
217
  site: evidence.site,
216
218
  ...(evidence.whileLive === undefined ? {} : { whileLive: evidence.whileLive }),
219
+ ...(evidence.implementing === undefined ? {} : { implementing: evidence.implementing }),
217
220
  });
218
221
  }
219
222
  }
@@ -37,7 +37,7 @@ function neededBy(dependencies: Dependencies): ReadonlySet<string> {
37
37
  }
38
38
 
39
39
  /**
40
- * A dependency, development dependency or peer dependency of the target's manifest
40
+ * A dependency, development dependency or peer dependency of a manifest the run reads
41
41
  * that the target does not need, at the position of its key.
42
42
  */
43
43
  function unusedDependencies(dependencies: Dependencies): Finding[] {
@@ -48,7 +48,7 @@ function unusedDependencies(dependencies: Dependencies): Finding[] {
48
48
  code: UNUSED_DEPENDENCY,
49
49
  position: dependency.position,
50
50
  symbol: {
51
- ref: renderRef(dependencies.manifest, {
51
+ ref: renderRef(dependency.manifest, {
52
52
  of: "dependency",
53
53
  name: dependency.name,
54
54
  section: dependency.section,
@@ -1,3 +1,4 @@
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";
@@ -82,8 +83,13 @@ export interface DecidedFindings {
82
83
  readonly ledger: Ledger<CompletedFinding>;
83
84
  /** Every finding a record could name: the run's with no mark, then the run's as swept. */
84
85
  readonly would: readonly CompletedFinding[];
86
+ /** The findings the minimum confidence withheld, by confidence. */
87
+ readonly withheld: Withheld;
85
88
  }
86
89
 
90
+ /** How many findings the minimum confidence withheld at each confidence. */
91
+ export type Withheld = Readonly<Record<Confidence, number>>;
92
+
87
93
  /** Every family's findings over one sweep, and those of any further emitter, completed by one step. */
88
94
  function completedOver(
89
95
  input: EmitterInput,
@@ -116,12 +122,12 @@ function dialsOver(
116
122
  }
117
123
 
118
124
  /**
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.
125
+ * Every finding of one run swept under its suppressions' marks, decided in one pass: each
126
+ * family's findings completed by one step, so every component number is minted once, the
127
+ * finding each record claims withheld, the rest withheld by the dials, so a root one family
128
+ * reports withholds what another reports in its component, and every declared edge
129
+ * evaluated over what remains, so a pending finding is in its evaluation alone. What the
130
+ * minimum confidence withheld is what the same decision at the lowest minimum adds.
125
131
  */
126
132
  export function decidedFindings(
127
133
  input: EmitterInput,
@@ -131,6 +137,43 @@ export function decidedFindings(
131
137
  const unmarked =
132
138
  suppressed.records.length === 0 ? [] : completedOver(suppressed.unmarked, fallenMembers);
133
139
  const would = [...unmarked, ...found];
140
+ const decided = dialledOver(input, suppressed, found, unmarked);
141
+ const shown =
142
+ input.config.analysis.minConfidence === "possible"
143
+ ? decided
144
+ : dialledOver(
145
+ atPossible(input),
146
+ {
147
+ unmarked: atPossible(suppressed.unmarked),
148
+ records: suppressed.records,
149
+ },
150
+ found,
151
+ unmarked,
152
+ );
153
+ return { ...decided, would, withheld: withheldOf(decided.findings, shown.findings) };
154
+ }
155
+
156
+ /** The run's input with the minimum confidence at its lowest, which withholds nothing. */
157
+ function atPossible(input: EmitterInput): EmitterInput {
158
+ const { config } = input;
159
+ return {
160
+ ...input,
161
+ config: { ...config, analysis: { ...config.analysis, minConfidence: "possible" } },
162
+ };
163
+ }
164
+
165
+ /**
166
+ * The run's completed findings decided under its dials: the finding each record claims
167
+ * withheld, the rest withheld by the dials, and every declared edge evaluated over what
168
+ * remains.
169
+ */
170
+ function dialledOver(
171
+ input: EmitterInput,
172
+ suppressed: Suppressed,
173
+ found: readonly CompletedFinding[],
174
+ unmarked: readonly CompletedFinding[],
175
+ ): Omit<DecidedFindings, "would" | "withheld"> {
176
+ const would = [...unmarked, ...found];
134
177
  const ledger = ledgerOf(suppressed.records, would, dialsOver(input, found, suppressed, unmarked));
135
178
  const unanswered = heldRefs(input);
136
179
  const markup = markupRefs(input);
@@ -145,12 +188,28 @@ export function decidedFindings(
145
188
  ),
146
189
  );
147
190
  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
- };
191
+ return { findings: evaluated.findings, evaluations: evaluated.evaluations, ledger };
192
+ }
193
+
194
+ /** The findings the lowest minimum would report that the run does not, counted by confidence. */
195
+ function withheldOf(
196
+ reported: readonly CompletedFinding[],
197
+ shown: readonly CompletedFinding[],
198
+ ): Withheld {
199
+ const kept = new Set(reported.map(findingKey));
200
+ const counts = { certain: 0, probable: 0, possible: 0 };
201
+ for (const finding of shown) {
202
+ if (!kept.has(findingKey(finding))) {
203
+ counts[finding.confidence] += 1;
204
+ }
205
+ }
206
+ return counts;
207
+ }
208
+
209
+ /** A finding's identity across two decisions of one run: its position, code and subject. */
210
+ function findingKey(finding: CompletedFinding): string {
211
+ const { path, line, column } = finding.position;
212
+ return JSON.stringify([path, line, column, finding.code, finding.symbol.ref]);
154
213
  }
155
214
 
156
215
  /** 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) &&
@@ -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) {
@@ -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) {