@cplieger/deadset-ts 8.0.0-dev.1 → 8.0.0-dev.11

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 (276) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/mapper-connection.js +2 -1
  3. package/dist/bin/node-host.js +24 -5
  4. package/dist/src/analysis.js +181 -341
  5. package/dist/src/augmentations.js +106 -13
  6. package/dist/src/automatic-types.js +10 -29
  7. package/dist/src/build-scripts.js +425 -0
  8. package/dist/src/component-layer.js +4 -2
  9. package/dist/src/components.js +2 -3
  10. package/dist/src/config.js +6 -82
  11. package/dist/src/configuration-files.js +28 -176
  12. package/dist/src/configured-declarations.js +15 -20
  13. package/dist/src/conformance-record.js +1 -1
  14. package/dist/src/consumers.js +3 -30
  15. package/dist/src/contextual-uses.js +56 -9
  16. package/dist/src/contract/config.generated.js +558 -0
  17. package/dist/src/contract/contract.generated.js +1244 -0
  18. package/dist/src/contract/report.generated.js +862 -0
  19. package/dist/src/convention-rows.js +73 -3
  20. package/dist/src/conventions.js +36 -66
  21. package/dist/src/dependencies.js +103 -40
  22. package/dist/src/edges.js +38 -122
  23. package/dist/src/element-access.js +4 -6
  24. package/dist/src/entry-point-gaps.js +2 -8
  25. package/dist/src/entry-points.js +11 -27
  26. package/dist/src/enum-group.js +3 -10
  27. package/dist/src/erased-stores.js +4 -6
  28. package/dist/src/exempt-classes.js +1 -63
  29. package/dist/src/exempt.js +21 -40
  30. package/dist/src/failure.js +36 -0
  31. package/dist/src/file-facts.js +2 -25
  32. package/dist/src/findings/completion.js +139 -72
  33. package/dist/src/findings/dependencies-and-module-machinery.js +4 -5
  34. package/dist/src/findings/emitters.js +5 -5
  35. package/dist/src/findings/interfaces.js +4 -5
  36. package/dist/src/findings/intra-function.js +13 -10
  37. package/dist/src/findings/non-code-artifacts.js +7 -8
  38. package/dist/src/findings/reads-and-writes.js +16 -13
  39. package/dist/src/findings/self-check.js +10 -12
  40. package/dist/src/findings/unused-declarations.js +41 -32
  41. package/dist/src/findings/visibility-narrowing.js +19 -16
  42. package/dist/src/findings-pass.js +8 -15
  43. package/dist/src/framework-lifecycle.js +7 -7
  44. package/dist/src/fs/document.js +52 -0
  45. package/dist/src/fs/manifest.js +72 -0
  46. package/dist/src/fs/tree.js +88 -0
  47. package/dist/src/generated-file.js +7 -4
  48. package/dist/src/git-ignore.js +39 -26
  49. package/dist/src/graph.js +5 -3
  50. package/dist/src/html-entries.js +6 -33
  51. package/dist/src/in-operands.js +2 -1
  52. package/dist/src/index.js +1 -1
  53. package/dist/src/injection-container.js +1 -1
  54. package/dist/src/intra-function-parts.js +63 -43
  55. package/dist/src/inventory.js +2 -21
  56. package/dist/src/javascript-file.js +33 -0
  57. package/dist/src/json-document.js +198 -67
  58. package/dist/src/keyof-comparisons.js +4 -6
  59. package/dist/src/kinds.js +3 -366
  60. package/dist/src/load/classify.js +270 -0
  61. package/dist/src/load/diagnostics.js +80 -0
  62. package/dist/src/load/discover.js +277 -0
  63. package/dist/src/{emit-map.js → load/emit-map.js} +1 -1
  64. package/dist/src/load/expected-module.js +136 -0
  65. package/dist/src/load/load.js +27 -0
  66. package/dist/src/load/module-lookup.js +37 -0
  67. package/dist/src/load/plan.js +313 -0
  68. package/dist/src/load/specifiers.js +115 -0
  69. package/dist/src/load/synthesize.js +140 -0
  70. package/dist/src/load/test-globals.js +207 -0
  71. package/dist/src/load/test-joins.js +82 -0
  72. package/dist/src/{workspace-manifests.js → load/workspace-manifests.js} +4 -4
  73. package/dist/src/{workspace-programs.js → load/workspace-programs.js} +89 -66
  74. package/dist/src/{workspace-resolution.js → load/workspace-resolution.js} +23 -65
  75. package/dist/src/{workspace-run.js → load/workspace-run.js} +7 -7
  76. package/dist/src/{workspace.js → load/workspace.js} +62 -124
  77. package/dist/src/manifest.js +49 -54
  78. package/dist/src/matrix.js +76 -4
  79. package/dist/src/mdx-entries.js +3 -26
  80. package/dist/src/outside-tests.js +1 -1
  81. package/dist/src/partial.js +2 -1
  82. package/dist/src/paths.js +26 -15
  83. package/dist/src/position.js +2 -1
  84. package/dist/src/print.js +11 -79
  85. package/dist/src/product.js +13 -0
  86. package/dist/src/query.js +1 -0
  87. package/dist/src/ref.js +1 -0
  88. package/dist/src/references.js +48 -30
  89. package/dist/src/report-schema.js +51 -0
  90. package/dist/src/report.js +57 -41
  91. package/dist/src/reporters/annotations.js +15 -8
  92. package/dist/src/reporters/sarif.js +16 -10
  93. package/dist/src/reporters/template.js +4 -19
  94. package/dist/src/resolve.js +245 -729
  95. package/dist/src/roots.js +273 -87
  96. package/dist/src/run.js +66 -77
  97. package/dist/src/schema.js +320 -343
  98. package/dist/src/scope.js +38 -115
  99. package/dist/src/serialization-contract.js +595 -21
  100. package/dist/src/session.js +34 -29
  101. package/dist/src/source-files.js +1 -1
  102. package/dist/src/suppress-file.js +25 -28
  103. package/dist/src/suppress-inline.js +3 -2
  104. package/dist/src/suppress.js +33 -51
  105. package/dist/src/sweep.js +7 -9
  106. package/dist/src/symbol-keys.js +3 -5
  107. package/dist/src/template-field.js +9 -19
  108. package/dist/src/test-support.js +8 -11
  109. package/dist/src/text/compare.js +26 -0
  110. package/dist/src/text/glob.js +194 -0
  111. package/dist/src/text/named-files.js +54 -0
  112. package/dist/src/text/shell.js +159 -0
  113. package/dist/src/text/yaml.js +393 -0
  114. package/dist/src/type-errors.js +11 -232
  115. package/dist/src/unseen-loads.js +100 -0
  116. package/dist/src/unwrap.js +23 -0
  117. package/dist/src/usage.js +10 -0
  118. package/dist/src/verbs/analyze.js +166 -126
  119. package/dist/src/verbs/describe.js +9 -16
  120. package/dist/src/verbs/explain.js +10 -7
  121. package/dist/src/verbs/print-config.js +2 -1
  122. package/dist/src/verbs/print-projects.js +6 -4
  123. package/dist/src/verbs/print-retained.js +4 -2
  124. package/dist/src/verbs/print-roots.js +4 -2
  125. package/dist/src/verbs/verb.js +1 -5
  126. package/dist/src/verbs/version.js +14 -3
  127. package/dist/src/workflow-steps.js +62 -64
  128. package/jsr.json +1 -1
  129. package/package.json +5 -4
  130. package/src/analysis.ts +245 -502
  131. package/src/augmentations.ts +135 -17
  132. package/src/automatic-types.ts +11 -29
  133. package/src/build-scripts.ts +543 -0
  134. package/src/component-layer.ts +4 -2
  135. package/src/components.ts +2 -4
  136. package/src/config.ts +31 -302
  137. package/src/configuration-files.ts +34 -179
  138. package/src/configured-declarations.ts +18 -23
  139. package/src/conformance-record.ts +1 -1
  140. package/src/consumers.ts +6 -57
  141. package/src/contextual-uses.ts +64 -12
  142. package/src/contract/config.generated.ts +818 -0
  143. package/src/contract/contract.generated.ts +1308 -0
  144. package/src/contract/report.generated.ts +1237 -0
  145. package/src/convention-rows.ts +85 -3
  146. package/src/conventions.ts +46 -84
  147. package/src/dependencies.ts +118 -46
  148. package/src/edges.ts +41 -140
  149. package/src/element-access.ts +4 -7
  150. package/src/entry-point-gaps.ts +3 -9
  151. package/src/entry-points.ts +12 -38
  152. package/src/enum-group.ts +2 -11
  153. package/src/erased-stores.ts +3 -7
  154. package/src/exempt-classes.ts +1 -75
  155. package/src/exempt.ts +36 -50
  156. package/src/failure.ts +63 -0
  157. package/src/file-facts.ts +9 -24
  158. package/src/finding.ts +14 -9
  159. package/src/findings/boundary.ts +2 -0
  160. package/src/findings/completion.ts +181 -84
  161. package/src/findings/dependencies-and-module-machinery.ts +4 -7
  162. package/src/findings/emitter.ts +8 -9
  163. package/src/findings/emitters.ts +7 -7
  164. package/src/findings/interfaces.ts +4 -6
  165. package/src/findings/intra-function.ts +13 -11
  166. package/src/findings/non-code-artifacts.ts +9 -11
  167. package/src/findings/reads-and-writes.ts +20 -16
  168. package/src/findings/self-check.ts +11 -17
  169. package/src/findings/unused-declarations.ts +45 -32
  170. package/src/findings/visibility-narrowing.ts +20 -17
  171. package/src/findings-pass.ts +10 -19
  172. package/src/framework-lifecycle.ts +7 -7
  173. package/src/fs/document.ts +70 -0
  174. package/src/fs/manifest.ts +92 -0
  175. package/src/fs/tree.ts +110 -0
  176. package/src/generated-file.ts +7 -4
  177. package/src/git-ignore.ts +50 -26
  178. package/src/graph.ts +5 -3
  179. package/src/host.ts +12 -5
  180. package/src/html-entries.ts +6 -33
  181. package/src/in-operands.ts +2 -1
  182. package/src/index.ts +1 -1
  183. package/src/injection-container.ts +1 -1
  184. package/src/intra-function-parts.ts +73 -44
  185. package/src/inventory.ts +2 -20
  186. package/src/javascript-file.ts +37 -0
  187. package/src/json-document.ts +241 -76
  188. package/src/keyof-comparisons.ts +3 -7
  189. package/src/kinds.ts +5 -413
  190. package/src/load/classify.ts +397 -0
  191. package/src/load/diagnostics.ts +144 -0
  192. package/src/load/discover.ts +455 -0
  193. package/src/{emit-map.ts → load/emit-map.ts} +1 -1
  194. package/src/load/expected-module.ts +188 -0
  195. package/src/load/load.ts +73 -0
  196. package/src/load/module-lookup.ts +50 -0
  197. package/src/load/plan.ts +504 -0
  198. package/src/load/specifiers.ts +168 -0
  199. package/src/load/synthesize.ts +211 -0
  200. package/src/load/test-globals.ts +251 -0
  201. package/src/load/test-joins.ts +98 -0
  202. package/src/{workspace-manifests.ts → load/workspace-manifests.ts} +5 -5
  203. package/src/{workspace-programs.ts → load/workspace-programs.ts} +128 -85
  204. package/src/{workspace-resolution.ts → load/workspace-resolution.ts} +33 -76
  205. package/src/{workspace-run.ts → load/workspace-run.ts} +18 -16
  206. package/src/{workspace.ts → load/workspace.ts} +90 -134
  207. package/src/manifest.ts +81 -75
  208. package/src/matrix.ts +122 -4
  209. package/src/mdx-entries.ts +3 -26
  210. package/src/outside-tests.ts +1 -1
  211. package/src/partial.ts +3 -2
  212. package/src/paths.ts +27 -15
  213. package/src/position.ts +2 -1
  214. package/src/print.ts +12 -92
  215. package/src/product.ts +18 -0
  216. package/src/query.ts +11 -0
  217. package/src/ref.ts +1 -0
  218. package/src/references.ts +52 -31
  219. package/src/report-schema.ts +68 -0
  220. package/src/report.ts +101 -224
  221. package/src/reporters/annotations.ts +22 -11
  222. package/src/reporters/json.ts +1 -1
  223. package/src/reporters/reporter.ts +2 -4
  224. package/src/reporters/reporters.ts +1 -1
  225. package/src/reporters/sarif.ts +29 -16
  226. package/src/reporters/template.ts +4 -20
  227. package/src/reporters/text.ts +3 -3
  228. package/src/resolve.ts +283 -1114
  229. package/src/roots.ts +339 -104
  230. package/src/run.ts +75 -91
  231. package/src/schema.ts +391 -388
  232. package/src/scope.ts +46 -138
  233. package/src/serialization-contract.ts +763 -22
  234. package/src/session.ts +70 -54
  235. package/src/source-files.ts +1 -1
  236. package/src/suppress-file.ts +24 -33
  237. package/src/suppress-inline.ts +2 -2
  238. package/src/suppress.ts +43 -58
  239. package/src/sweep.ts +23 -9
  240. package/src/symbol-keys.ts +2 -5
  241. package/src/template-field.ts +15 -25
  242. package/src/test-support.ts +17 -13
  243. package/src/text/compare.ts +27 -0
  244. package/src/text/glob.ts +210 -0
  245. package/src/text/named-files.inputs.json +1522 -0
  246. package/src/text/named-files.ts +68 -0
  247. package/src/text/shell.ts +161 -0
  248. package/src/text/yaml.ts +423 -0
  249. package/src/type-errors.ts +18 -288
  250. package/src/unseen-loads.ts +134 -0
  251. package/src/unwrap.ts +34 -0
  252. package/src/usage.ts +10 -0
  253. package/src/verbs/analyze.ts +224 -162
  254. package/src/verbs/describe.ts +14 -15
  255. package/src/verbs/explain.ts +10 -11
  256. package/src/verbs/print-config.ts +2 -1
  257. package/src/verbs/print-projects.ts +6 -4
  258. package/src/verbs/print-retained.ts +4 -2
  259. package/src/verbs/print-roots.ts +4 -2
  260. package/src/verbs/verb.ts +2 -8
  261. package/src/verbs/version.ts +15 -3
  262. package/src/workflow-steps.ts +74 -64
  263. package/dist/src/base-url.js +0 -18
  264. package/dist/src/discover.js +0 -468
  265. package/dist/src/glob.js +0 -65
  266. package/dist/src/pnpm-workspace.js +0 -120
  267. package/dist/src/setup-failure.js +0 -23
  268. package/dist/src/test-joins.js +0 -101
  269. package/dist/src/version.js +0 -7
  270. package/src/base-url.ts +0 -24
  271. package/src/discover.ts +0 -653
  272. package/src/glob.ts +0 -67
  273. package/src/pnpm-workspace.ts +0 -135
  274. package/src/setup-failure.ts +0 -39
  275. package/src/test-joins.ts +0 -120
  276. package/src/version.ts +0 -7
