@cplieger/deadset-ts 4.0.1 → 5.1.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 (138) hide show
  1. package/README.md +4 -4
  2. package/dist/bin/component-mapper.js +5 -0
  3. package/dist/bin/mapper-connection.js +101 -0
  4. package/dist/bin/node-host.js +24 -2
  5. package/dist/src/analysis.js +301 -41
  6. package/dist/src/component-files.js +612 -0
  7. package/dist/src/component-layer.js +132 -0
  8. package/dist/src/config.js +9 -2
  9. package/dist/src/configuration-files.js +290 -0
  10. package/dist/src/conformance-record.js +3 -9
  11. package/dist/src/contextual-uses.js +240 -0
  12. package/dist/src/convention-rows.js +389 -0
  13. package/dist/src/conventions.js +565 -0
  14. package/dist/src/dependencies.js +36 -3
  15. package/dist/src/destructuring.js +6 -6
  16. package/dist/src/discover.js +10 -3
  17. package/dist/src/emit-map.js +72 -0
  18. package/dist/src/entry-point-gaps.js +81 -35
  19. package/dist/src/entry-points.js +50 -32
  20. package/dist/src/exempt-classes.js +6 -1
  21. package/dist/src/exempt.js +32 -20
  22. package/dist/src/findings/completion.js +46 -32
  23. package/dist/src/findings/emitters.js +22 -1
  24. package/dist/src/findings/interfaces.js +6 -2
  25. package/dist/src/findings/reads-and-writes.js +53 -9
  26. package/dist/src/findings/unused-declarations.js +47 -3
  27. package/dist/src/generated-file.js +24 -0
  28. package/dist/src/graph.js +13 -5
  29. package/dist/src/interface-satisfaction.js +2 -68
  30. package/dist/src/inventory.js +8 -3
  31. package/dist/src/kinds.js +6 -5
  32. package/dist/src/manifest.js +37 -11
  33. package/dist/src/matrix.js +21 -13
  34. package/dist/src/partial.js +3 -1
  35. package/dist/src/pnpm-workspace.js +120 -0
  36. package/dist/src/position.js +17 -2
  37. package/dist/src/print.js +2 -0
  38. package/dist/src/query.js +15 -0
  39. package/dist/src/references.js +404 -36
  40. package/dist/src/report.js +26 -6
  41. package/dist/src/reporters/annotations.js +10 -3
  42. package/dist/src/reporters/template.js +3 -0
  43. package/dist/src/resolve.js +37 -11
  44. package/dist/src/roots.js +37 -8
  45. package/dist/src/run.js +13 -3
  46. package/dist/src/schema.js +2 -0
  47. package/dist/src/session.js +141 -34
  48. package/dist/src/setup-failure.js +23 -0
  49. package/dist/src/source-files.js +4 -61
  50. package/dist/src/stores.js +3 -0
  51. package/dist/src/sweep.js +4 -2
  52. package/dist/src/template-field.js +38 -6
  53. package/dist/src/test-support.js +59 -0
  54. package/dist/src/type-errors.js +252 -0
  55. package/dist/src/type-query-alias.js +92 -0
  56. package/dist/src/value-flow.js +74 -0
  57. package/dist/src/verbs/analyze.js +24 -4
  58. package/dist/src/verbs/explain.js +4 -1
  59. package/dist/src/verbs/print-projects.js +17 -56
  60. package/dist/src/verbs/print-retained.js +7 -2
  61. package/dist/src/verbs/print-roots.js +7 -2
  62. package/dist/src/version-range.js +255 -0
  63. package/dist/src/version.js +1 -1
  64. package/dist/src/workspace-manifests.js +68 -0
  65. package/dist/src/workspace-programs.js +342 -0
  66. package/dist/src/workspace-resolution.js +307 -0
  67. package/dist/src/workspace-run.js +75 -0
  68. package/dist/src/workspace.js +358 -0
  69. package/jsr.json +1 -1
  70. package/package.json +3 -2
  71. package/src/analysis.ts +475 -72
  72. package/src/component-files.ts +718 -0
  73. package/src/component-layer.ts +160 -0
  74. package/src/config.ts +11 -2
  75. package/src/configuration-files.ts +370 -0
  76. package/src/conformance-record.ts +3 -10
  77. package/src/contextual-uses.ts +354 -0
  78. package/src/convention-rows.ts +448 -0
  79. package/src/conventions.ts +788 -0
  80. package/src/dependencies.ts +49 -2
  81. package/src/destructuring.ts +16 -6
  82. package/src/discover.ts +10 -6
  83. package/src/emit-map.ts +93 -0
  84. package/src/entry-point-gaps.ts +92 -38
  85. package/src/entry-points.ts +79 -36
  86. package/src/exempt-classes.ts +6 -1
  87. package/src/exempt.ts +69 -20
  88. package/src/file-facts.ts +2 -0
  89. package/src/findings/completion.ts +55 -36
  90. package/src/findings/emitters.ts +32 -1
  91. package/src/findings/interfaces.ts +9 -2
  92. package/src/findings/reads-and-writes.ts +70 -9
  93. package/src/findings/unused-declarations.ts +60 -3
  94. package/src/generated-file.ts +27 -0
  95. package/src/graph.ts +15 -4
  96. package/src/host.ts +23 -0
  97. package/src/index.ts +1 -1
  98. package/src/interface-satisfaction.ts +1 -92
  99. package/src/inventory.ts +17 -3
  100. package/src/kinds.ts +7 -5
  101. package/src/manifest.ts +47 -14
  102. package/src/matrix.ts +43 -15
  103. package/src/partial.ts +6 -1
  104. package/src/pnpm-workspace.ts +135 -0
  105. package/src/position.ts +23 -2
  106. package/src/print.ts +2 -0
  107. package/src/query.ts +26 -0
  108. package/src/references.ts +472 -40
  109. package/src/report.ts +56 -6
  110. package/src/reporters/annotations.ts +12 -3
  111. package/src/reporters/template.ts +3 -0
  112. package/src/resolve.ts +65 -11
  113. package/src/roots.ts +70 -9
  114. package/src/run.ts +15 -5
  115. package/src/schema.ts +2 -0
  116. package/src/session.ts +218 -37
  117. package/src/setup-failure.ts +39 -0
  118. package/src/source-files.ts +4 -66
  119. package/src/stores.ts +9 -0
  120. package/src/sweep.ts +9 -2
  121. package/src/template-field.ts +42 -7
  122. package/src/test-support.ts +77 -0
  123. package/src/type-errors.ts +340 -0
  124. package/src/type-query-alias.ts +133 -0
  125. package/src/value-flow.ts +104 -0
  126. package/src/verbs/analyze.ts +38 -4
  127. package/src/verbs/explain.ts +4 -1
  128. package/src/verbs/print-projects.ts +17 -69
  129. package/src/verbs/print-retained.ts +7 -2
  130. package/src/verbs/print-roots.ts +7 -2
  131. package/src/verbs/verb.ts +6 -3
  132. package/src/version-range.ts +309 -0
  133. package/src/version.ts +1 -1
  134. package/src/workspace-manifests.ts +100 -0
  135. package/src/workspace-programs.ts +486 -0
  136. package/src/workspace-resolution.ts +393 -0
  137. package/src/workspace-run.ts +148 -0
  138. package/src/workspace.ts +463 -0
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * The files a tool or the platform enters without any import naming them: the
3
- * configuration files the test runner, the linter, the mutation tester and the
4
- * browser-test runner load by their file name, the test files the runner executes,
5
- * and a worker a call addresses by a string literal.
3
+ * configuration files a tool loads by their file name and the files their strings
4
+ * name, the test files the runner executes, and a worker a call addresses by a string
5
+ * literal.
6
6
  *