@@ -4,39 +4,38 @@
4
4
  * together with every finding that falls with a root they withhold.
5
5
  */
6
6
 
7
+ import {
8
+ CODE,
9
+ FAMILY_KEY_LENGTH,
10
+ type Confidence,
11
+ type Severity,
12
+ KIND_ROWS,
13
+ type KindRow,
14
+ } from "../contract/contract.generated.ts";
7
15
  import type { RunSweep } from "../analysis.ts";
8
16
  import { componentId, type Component } from "../components.ts";
9
17
  import { deadComponents } from "../matrix.ts";
10
- import type { Cascade, Confidence, Severity } from "../config.ts";
18
+ import type { Cascade } from "../config.ts";
11
19
  import type { CompletedFinding, Finding, FindingComponent, PositionedSymbol } from "../finding.ts";
12
20
  import { OUTSIDE, type Graph } from "../graph.ts";
13
21
  import type { SymbolKind } from "../inventory.ts";
14
- import { KINDS, type KindRow } from "../kinds.ts";
22
+ import { KINDS } from "../kinds.ts";
15
23
  import { byPosition, positionKey } from "../position.ts";
16
24
  import { declarationKey } from "../ref.ts";
17
- import { FAMILY_KEY_LENGTH } from "../resolve.ts";
18
25
  import { isPartSubject, isRowSubject, type Dials } from "../suppress.ts";
19
26
  import type { Candidate } from "../sweep.ts";
20
27
  import type { Boundary } from "./boundary.ts";
21
28
  import type { EmitterInput } from "./emitter.ts";
22
29
 
23
- /** The unused-file code, whose deletion removes every use its file writes. */
24
- const FILE_NEVER_IMPORTED = "DS1502";
25
-
26
30
  /**
27
- * The codes whose subject the run that counts test references holds live, as
28
- * finding.schema.json lists them, so a test file's candidate reported under one carries
29
- * none of a dead declaration's sweep facts.
31
+ * The codes whose subject the run that counts test references holds live, so a test
32
+ * file's candidate reported under one carries none of a dead declaration's sweep facts.
30
33
  */
31
- const LIVE_SUBJECT: ReadonlySet<string> = new Set([
32
- "DS1101",
33
- "DS1102",
34
- "DS1104",
35
- "DS1204",
36
- "DS1301",
37
- ]);
34
+ const LIVE_SUBJECT: ReadonlySet<string> = new Set(
35
+ KIND_ROWS.filter((row) => row.holdsLive).map((row) => row.code),
36
+ );
38
37
 
39
- /** The fixability grammar/analysis.md "Fixability" gives a finding about a held declaration. */
38
+ /** The fixability grammar/analysis/fixability.md gives a finding about a held declaration. */
40
39
  const MANUAL = "manual";