7
7
  * Each rule is read from the program and nothing else: a file name the tool
8
8
  * itself looks for, a literal written in a configuration file, or a literal
@@ -30,7 +30,14 @@ import {
30
30
  type SourceFile,
31
31
  } from "@typescript/native/unstable/ast";
32
32
  import { SymbolFlags } from "@typescript/native/unstable/sync";
33
+ import {
34
+ configurationFiles,
35
+ filesNamedBy,
36
+ moduleStrings,
37
+ type ParseFile,
38
+ } from "./configuration-files.ts";
33
39
  import { globExpression } from "./glob.ts";
40
+ import type { Host } from "./host.ts";
34
41
  import { dirnamePath, relativePath, resolvePath } from "./paths.ts";
35
42
  import { UNANSWERED } from "./query.ts";
36
43
  import type { ProjectView } from "./session.ts";
@@ -38,13 +45,15 @@ import type { SourceFiles } from "./source-files.ts";
38
45
 
39
46
  /** Which rule entered a file. */
40
47
  export type EntryRule =
41
- /** The test runner: its configuration, its setup files and the test files. */
48
+ /** A configuration module, by the form of its name. */
49
+ | "configuration-file"
50
+ /** A file a relative string of a configuration module or a JSON configuration file names. */
51
+ | "configuration-string"
52
+ /** The test runner: its workspace configuration, its setup files and the test files. */
42
53
  | "test-runner"
43
- /** The linter's flat configuration. */
44
- | "lint-configuration"
45
- /** The mutation tester's configuration, and the test-runner configuration it names. */
54
+ /** The mutation tester's configuration under a name the configuration form does not cover. */
46
55
  | "mutation-testing"
47
- /** The browser-test runner's configuration and the test files below its test directory. */
56
+ /** The test files below the browser-test runner's test directory. */
48
57
  | "browser-tests"
49
58
  /** A worker or service worker a call addresses by a string literal. */
50
59
  | "worker";
@@ -78,42 +87,28 @@ interface Convention {
78
87
  /** The extensions the test runner and the browser-test runner load a configuration from. */
79
88
  const MODULE_EXTENSIONS = ["js", "mjs", "cjs", "ts", "mts", "cts"] as const;
80
89
 
81
- /** The extensions the linter's flat configuration and the mutation tester's are loaded from. */
90
+ /** The extensions the mutation tester's configuration is loaded from. */
82
91
  const SCRIPT_EXTENSIONS = ["js", "mjs", "cjs"] as const;
83
92
 
93
+ /** The tool file names the configuration form does not cover. */
84
94
  const CONVENTIONS: readonly Convention[] = [
85
95
  {
86
96
  rule: "test-runner",
87
- stems: ["vitest.config", "vitest.workspace", "vitest.projects"],
97
+ stems: ["vitest.workspace", "vitest.projects"],
88
98
  extensions: MODULE_EXTENSIONS,
89
99
  testRunner: true,
90
100
  },
91
- {
92
- rule: "lint-configuration",
93
- stems: ["eslint.config"],
94
- extensions: MODULE_EXTENSIONS,
95
- testRunner: false,
96
- },
97
101
  {
98
102
  rule: "mutation-testing",
99
- stems: ["stryker.config", "stryker.conf", ".stryker.config", ".stryker.conf"],
103
+ stems: ["stryker.conf", ".stryker.config", ".stryker.conf"],
100
104
  extensions: SCRIPT_EXTENSIONS,
101
105
  testRunner: false,
102
106
  },
103
- {
104
- rule: "mutation-testing",
105
- stems: ["vitest.stryker.config"],
106
- extensions: MODULE_EXTENSIONS,
107
- testRunner: true,
108
- },
109
- {
110
- rule: "browser-tests",
111
- stems: ["playwright.config"],
112
- extensions: MODULE_EXTENSIONS,
113
- testRunner: false,
114
- },
115
107
  ];
116
108
 
109
+ /** The stem of the browser-test runner's configuration, whose test directory is read. */
110
+ const BROWSER_TEST_CONFIG = "playwright.config";
111
+
117
112
  /** The browser-test runner's own test-file pattern, which holds where a configuration names none. */
118
113
  const BROWSER_TEST_MATCH = "**/*.{spec,test}.{js,ts,jsx,tsx,cjs,cts,cjsx,ctsx,mjs,mts,mjsx,mtsx}";
119
114
 
@@ -256,6 +251,50 @@ function filesNamed(files: SourceFiles, dir: string, text: string): readonly Sou
256
251
  return files.named(resolvePath(dir, pathPart(text)));
257
252
  }
258
253
 