41
40
 
42
41
  /** The member kinds an object property can declare, which an `in` expression tests by name. */
@@ -66,10 +65,9 @@ function everyDeclaredLoaded(boundary: Boundary): boolean {
66
65
  * naming its family, else the code's default.
67
66
  */
68
67
  function severityOf(input: EmitterInput, code: string, fallback: Severity): Severity {
69
- const { config } = input;
70
- return (
71
- config.severity.get(code) ?? config.severity.get(code.slice(0, FAMILY_KEY_LENGTH)) ?? fallback
72
- );
68
+ const { severity } = input.config;
69
+ const key = [code, code.slice(0, FAMILY_KEY_LENGTH)].find((one) => Object.hasOwn(severity, one));
70
+ return key === undefined ? fallback : (severity[key] ?? fallback);
73
71
  }
74
72
 
75
73
  /**
@@ -112,10 +110,12 @@ function testedByIn(input: EmitterInput): ReadonlySet<string> {
112
110
  * published API, a public member of a published type included, is `certain` where the
113
111
  * run loaded every consumer the scope declared and declared one, `probable` where one did
114
112
  * not load, and `possible` where it declared none, whatever the configuration says of the
115
- * consumer set. A member an `in` expression tests by name is `possible`.
113
+ * consumer set. A member an `in` expression tests by name, and one a function outside the
114
+ * analysis fills, is `possible`.
116
115
  */
117
116
  function reachabilityClasses(input: EmitterInput): (id: string) => Confidence {
118
117
  const tested = testedByIn(input);
118
+ const filled = input.boundary.filledOutside ?? new Set<string>();
119
119
  const published = publishedIds(input.swept.matrix.union);
120
120
  const { declared } = input.boundary.consumers;
121
121
  const outside: Confidence =
@@ -125,13 +125,41 @@ function reachabilityClasses(input: EmitterInput): (id: string) => Confidence {
125
125
  ? "certain"
126
126
  : "probable";
127
127
  return (id) => {
128
- if (tested.has(id)) {
128
+ if (tested.has(id) || filled.has(id)) {
129
129
  return "possible";
130
130
  }
131
131
  return published.has(id) ? outside : "certain";
132
132
  };
133
133
  }
134
134
 
135
+ /**
136
+ * Whether one test of dead code references a target symbol that is published API of a run
137
+ * that declares no consumer, which outside programs the run cannot see may use, so the
138
+ * target counts as live for the test and the test is no test of dead code.
139
+ */
140
+ export function testsUnseenAPI(input: EmitterInput): (id: string) => boolean {
141
+ if (input.boundary.consumers.declared.length > 0) {
142
+ return () => false;
143
+ }
144
+ const union = input.swept.matrix.union;
145
+ const published = publishedIds(union);
146
+ return (id) => testTargets(union, id).some((target) => published.has(target));
147
+ }
148
+
149
+ /** The target symbols one test of dead code references, by id: production declarations the sweep judges. */
150
+ function testTargets(union: Graph, id: string): readonly string[] {
151
+ return (union.out[union.at(id)] ?? []).flatMap((edge) => {
152
+ const target = union.symbols[edge.to];
153
+ const testCode = union.test[edge.to] === true || union.support[edge.to] === true;
154
+ return target !== undefined &&
155
+ !testCode &&
156
+ union.ambient[edge.to] !== true &&
157
+ union.subject[edge.to] === true
158
+ ? [target.id]
159
+ : [];
160
+ });
161
+ }
162
+
135
163
  /**
136
164
  * The confidence ceiling of a test of dead code: the lowest reachability class among the
137
165
  * target symbols it references, because its deadness is theirs, whether or not they share
@@ -145,40 +173,59 @@ function testCeilings(
145
173
  const tests = new Set(
146
174
  input.swept.sweep.candidates.filter((one) => one.testOfDeadCode).map((one) => one.id),
147
175
  );
148
- return (id) => {
149
- let lowest: Confidence = "certain";
150
- if (!tests.has(id)) {
151
- return lowest;
152
- }
153
- for (const edge of union.out[union.at(id)] ?? []) {
154
- const target = union.symbols[edge.to];
155
- const testCode = union.test[edge.to] === true || union.support[edge.to] === true;
156
- if (
157
- target !== undefined &&
158
- !testCode &&
159
- union.ambient[edge.to] !== true &&
160
- union.subject[edge.to] === true
161
- ) {
162
- lowest = capped(lowest, classOf(target.id));
163
- }
176
+ return (id) =>
177
+ tests.has(id)
178
+ ? testTargets(union, id).reduce<Confidence>(
179
+ (lowest, target) => capped(lowest, classOf(target)),
180
+ "certain",
181
+ )
182
+ : "certain";
183
+ }
184
+
185
+ /**
186
+ * The subjects dead only because production code only tests use is: each declaration the
187
+ * sweep marks test-used, each `DS1602` file of such code, and each test of dead code every
188
+ * target of which is one. Nothing they hold references a dead declaration outside them, so
189
+ * the rest of a component they share is reported without them.
190
+ */
191
+ function testUsedSubjects(input: EmitterInput): {
192
+ readonly declarations: ReadonlySet<string>;
193
+ readonly files: ReadonlySet<string>;
194
+ } {
195
+ const { matrix, sweep } = input.swept;
196
+ const union = matrix.union;
197
+ const used = new Set(sweep.candidates.filter((one) => one.testUsed).map((one) => one.id));
198
+ for (const test of sweep.candidates.filter((one) => one.testOfDeadCode)) {
199
+ const targets = testTargets(union, test.id);
200
+ if (targets.length > 0 && targets.every((target) => used.has(target))) {
201
+ used.add(test.id);
164
202
  }
165
- return lowest;
166
- };
203
+ }
204
+ const files = new Set(
205
+ union.symbols.flatMap((symbol, at) =>
206
+ matrix.testUsed[at] === true ? [symbol.position.path] : [],
207
+ ),
208
+ );
209
+ return { declarations: used, files };
167
210
  }
168
211
 
169
212
  /**
170
213
  * The ceiling the roots the run found put on one dead subject: a subject other declarations
171
- * reference, found dead only because no root reaches them, is `probable` unless
172
- * the configuration declares the roots complete, because a root the analysis cannot see
173
- * would make it live; every other declaration has none below `certain`.
214
+ * reference, found dead only because no root reaches them, is `probable` unless the
215
+ * configuration declares the roots complete and no configuration holding the subject loads
216
+ * a module the analysis cannot resolve, because a root the analysis cannot see would make
217
+ * it live; every other declaration has none below `certain`.
174
218
  */
175
219
  function rootsCeiling(
176
220
  input: EmitterInput,
177
221
  finding: Finding,
178
222
  candidate: Candidate | undefined,
223
+ configurations: readonly string[],
179
224
  ): Confidence {
180
225
  const unreachable = candidate?.relation === "reachability" || finding.unreachableOnly === true;
181
- return unreachable && !input.config.ts.rootsComplete ? "probable" : "certain";
226
+ const unseen = new Set((input.files.unseenLoads ?? []).map((load) => load.configuration));
227
+ const complete = input.config.ts.roots_complete && !configurations.some((one) => unseen.has(one));
228
+ return unreachable && !complete ? "probable" : "certain";
182
229
  }
183
230
 
184
231
  /**
@@ -196,9 +243,28 @@ function findingComponents(
196
243
  held.set(member, component);
197
244
  }
198
245
  }
246
+ const listed = membersListed(swept);
247
+ return (id) => {
248
+ const component = held.get(id);
249
+ return component === undefined
250
+ ? undefined
251
+ : {
252
+ id: component.id,
253
+ root: component.roots.includes(id),
254
+ symbolCount: component.members.length,
255
+ deletableLines: component.deletableLines,
256
+ ...(cascade === "full" ? { members: listed(component.members) } : {}),
257
+ };
258
+ };
259
+ }
260
+
261
+ /** The members of a component as the run lists them, positioned and in the canonical order. */
262
+ function membersListed(
263
+ swept: RunSweep,
264
+ ): (members: readonly string[]) => readonly PositionedSymbol[] {
199
265
  const symbols = new Map(swept.matrix.union.symbols.map((symbol) => [symbol.id, symbol]));
200
- const listed = (component: Component): readonly PositionedSymbol[] =>
201
- component.members
266
+ return (members) =>
267
+ members
202
268
  .flatMap((member) => {
203
269
  const symbol = symbols.get(member);
204
270
  return symbol === undefined
@@ -215,22 +281,10 @@ function findingComponents(
215
281
  ];
216
282
  })
217
283
  .sort((a, b) => byPosition(a.position, b.position));
218
- return (id) => {
219
- const component = held.get(id);
220
- return component === undefined
221
- ? undefined
222
- : {
223
- id: component.id,
224
- root: component.roots.includes(id),
225
- symbolCount: component.members.length,
226
- deletableLines: component.deletableLines,
227
- ...(cascade === "full" ? { members: listed(component) } : {}),
228
- };
229
- };
230
284
  }
231
285
 
232
286
  /**
233
- * The run's dead components with each file a `DS1502` finding reports dead among them, so
287
+ * The run's dead components with each file a `DS1602` finding reports dead among them, so
234
288
  * the file's declarations fall with it, and with the components each subject that is no
235
289
  * declaration falls with joined into one.
236
290
  */
@@ -244,7 +298,7 @@ function runComponents(
244
298
  } {
245
299
  const union = swept.matrix.union;
246
300
  const reported = new Set(
247
- findings.filter((one) => one.code === FILE_NEVER_IMPORTED).map((one) => one.position.path),
301
+ findings.filter((one) => one.code === CODE.fileNeverImported).map((one) => one.position.path),
248
302
  );
249
303
  const files = new Map(
250
304
  union.symbols.flatMap((symbol, at) =>
@@ -321,18 +375,20 @@ export function completed(
321
375
  const candidates = new Map(swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
322
376
  const classOf = reachabilityClasses(input);
323
377
  const ceilingOf = testCeilings(input, classOf);
378
+ const testUsed = testUsedSubjects(input);
324
379
  const { components, computed, files } = runComponents(swept, findings);
325
380
  const componentOf = findingComponents(swept, components, input.config.reporters.cascade);
326
381
  const held = new Set(components.filter((one) => one.held).flatMap((one) => one.members));
327
382
  // A join keeps its first component's identifier, so a minted one counts past every computed one.
328
383
  let minted = computed;
329
384
  const dead = new Set(components.map((component) => component.id));
330
- const fallingWith = new Map<string, number>();
331
- for (const { heldByTests } of swept.sweep.candidates) {
385
+ const fallingWith = new Map<string, string[]>();
386
+ for (const { id, heldByTests } of swept.sweep.candidates) {
332
387
  if (heldByTests !== undefined) {
333
- fallingWith.set(heldByTests, (fallingWith.get(heldByTests) ?? 0) + 1);
388
+ fallingWith.set(heldByTests, [...(fallingWith.get(heldByTests) ?? []), id]);
334
389
  }
335
390
  }
391
+ const listed = membersListed(swept);
336
392
 
337
393
  const done = findings.map((finding): CompletedFinding => {
338
394
  const row = kinds.get(finding.code);
@@ -361,7 +417,7 @@ export function completed(
361
417
  const fallsWith = finding.fallsWith?.[0];
362
418
  const joined = fallsWith === undefined ? undefined : componentOf(fallsWith);
363
419
  const file =
364
- finding.code === FILE_NEVER_IMPORTED ? files.get(finding.position.path) : undefined;
420
+ finding.code === CODE.fileNeverImported ? files.get(finding.position.path) : undefined;
365
421
  const found =
366
422
  joined !== undefined
367
423
  ? { ...joined, root: false }
@@ -383,19 +439,50 @@ export function completed(
383
439
  let component = found;
384
440
  if (component === undefined) {
385
441
  minted += 1;
442
+ const falling = symbol?.kind === "interface" ? fallingWith.get(symbol.id) : undefined;
386
443
  component = {
387
444
  id: componentId(minted),
388
445
  root: true,
389
- symbolCount: symbol?.kind === "interface" ? (fallingWith.get(symbol.id) ?? 1) : 1,
446
+ symbolCount: falling?.length ?? 1,
390
447
  deletableLines: 0,
448
+ ...(input.config.reporters.cascade !== "full"
449
+ ? {}
450
+ : falling === undefined
451
+ ? {
452
+ members: [
453
+ {
454
+ ref: finding.symbol.ref,
455
+ name: finding.symbol.name,
456
+ position: finding.position,
457
+ },
458
+ ],
459
+ }
460
+ : { members: listed(falling) }),
391
461
  };
392
462
  }
393
463
  const counts =
394
464
  candidate === undefined
395
465
  ? union.made[at]
396
466
  : { production: candidate.productionRefs, test: candidate.testRefs };
397
- const reachabilityClass =
467
+ const ownClass =
398
468
  judged === undefined ? (finding.reachabilityClass ?? "certain") : classOf(judged.id);
469
+ const used =
470
+ judged !== undefined
471
+ ? candidate !== undefined && testUsed.declarations.has(judged.id)
472
+ : finding.code === CODE.fileNeverImported && testUsed.files.has(finding.position.path);
473
+ const reachabilityClass = used && candidate?.testOfDeadCode !== true ? "possible" : ownClass;
474
+ const configurations =
475
+ candidate?.configurations ??
476
+ (judged === undefined && !part
477
+ ? swept.matrix.configurations
478
+ : (swept.matrix.heldIn[at] ?? swept.matrix.configurations));
479
+ const untested = capped(
480
+ capped(
481
+ capped(ownClass, row.maxClass),
482
+ rootsCeiling(input, finding, candidate, configurations),
483
+ ),
484
+ judged === undefined ? "certain" : ceilingOf(judged.id),
485
+ );
399
486
  const generated = input.files.generated?.has(finding.position.path) === true;
400
487
  const fixability = generated
401
488
  ? "none"
@@ -410,7 +497,7 @@ export function completed(
410
497
  ? undefined
411
498
  : candidate !== undefined
412
499
  ? input.dependencies.lastUses.get(candidate.id)
413
- : finding.code === FILE_NEVER_IMPORTED
500
+ : finding.code === CODE.fileNeverImported
414
501
  ? input.dependencies.fileLastUses.get(finding.position.path)
415
502
  : undefined;
416
503
  return {
@@ -420,20 +507,14 @@ export function completed(
420
507
  position: finding.position,
421
508
  symbol: finding.symbol,
422
509
  reachabilityClass,
423
- confidence: capped(
424
- capped(capped(reachabilityClass, row.maxClass), rootsCeiling(input, finding, candidate)),
425
- judged === undefined ? "certain" : ceilingOf(judged.id),
426
- ),
510
+ confidence: used ? "possible" : untested,
511
+ ...(used ? { componentConfidence: untested } : {}),
427
512
  ...(candidate === undefined ? {} : { livenessRelation: candidate.relation }),
428
513
  testOnly: judged !== undefined && counts?.production === 0 && counts.test > 0,
429
514
  generated,
430
515
  component,
431
516
  retainedBy: [],
432
- configurations:
433
- candidate?.configurations ??
434
- (judged === undefined && !part
435
- ? swept.matrix.configurations
436
- : (swept.matrix.heldIn[at] ?? swept.matrix.configurations)),
517
+ configurations,
437
518
  consumersLoaded: input.boundary.consumers.loaded,
438
519
  fixability,
439
520
  severity: severityOf(input, finding.code, row.defaultSeverity),
@@ -450,7 +531,7 @@ export function completed(
450
531
  /**
451
532
  * The findings with every finding of a dead component capped by the lowest confidence
452
533
  * among the component's root members, so the minimum confidence reports or withholds a
453
- * component whole.
534
+ * component whole but for the findings test-used code lowers on their own.
454
535
  */
455
536
  function componentCapped(
456
537
  findings: readonly CompletedFinding[],
@@ -461,17 +542,29 @@ function componentCapped(
461
542
  const { id, root } = finding.component;
462
543
  if (root && dead.has(id)) {
463
544
  const held = lowest.get(id);
464
- lowest.set(id, held === undefined ? finding.confidence : capped(held, finding.confidence));
545
+ const own = componentConfidenceOf(finding);
546
+ lowest.set(id, held === undefined ? own : capped(held, own));
465
547
  }
466
548
  }
467
549
  return findings.map((finding) => {
468
550
  const cap = lowest.get(finding.component.id);
469
- return cap === undefined || RANK[cap] >= RANK[finding.confidence]
470
- ? finding
471
- : { ...finding, confidence: cap };
551
+ if (cap === undefined) {
552
+ return finding;
553
+ }
554
+ const own = finding.componentConfidence;
555
+ return {
556
+ ...finding,
557
+ confidence: capped(finding.confidence, cap),
558
+ ...(own === undefined ? {} : { componentConfidence: capped(own, cap) }),
559
+ };
472
560
  });
473
561
  }
474
562
 
563
+ /** The confidence a finding gives its component's cap and the dials. */
564
+ function componentConfidenceOf(finding: CompletedFinding): Confidence {
565
+ return finding.componentConfidence ?? finding.confidence;
566
+ }
567
+
475
568
  /** Whether the dials withhold a finding by its own severity or confidence. */
476
569
  function dialed(
477
570
  finding: Pick<CompletedFinding, "severity" | "confidence">,
@@ -491,10 +584,14 @@ export function dialsOf(
491
584
  input: EmitterInput,
492
585
  findings: readonly CompletedFinding[],
493
586
  ): Dials<CompletedFinding> {
494
- const minConfidence = input.config.analysis.minConfidence;
587
+ const minConfidence = input.config.analysis.min_confidence;
495
588
  const components = new Set(
496
589
  findings
497
- .filter((finding) => finding.component.root && dialed(finding, minConfidence))
590
+ .filter(
591
+ (finding) =>
592
+ finding.component.root &&
593
+ dialed({ ...finding, confidence: componentConfidenceOf(finding) }, minConfidence),
594
+ )
498
595
  .map((finding) => finding.component.id),
499
596
  );
500
597
  return {
@@ -1,13 +1,10 @@
1
+ import { CODE } from "../contract/contract.generated.ts";
1
2
  import { declarationKey, type Dependencies } from "../dependencies.ts";
2
3
  import type { Finding } from "../finding.ts";
3
4
  import { renderRef, type DependencySection } from "../ref.ts";
4
5
  import type { Emitter, EmitterInput } from "./emitter.ts";
5
6
  import { nonCodeArtifacts } from "./non-code-artifacts.ts";
6
7
 
7
- const UNUSED_DEPENDENCY = "DS1601";
8
-
9
- const FILE_NEVER_IMPORTED = "DS1502";
10
-
11
8
  const MESSAGES: Readonly<Record<DependencySection, string>> = {
12
9
  dependency: "no import in the target needs the declared dependency",
13
10
  "dev-dependency": "no import in the target needs the declared development dependency",
@@ -46,7 +43,7 @@ function deadCode(input: EmitterInput): DeadCode {
46
43
  );
47
44
  for (const finding of nonCodeArtifacts(input)) {
48
45
  const id = files.get(finding.position.path);
49
- if (finding.code === FILE_NEVER_IMPORTED && id !== undefined) {
46
+ if (finding.code === CODE.fileNeverImported && id !== undefined) {
50
47
  dead.add(id);
51
48
  if (finding.unreachableOnly === true) {
52
49
  reachedOnly.add(id);
@@ -81,7 +78,7 @@ function unusedDependencies(input: EmitterInput, dependencies: Dependencies): Fi
81
78
  const atPossible = unbuilt || users?.some((id) => published.has(id)) === true;
82
79
  return [
83
80
  {
84
- code: UNUSED_DEPENDENCY,
81
+ code: CODE.unusedDependency,
85
82
  position: dependency.position,
86
83
  symbol: {
87
84
  ref: renderRef(dependency.manifest, {
@@ -105,6 +102,6 @@ function unusedDependencies(input: EmitterInput, dependencies: Dependencies): Fi
105
102
  });
106
103
  }
107
104
 
108
- /** The findings of the dependencies-and-module-machinery family, `DS1600` to `DS1699`. */
105
+ /** The findings of the dependencies family, `DS1700` to `DS1799`. */
109
106
  export const dependenciesAndModuleMachinery: Emitter = (input) =>
110
107
  unusedDependencies(input, input.dependencies);
@@ -1,5 +1,5 @@
1
1
  import type { RunSweep } from "../analysis.ts";
2
- import type { Config } from "../config.ts";
2
+ import type { ResolvedConfig } from "../config.ts";
3
3
  import type { Dependencies } from "../dependencies.ts";
4
4
  import type { FileFacts } from "../file-facts.ts";
5
5
  import type { Finding } from "../finding.ts";
@@ -11,10 +11,10 @@ import type { SelfCheckFacts } from "./self-check.ts";
11
11
 
12
12
  /**
13
13
  * What every emitter reads: the configuration the run resolved, the run's sweep, and
14
- * the facts about the run each kind family reads beside the sweep.
14
+ * the facts about the run each emitter reads beside the sweep.
15
15
  */
16
16
  export interface EmitterInput {
17
- readonly config: Config;
17
+ readonly config: ResolvedConfig;
18
18
  /** The production sweep a report is built from, beside the matrix it judged. */
19
19
  readonly swept: RunSweep;
20
20
  /** The declarations carrying the deprecation marker in any configuration of the run. */
@@ -29,18 +29,17 @@ export interface EmitterInput {
29
29
  readonly files: FileFacts;
30
30
  /** What can reach the target from outside it: consumers, declared edges, and `exports`. */
31
31
  readonly boundary: Boundary;
32
- /** The run's suppression outcomes and configured roots, which the self-check family reports. */
32
+ /** The run's suppression outcomes and configured roots, which the configuration family reports. */
33
33
  readonly selfCheck: SelfCheckFacts;
34
- /** The parts of the run's declarations, which the intra-function family reports. */
34
+ /** The parts of the run's declarations, which the inside-functions family reports. */
35
35
  readonly intraFunction: IntraFunctionFacts;
36
36
  }
37
37
 
38
38
  /**
39
- * One kind family's rule over one run: every finding of the codes in its family's range
40
- * of the Contract's code space, whatever severity and minimum confidence the
41
- * configuration sets, in no particular order.
39
+ * One emitter's rule over one run: every finding of the codes it reports, whatever
40
+ * severity and minimum confidence the configuration sets, in no particular order.
42
41
  */
43
42
  export type Emitter = (input: EmitterInput) => readonly Finding[];
44
43
 
45
- /** The emitter of each kind family, by the family's name in the Contract, in code order. */
44
+ /** Every emitter this analyzer runs, by the name its tests select it by. */
46
45
  export type Emitters = ReadonlyMap<string, Emitter>;
@@ -1,4 +1,4 @@
1
- import type { Confidence } from "../config.ts";
1
+ import type { Confidence } from "../contract/contract.generated.ts";
2
2
  import { evaluateEdges, type EdgeEvaluation } from "../edges.ts";
3
3
  import type { CompletedFinding } from "../finding.ts";
4
4
  import { liveFilesAt } from "../matrix.ts";
@@ -16,7 +16,7 @@ import { selfCheck } from "./self-check.ts";
16
16
  import { fallenMembers, unusedDeclarations } from "./unused-declarations.ts";
17
17
  import { visibilityNarrowing } from "./visibility-narrowing.ts";
18
18
 
19
- /** Every kind family this analyzer reports, each by its emitter, in code order. */
19
+ /** Every emitter this analyzer runs, by the name its tests select it by. */
20
20
  export const EMITTERS: Emitters = new Map<string, Emitter>([
21
21
  ["unused-declarations", unusedDeclarations],
22
22
  ["visibility-narrowing", visibilityNarrowing],
@@ -76,7 +76,7 @@ interface Suppressed {
76
76
 
77
77
  /** What the emitter table answers about one run. */
78
78
  interface DecidedFindings {
79
- /** Every finding the run reports, each family's in table order. */
79
+ /** Every finding the run reports, each emitter's in table order. */
80
80
  readonly findings: readonly CompletedFinding[];
81
81
  /** One evaluation per declared edge side of this language, ordered by edge, then by side. */
82
82
  readonly evaluations: readonly EdgeEvaluation<CompletedFinding>[];
@@ -91,7 +91,7 @@ interface DecidedFindings {
91
91
  /** How many findings the minimum confidence withheld at each confidence. */
92
92
  export type Withheld = Readonly<Record<Confidence, number>>;
93
93
 
94
- /** Every family's findings over one sweep, and those of any further emitter, completed by one step. */
94
+ /** Every emitter's findings over one sweep, and those of any further emitter, completed by one step. */
95
95
  function completedOver(
96
96
  input: EmitterInput,
97
97
  ...more: readonly Emitter[]
@@ -124,8 +124,8 @@ function dialsOver(
124
124
 
125
125
  /**
126
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
127
+ * emitter'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 emitter
129
129
  * reports withholds what another reports in its component, and every declared edge
130
130
  * evaluated over what remains, so a pending finding is in its evaluation alone.
131
131
  */
@@ -145,7 +145,7 @@ function atPossible(input: EmitterInput): EmitterInput {
145
145
  const { config } = input;
146
146
  return {
147
147
  ...input,
148
- config: { ...config, analysis: { ...config.analysis, minConfidence: "possible" } },
148
+ config: { ...config, analysis: { ...config.analysis, min_confidence: "possible" } },
149
149
  };
150
150
  }
151
151
 
@@ -1,12 +1,10 @@
1
+ import { CODE } from "../contract/contract.generated.ts";
1
2
  import type { Finding, FindingPosition, PositionedSymbol } from "../finding.ts";
2
3
  import type { InventorySymbol } from "../inventory.ts";
3
4
  import { byPosition } from "../position.ts";
4
5
  import type { MatrixCandidate } from "../matrix.ts";
5
6
  import type { Emitter, EmitterInput } from "./emitter.ts";
6
7
 
7
- const UNUSED_INTERFACE = "DS1201";
8
- const UNCALLED_INTERFACE_METHOD = "DS1203";
9
-
10
8
  /** What each finding says, by what the sweep found about the subject's references. */
11
9
  const UNUSED_MESSAGE = "nothing in the target names the interface as a type";
12
10
  const UNUSED_DEAD_MESSAGE =
@@ -69,7 +67,7 @@ function findingOf(
69
67
 
70
68
  /**
71
69
  * The findings of the interfaces family, `DS1200` to `DS1299`: `DS1201`, a dead
72
- * interface, whose members fall with it unreported; and `DS1203`, a method of a live
70
+ * interface, whose members fall with it unreported; and `DS1202`, a method of a live
73
71
  * interface no reference names, unless it is a marker, a method whose every
74
72
  * implementation has an empty body. A test file's declaration is neither kind's subject,
75
73
  * because a production sweep counts none of the references it can have, save an interface
@@ -86,7 +84,7 @@ export const interfaces: Emitter = (input) => {
86
84
  if (symbol.kind === "interface") {
87
85
  found.push(
88
86
  findingOf(
89
- UNUSED_INTERFACE,
87
+ CODE.unusedInterface,
90
88
  symbol,
91
89
  unusedMessage(candidate),
92
90
  run.implementationsOf(candidate.id),
@@ -104,7 +102,7 @@ export const interfaces: Emitter = (input) => {
104
102
  }
105
103
  found.push(
106
104
  findingOf(
107
- UNCALLED_INTERFACE_METHOD,
105
+ CODE.uncalledInterfaceMethod,
108
106
  symbol,
109
107
  UNCALLED_MESSAGE,
110
108
  run.implementationsOf(symbol.parent),