254
+ /** Whether one file name is the browser-test runner's configuration. */
255
+ function isBrowserTestConfig(name: string): boolean {
256
+ return MODULE_EXTENSIONS.some((ext) => name === `${BROWSER_TEST_CONFIG}.${ext}`);
257
+ }
258
+
259
+ /**
260
+ * The files the configuration files of one project enter: each configuration module,
261
+ * with what it exports, and each own file a relative string of a configuration module
262
+ * or a JSON configuration file names, read against that file's directory.
263
+ */
264
+ function configurationEntries(
265
+ host: Host,
266
+ targetRoot: string,
267
+ files: SourceFiles,
268
+ parse: ParseFile | undefined,
269
+ ): readonly EntryPoint[] {
270
+ const { modules, documents, outside } = configurationFiles(host, targetRoot, files, parse);
271
+ const found: EntryPoint[] = [];
272
+ const strings = (dir: string, texts: readonly string[]): void => {
273
+ for (const text of texts) {
274
+ for (const file of filesNamedBy(files, dir, text)) {
275
+ found.push({ file, rule: "configuration-string", source: text, exports: true });
276
+ }
277
+ }
278
+ };
279
+ for (const { file, form } of modules) {
280
+ found.push({ file, rule: "configuration-file", source: form, exports: true });
281
+ strings(
282
+ dirnamePath(file.fileName),
283
+ moduleStrings(file).map((one) => one.text),
284
+ );
285
+ }
286
+ for (const document of documents) {
287
+ strings(dirnamePath(document.path), document.strings);
288
+ }
289
+ for (const { file } of outside) {
290
+ strings(
291
+ dirnamePath(file.fileName),
292
+ moduleStrings(file).map((one) => one.text),
293
+ );
294
+ }
295
+ return found;
296
+ }
297
+
259
298
  /**
260
299
  * The setup files one test-runner configuration names: every literal under
261
300
  * `test.setupFiles` and `test.globalSetup`, read against the configuration's own
@@ -467,8 +506,8 @@ function workersOf<Brand>(
467
506
  }
468
507
 
469
508
  /**
470
- * Every file of one project the lifted rules enter, in no particular order and
471
- * possibly more than once: a file two rules enter appears once per rule.
509
+ * Every file of one project the rules enter, in no particular order and possibly more
510
+ * than once: a file two rules enter appears once per rule.
472
511
  *
473
512
  * `testFiles` is the configuration's test-file patterns, matched against each
474
513
  * file's path below the target root; a file one of them matches is a test file the
@@ -478,19 +517,23 @@ export function entryPoints<Brand>(
478
517
  project: ProjectView<Brand>,
479
518
  files: SourceFiles,
480
519
  testFiles: readonly string[],
520
+ host: Host,
521
+ targetRoot: string,
522
+ parse?: ParseFile,
481
523
  ): readonly EntryPoint[] {
482
- const found: EntryPoint[] = [];
524
+ const found: EntryPoint[] = [...configurationEntries(host, targetRoot, files, parse)];
483
525
  const tests = testFiles.map((pattern) => ({ pattern, expression: globExpression(pattern) }));
484
526
  for (const [path, file] of files.byPath) {
485
- const named = conventionOf(basename(path));
527
+ const name = basename(path);
528
+ const named = conventionOf(name);
486
529
  if (named !== undefined) {
487
530
  found.push({ file, rule: named.convention.rule, source: named.spelled, exports: true });
488
531
  if (named.convention.testRunner) {
489
532
  found.push(...setupFilesOf(file, files));
490
533
  }
491
- if (named.convention.rule === "browser-tests") {
492
- found.push(...browserTestsOf(file, files));
493
- }
534
+ }
535
+ if (isBrowserTestConfig(name)) {
536
+ found.push(...browserTestsOf(file, files));
494
537
  }
495
538
  for (const { pattern, expression } of tests) {
496
539
  if (expression.test(path)) {
@@ -49,7 +49,12 @@ export const EXEMPTION_CLASSES = [
49
49
  confidence: "certain",
50
50
  typescriptVisibility: { private: false, privateName: false },
51
51
  },
52
- { class: "generated-file", languages: ["go"], confidence: "certain" },
52
+ {
53
+ class: "generated-file",
54
+ languages: ["go", "ts"],
55
+ confidence: "certain",
56
+ typescriptVisibility: { private: true, privateName: false },
57
+ },
53
58
  { class: "linkname-cgo-asm-plugin", languages: ["go"], confidence: "certain" },
54
59
  {
55
60
  class: "template-field",
package/src/exempt.ts CHANGED
@@ -33,6 +33,8 @@ export interface Evidence {
33
33
  /** One clause naming the relation, then the thing it relates to. */
34
34
  readonly detail: string;
35
35
  readonly site: Position;
36
+ /** The component file, below the target root, whose markup holds the evidence. */
37
+ readonly whileLive?: string;
36
38
  }
37
39
 
38
40
  /** What a detector reads of one project, while the project's view is open. */
@@ -46,6 +48,11 @@ export interface DetectorInput<Brand> {
46
48
  readonly ts: TSSection;
47
49
  /** The absolute path of each declared consumer, whose modules are part of the analysis. */
48
50
  readonly consumers: readonly string[];
51
+ /**
52
+ * The project's generated files, by path below the target root, each with the
53
+ * convention row that names its directory as generated.
54
+ */
55
+ readonly generated?: ReadonlyMap<string, string>;
49
56
  }
50
57
 
51
58
  /**
@@ -160,8 +167,29 @@ export function computeExemptions<Brand>(
160
167
  detectors: Detectors,
161
168
  holding: Holding,
162
169
  ): readonly Exemption[] {
170
+ return detectExemptions(input, detectors, holding).records;
171
+ }
172
+
173
+ /** One project's records, and the evidence a production mode found in a test file. */
174
+ export interface Detected {
175
+ readonly records: readonly Exemption[];
176
+ /**
177
+ * Under a production mode, each record whose evidence is written in a test file. It
178
+ * holds nothing, and it is a test reference to its declaration, as a reference the
179
+ * test file wrote would be.
180
+ */
181
+ readonly inTestFiles: readonly Exemption[];
182
+ }
183
+
184
+ /** The records of {@link computeExemptions}, beside the test-file evidence it set aside. */
185
+ export function detectExemptions<Brand>(
186
+ input: DetectorInput<Brand>,
187
+ detectors: Detectors,
188
+ holding: Holding,
189
+ ): Detected {
163
190
  const visibility = new Map(input.held.symbols.map((symbol) => [symbol.id, symbol.visibility]));
164
191
  const found: Exemption[] = [];
192
+ const inTestFiles: Exemption[] = [];
165
193
  for (const exemptionClass of TS_EXEMPTION_CLASSES) {
166
194
  const detect = detectors.get(exemptionClass);
167
195
  if (detect === undefined || holding.disabled.has(exemptionClass)) {
@@ -172,18 +200,21 @@ export function computeExemptions<Brand>(
172
200
  if (!retainable(allowed, visibility.get(evidence.id))) {
173
201
  continue;
174
202
  }
175
- if (holding.mode.production && holding.testFiles.has(evidence.site.path)) {
176
- continue;
177
- }
178
- found.push({
203
+ const record = {
179
204
  id: evidence.id,
180
205
  class: exemptionClass,
181
206
  detail: evidence.detail,
182
207
  site: evidence.site,
183
- });
208
+ ...(evidence.whileLive === undefined ? {} : { whileLive: evidence.whileLive }),
209
+ };
210
+ if (holding.mode.production && holding.testFiles.has(evidence.site.path)) {
211
+ inTestFiles.push(record);
212
+ continue;
213
+ }
214
+ found.push(record);
184
215
  }
185
216
  }
186
- return exemptionsOf(found);
217
+ return { records: exemptionsOf(found), inTestFiles };
187
218
  }
188
219
 
189
220
  /** Two strings ordered bytewise. */
@@ -196,8 +227,8 @@ function compare(a: string, b: string): number {
196
227
 
197
228
  /**
198
229
  * Records as a run holds them: ordered by site, then class, then declaration, and one
199
- * per declaration, class and detail, the one at the first site. A fact found at many
200
- * sites, or by several projects, is one record.
230
+ * per declaration, class, detail and file it holds while live, the one at the first
231
+ * site. A fact found at many sites, or by several projects, is one record.
201
232
  */
202
233
  export function exemptionsOf(found: readonly Exemption[]): readonly Exemption[] {
203
234
  const ordered = [...found].sort(
@@ -205,7 +236,7 @@ export function exemptionsOf(found: readonly Exemption[]): readonly Exemption[]
205
236
  );
206
237
  const seen = new Set<string>();
207
238
  return ordered.filter((record) => {
208
- const fact = [record.id, record.class, record.detail].join("\u0000");
239
+ const fact = [record.id, record.class, record.detail, record.whileLive ?? ""].join("\u0000");
209
240
  if (seen.has(fact)) {
210
241
  return false;
211
242
  }
@@ -214,6 +245,21 @@ export function exemptionsOf(found: readonly Exemption[]): readonly Exemption[]
214
245
  });
215
246
  }
216
247
 
248
+ /**
249
+ * The records that hold while the component files at `live` are live: every record that
250
+ * holds whatever is live, and each one a live file's markup holds, as a record of the run.
251
+ */
252
+ export function holdingWhile(
253
+ records: readonly Exemption[],
254
+ live: ReadonlySet<string>,
255
+ ): readonly Exemption[] {
256
+ return exemptionsOf(
257
+ records.flatMap(({ whileLive, ...record }) =>
258
+ whileLive === undefined || live.has(whileLive) ? [record] : [],
259
+ ),
260
+ );
261
+ }
262
+
217
263
  /** The records per declaration, each declaration's in the order given. */
218
264
  function byDeclaration(records: readonly Exemption[]): ReadonlyMap<string, readonly Exemption[]> {
219
265
  const grouped = new Map<string, Exemption[]>();
@@ -229,17 +275,18 @@ function byDeclaration(records: readonly Exemption[]): ReadonlyMap<string, reado
229
275
  }
230
276
 
231
277
  /**
232
- * The records that held back a declaration some configuration of the run would
233
- * otherwise judge dead: each configuration's graph is swept once more with no
234
- * exemption and the marks it was given, and a record is retained when one of those
235
- * sweeps judged its declaration a candidate. A record that holds a declaration back in
236
- * one configuration is retained whatever another configuration answers about it, as a
237
- * run of that configuration alone would report the declaration without it. A record
238
- * on a declaration that a relation or a mark holds live in every configuration held
239
- * nothing back. The order is the declarations' site order, and for one declaration
240
- * the input's.
278
+ * The records that held back a declaration: one some configuration's graph, swept
279
+ * again with no exemption and its marks, judges a candidate, whatever another
280
+ * configuration answers, or one in `writeOnly`, which the run would report as written
281
+ * and never read without the record's read. A declaration a relation or a mark holds
282
+ * live in every configuration is held back by no record. The order is the
283
+ * declarations' site order, and for one declaration the input's.
241
284
  */
242
- export function retainedIn(matrix: Matrix, input: SweepInput): readonly Exemption[] {
285
+ export function retainedIn(
286
+ matrix: Matrix,
287
+ input: SweepInput,
288
+ writeOnly: ReadonlySet<string> = new Set(),
289
+ ): readonly Exemption[] {
243
290
  const exempt = input.exempt ?? [];
244
291
  if (exempt.length === 0) {
245
292
  return [];
@@ -250,7 +297,9 @@ export function retainedIn(matrix: Matrix, input: SweepInput): readonly Exemptio
250
297
  sweep(graph, bare).candidates.map((candidate) => candidate.id),
251
298
  ),
252
299
  );
253
- const held = byDeclaration(exempt.filter((record) => dead.has(record.id)));
300
+ const held = byDeclaration(
301
+ exempt.filter((record) => dead.has(record.id) || writeOnly.has(record.id)),
302
+ );
254
303
  return matrix.union.symbols.flatMap((symbol) => held.get(symbol.id) ?? []);
255
304
  }
256
305
 
package/src/file-facts.ts CHANGED
@@ -47,6 +47,8 @@ export interface FileFacts {
47
47
  * reference names is included by the file that names it.
48
48
  */
49
49
  readonly heldByInclusion: ReadonlySet<string>;
50
+ /** The paths below the target root of the files below a directory an applied row names as generated. */
51
+ readonly generated?: ReadonlySet<string>;
50
52
  }
51
53
 
52
54
  /** The line of the last character of one text, counted from one; an empty text is one line. */
@@ -8,7 +8,7 @@ import type { RunSweep } from "../analysis.ts";
8
8
  import { componentId, type Component } from "../components.ts";
9
9
  import type { Cascade, Confidence, Severity } from "../config.ts";
10
10
  import type { CompletedFinding, Finding, FindingComponent, PositionedSymbol } from "../finding.ts";
11
- import { OUTSIDE } from "../graph.ts";
11
+ import { OUTSIDE, type Graph } from "../graph.ts";
12
12
  import { KINDS, type KindRow } from "../kinds.ts";
13
13
  import { byPosition } from "../position.ts";
14
14
  import { FAMILY_KEY_LENGTH } from "../resolve.ts";
@@ -24,56 +24,47 @@ function capped(found: Confidence, ceiling: Confidence): Confidence {
24
24
  return RANK[found] <= RANK[ceiling] ? found : ceiling;
25
25
  }
26
26
 
27
- /** The kind whose default turns on what the run knows about the target's consumers. */
28
- const UNUSED_EXPORTED = "DS1001";
29
-
30
27
  /** Whether the run loaded every consumer the scope declared, which holds of a scope declaring none. */
31
28
  function everyDeclaredLoaded(boundary: Boundary): boolean {
32
29
  const { declared, loaded } = boundary.consumers;
33
30
  return declared.every((id) => loaded.includes(id));
34
31
  }
35
32
 
36
- /**
37
- * Whether a library's published API may have a caller the run cannot see: the target is
38
- * a library, and the run either holds no consumer information or did not load a consumer
39
- * the scope declared. A configuration declaring the consumer set complete holds consumer
40
- * information whether or not it declares a consumer, because it says no other exists.
41
- */
42
- function openWorld(input: EmitterInput): boolean {
43
- const known = input.config.consumersComplete || input.boundary.consumers.declared.length > 0;
44
- return input.config.targetKind === "library" && !(known && everyDeclaredLoaded(input.boundary));
45
- }
46
-
47
33
  /**
48
34
  * The severity the configuration gives one code: the key naming the code, else the key
49
- * naming its family, else the code's default. The unused-exported kind defaults to
50
- * `allow` while a library's published API is open to callers the run cannot see.
35
+ * naming its family, else the code's default.
51
36
  */
52
37
  function severityOf(input: EmitterInput, code: string, fallback: Severity): Severity {
53
38
  const { config } = input;
54
- const named = config.severity.get(code) ?? config.severity.get(code.slice(0, FAMILY_KEY_LENGTH));
55
- if (named !== undefined) {
56
- return named;
57
- }
58
- return code === UNUSED_EXPORTED && openWorld(input) ? "allow" : fallback;
39
+ return (
40
+ config.severity.get(code) ?? config.severity.get(code.slice(0, FAMILY_KEY_LENGTH)) ?? fallback
41
+ );
59
42
  }
60
43
 
61
44
  /**
62
- * The reachability class of one declaration of the run. A declaration of a library's
63
- * published API has callers outside the target: it is `certain` where the run loaded
64
- * every consumer the scope declared and declared at least one, `probable` where it
65
- * declared consumers and did not load them all, and `possible` where it declared none.
66
- * Whether the configuration declares the consumer set complete decides nothing here.
67
- * Every other declaration has every reference in the loaded program, a private member
68
- * and a `#private` name among them, and is `certain`.
45
+ * The declarations the roots name as a library's published API, each public member of a
46
+ * published type included.
69
47
  */
70
- function reachabilityClasses(input: EmitterInput): (id: string) => Confidence {
71
- const union = input.swept.matrix.union;
72
- const published = new Set(
48
+ function publishedIds(union: Graph): ReadonlySet<string> {
49
+ return new Set(
73
50
  union.rooted
74
51
  .filter((root) => root.kind === "published-api")
75
- .map((root) => union.symbols[root.at]?.id),
52
+ .flatMap((root) => union.symbols[root.at]?.id ?? []),
76
53
  );
54
+ }
55
+
56
+ /**
57
+ * The reachability class of one declaration of the run. A declaration of a library's
58
+ * published API, a public member of a published type included, has callers outside the
59
+ * target: it is `certain` where the run loaded every consumer the scope declared and
60
+ * declared at least one, `probable` where it declared consumers and did not load them
61
+ * all, and `possible` where it declared none. Whether the configuration declares the
62
+ * consumer set complete decides nothing here. Every other declaration has every reference
63
+ * in the loaded program, a private member and a `#private` name among them, and is
64
+ * `certain`.
65
+ */
66
+ function reachabilityClasses(input: EmitterInput): (id: string) => Confidence {
67
+ const published = publishedIds(input.swept.matrix.union);
77
68
  const { declared } = input.boundary.consumers;
78
69
  const outside: Confidence =
79
70
  declared.length === 0
@@ -155,8 +146,9 @@ export function completed(
155
146
  const classOf = reachabilityClasses(input);
156
147
  const componentOf = findingComponents(swept, input.config.reporters.cascade);
157
148
  let minted = swept.sweep.components.length;
149
+ const dead = new Set(swept.sweep.components.map((component) => component.id));
158
150
 
159
- return findings.map((finding): CompletedFinding => {
151
+ const done = findings.map((finding): CompletedFinding => {
160
152
  const row = kinds.get(finding.code);
161
153
  if (row === undefined) {
162
154
  throw new Error(`${finding.code} names no live row of the issue-kind vocabulary`);
@@ -192,6 +184,7 @@ export function completed(
192
184
  ? union.made[at]
193
185
  : { production: candidate.productionRefs, test: candidate.testRefs };
194
186
  const reachabilityClass = judged === undefined ? "certain" : classOf(judged.id);
187
+ const generated = input.files.generated?.has(finding.position.path) === true;
195
188
  return {
196
189
  code: finding.code,
197
190
  kind: row.name,
@@ -202,7 +195,7 @@ export function completed(
202
195
  confidence: capped(reachabilityClass, row.maxClass),
203
196
  ...(candidate === undefined ? {} : { livenessRelation: candidate.relation }),
204
197
  testOnly: judged !== undefined && counts?.production === 0 && counts.test > 0,
205
- generated: false,
198
+ generated,
206
199
  component,
207
200
  retainedBy: [],
208
201
  configurations:
@@ -211,12 +204,38 @@ export function completed(
211
204
  ? swept.matrix.configurations
212
205
  : (swept.matrix.heldIn[at] ?? swept.matrix.configurations)),
213
206
  consumersLoaded: input.boundary.consumers.loaded,
214
- fixability: row.fixability,
207
+ fixability: generated ? "none" : row.fixability,
215
208
  severity: severityOf(input, finding.code, row.defaultSeverity),
216
209
  message: finding.message,
217
210
  details: finding.details ?? {},
218
211
  };
219
212
  });
213
+ return componentCapped(done, dead);
214
+ }
215
+
216
+ /**
217
+ * The findings with every finding of a dead component capped by the lowest confidence
218
+ * among the component's root members, so the minimum confidence reports or withholds a
219
+ * component whole.
220
+ */
221
+ function componentCapped(
222
+ findings: readonly CompletedFinding[],
223
+ dead: ReadonlySet<string>,
224
+ ): readonly CompletedFinding[] {
225
+ const lowest = new Map<string, Confidence>();
226
+ for (const finding of findings) {
227
+ const { id, root } = finding.component;
228
+ if (root && dead.has(id)) {
229
+ const held = lowest.get(id);
230
+ lowest.set(id, held === undefined ? finding.confidence : capped(held, finding.confidence));
231
+ }
232
+ }
233
+ return findings.map((finding) => {
234
+ const cap = lowest.get(finding.component.id);
235
+ return cap === undefined || RANK[cap] >= RANK[finding.confidence]
236
+ ? finding
237
+ : { ...finding, confidence: cap };
238
+ });
220
239
  }
221
240
 
222
241
  /** Whether the dials withhold a finding by its own severity or confidence. */
@@ -1,6 +1,8 @@
1
1
  import { evaluateEdges, type EdgeEvaluation } from "../edges.ts";
2
2
  import type { CompletedFinding } from "../finding.ts";
3
+ import { liveFilesAt } from "../matrix.ts";
3
4
  import { ledgerOf, type Dials, type Ledger, type SuppressionRecord } from "../suppress.ts";
5
+ import { insideSkipped } from "../type-errors.ts";
4
6
  import { completed, dialsOf, reportable } from "./completion.ts";
5
7
  import { dependenciesAndModuleMachinery } from "./dependencies-and-module-machinery.ts";
6
8
  import type { Emitter, EmitterInput, Emitters } from "./emitter.ts";
@@ -40,6 +42,28 @@ function heldRefs(input: EmitterInput): ReadonlySet<string> {
40
42
  );
41
43
  }
42
44
 
45
+ /**
46
+ * The references of every top-level declaration of a live component file. The file's
47
+ * markup may use each of them, which no analysis of the markup can rule out, so the run
48
+ * reports nothing about one while the file is live.
49
+ */
50
+ function markupRefs(input: EmitterInput): ReadonlySet<string> {
51
+ const { swept } = input;
52
+ if (swept.componentFiles.length === 0) {
53
+ return new Set();
54
+ }
55
+ const live = new Set(
56
+ liveFilesAt(swept.matrix, swept.sweep, new Set(swept.componentFiles)).map(
57
+ (symbol) => symbol.id,
58
+ ),
59
+ );
60
+ return new Set(
61
+ swept.matrix.union.symbols
62
+ .filter((symbol) => live.has(symbol.parent))
63
+ .map((symbol) => symbol.ref),
64
+ );
65
+ }
66
+
43
67
  /** What the suppressions of a run swept under their marks are decided against. */
44
68
  export interface Suppressed {
45
69
  /** The same run swept with no mark, which is where a kind about liveness reports a marked declaration. */
@@ -109,9 +133,16 @@ export function decidedFindings(
109
133
  const would = [...unmarked, ...found];
110
134
  const ledger = ledgerOf(suppressed.records, would, dialsOver(input, found, suppressed, unmarked));
111
135
  const unanswered = heldRefs(input);
136
+ const markup = markupRefs(input);
112
137
  const kept = reportable(
113
138
  input,
114
- found.filter((finding) => !ledger.withheld(finding) && !unanswered.has(finding.symbol.ref)),
139
+ found.filter(
140
+ (finding) =>
141
+ !ledger.withheld(finding) &&
142
+ !unanswered.has(finding.symbol.ref) &&
143
+ !markup.has(finding.symbol.ref) &&
144
+ !insideSkipped(input.swept.skipped, finding.position),
145
+ ),
115
146
  );
116
147
  const evaluated = evaluateEdges(kept, input.boundary.edges, input.swept.matrix.union.symbols);
117
148
  return {
@@ -31,6 +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 inSupport: (id: string) => boolean;
34
35
  readonly candidateOf: (id: string) => Candidate | undefined;
35
36
  readonly implementationsOf: (id: string) => readonly PositionedSymbol[];
36
37
  } {
@@ -43,6 +44,7 @@ function indexed(input: EmitterInput): {
43
44
  return {
44
45
  symbolOf,
45
46
  inTest: (id) => union.test[union.at(id)] === true,
47
+ inSupport: (id) => union.support[union.at(id)] === true,
46
48
  candidateOf: (id) => candidates.get(id),
47
49
  implementationsOf: (id) =>
48
50
  [...(input.implementations.classes.get(id) ?? [])]
@@ -80,14 +82,19 @@ function findingOf(
80
82
  * interface, whose members fall with it unreported; and `DS1203`, a method of a live
81
83
  * interface no reference names, unless it is a marker, a method whose every
82
84
  * 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.
85
+ * subject, because a production sweep counts none of the references it can have, and
86
+ * neither is a test-support declaration test code references, which that code keeps live.
84
87
  */
85
88
  export const interfaces: Emitter = (input) => {
86
89
  const run = indexed(input);
87
90
  const found: Finding[] = [];
88
91
  for (const candidate of input.swept.sweep.candidates) {
89
92
  const symbol = run.symbolOf(candidate.id);
90
- if (symbol === undefined || run.inTest(candidate.id)) {
93
+ if (
94
+ symbol === undefined ||
95
+ run.inTest(candidate.id) ||
96
+ (run.inSupport(candidate.id) && candidate.testRefs > 0)
97
+ ) {
91
98
  continue;
92
99
  }
93
100
  if (symbol.kind === "interface") {