@cplieger/deadset-ts 7.2.0-dev.4 → 8.0.0-dev.10

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 (288) 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 +245 -309
  5. package/dist/src/augmentations.js +124 -18
  6. package/dist/src/automatic-types.js +170 -0
  7. package/dist/src/build-scripts.js +425 -0
  8. package/dist/src/component-layer.js +4 -2
  9. package/dist/src/components.js +9 -9
  10. package/dist/src/config.js +6 -81
  11. package/dist/src/configuration-files.js +78 -188
  12. package/dist/src/configured-declarations.js +15 -20
  13. package/dist/src/conformance-record.js +2 -2
  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 +146 -1
  20. package/dist/src/conventions.js +136 -70
  21. package/dist/src/decorator.js +21 -18
  22. package/dist/src/dependencies.js +280 -62
  23. package/dist/src/edges.js +38 -122
  24. package/dist/src/element-access.js +4 -6
  25. package/dist/src/entry-point-gaps.js +9 -18
  26. package/dist/src/entry-points.js +106 -21
  27. package/dist/src/enum-group.js +3 -10
  28. package/dist/src/erased-stores.js +4 -6
  29. package/dist/src/exempt-classes.js +1 -63
  30. package/dist/src/exempt.js +21 -40
  31. package/dist/src/failure.js +36 -0
  32. package/dist/src/file-facts.js +2 -25
  33. package/dist/src/findings/completion.js +225 -63
  34. package/dist/src/findings/dependencies-and-module-machinery.js +72 -27
  35. package/dist/src/findings/emitters.js +6 -6
  36. package/dist/src/findings/interfaces.js +4 -5
  37. package/dist/src/findings/intra-function.js +53 -19
  38. package/dist/src/findings/non-code-artifacts.js +71 -39
  39. package/dist/src/findings/reads-and-writes.js +16 -13
  40. package/dist/src/findings/self-check.js +10 -12
  41. package/dist/src/findings/unused-declarations.js +57 -28
  42. package/dist/src/findings/visibility-narrowing.js +40 -18
  43. package/dist/src/findings-pass.js +8 -15
  44. package/dist/src/framework-lifecycle.js +7 -7
  45. package/dist/src/fs/document.js +52 -0
  46. package/dist/src/fs/manifest.js +72 -0
  47. package/dist/src/fs/tree.js +88 -0
  48. package/dist/src/generated-file.js +18 -4
  49. package/dist/src/git-ignore.js +99 -0
  50. package/dist/src/graph.js +9 -4
  51. package/dist/src/html-entries.js +6 -33
  52. package/dist/src/import-bindings.js +97 -0
  53. package/dist/src/in-operands.js +2 -1
  54. package/dist/src/index.js +1 -1
  55. package/dist/src/indexed-access.js +130 -28
  56. package/dist/src/injection-container.js +1 -1
  57. package/dist/src/interface-satisfaction.js +30 -12
  58. package/dist/src/intra-function-parts.js +341 -69
  59. package/dist/src/inventory.js +54 -29
  60. package/dist/src/javascript-file.js +33 -0
  61. package/dist/src/json-document.js +198 -67
  62. package/dist/src/keyof-comparisons.js +4 -6
  63. package/dist/src/kinds.js +3 -366
  64. package/dist/src/load/classify.js +270 -0
  65. package/dist/src/load/diagnostics.js +80 -0
  66. package/dist/src/load/discover.js +277 -0
  67. package/dist/src/{emit-map.js → load/emit-map.js} +1 -1
  68. package/dist/src/load/expected-module.js +136 -0
  69. package/dist/src/load/load.js +27 -0
  70. package/dist/src/load/module-lookup.js +37 -0
  71. package/dist/src/load/plan.js +313 -0
  72. package/dist/src/load/specifiers.js +115 -0
  73. package/dist/src/load/synthesize.js +140 -0
  74. package/dist/src/load/test-globals.js +207 -0
  75. package/dist/src/load/test-joins.js +82 -0
  76. package/dist/src/{workspace-manifests.js → load/workspace-manifests.js} +4 -4
  77. package/dist/src/{workspace-programs.js → load/workspace-programs.js} +88 -61
  78. package/dist/src/{workspace-resolution.js → load/workspace-resolution.js} +60 -59
  79. package/dist/src/{workspace-run.js → load/workspace-run.js} +7 -7
  80. package/dist/src/{workspace.js → load/workspace.js} +62 -124
  81. package/dist/src/manifest.js +49 -54
  82. package/dist/src/matrix.js +94 -13
  83. package/dist/src/mdx-entries.js +174 -0
  84. package/dist/src/module-declarations.js +9 -13
  85. package/dist/src/outside-tests.js +1 -1
  86. package/dist/src/overrides.js +41 -10
  87. package/dist/src/partial.js +2 -1
  88. package/dist/src/paths.js +26 -15
  89. package/dist/src/position.js +2 -1
  90. package/dist/src/print.js +11 -78
  91. package/dist/src/product.js +13 -0
  92. package/dist/src/query.js +1 -0
  93. package/dist/src/ref.js +11 -0
  94. package/dist/src/references.js +52 -31
  95. package/dist/src/report-schema.js +51 -0
  96. package/dist/src/report.js +59 -41
  97. package/dist/src/reporters/annotations.js +15 -8
  98. package/dist/src/reporters/sarif.js +16 -10
  99. package/dist/src/reporters/template.js +4 -19
  100. package/dist/src/resolve.js +245 -727
  101. package/dist/src/roots.js +374 -116
  102. package/dist/src/run.js +66 -77
  103. package/dist/src/schema.js +320 -342
  104. package/dist/src/scope.js +38 -115
  105. package/dist/src/serialization-contract.js +649 -29
  106. package/dist/src/session.js +41 -29
  107. package/dist/src/source-files.js +40 -15
  108. package/dist/src/suppress-file.js +34 -29
  109. package/dist/src/suppress-inline.js +21 -4
  110. package/dist/src/suppress.js +40 -56
  111. package/dist/src/sweep.js +32 -17
  112. package/dist/src/symbol-keys.js +105 -6
  113. package/dist/src/template-field.js +9 -19
  114. package/dist/src/test-support.js +119 -18
  115. package/dist/src/text/compare.js +26 -0
  116. package/dist/src/text/glob.js +194 -0
  117. package/dist/src/text/named-files.js +54 -0
  118. package/dist/src/text/shell.js +159 -0
  119. package/dist/src/text/yaml.js +393 -0
  120. package/dist/src/type-errors.js +27 -221
  121. package/dist/src/type-parameter-reads.js +172 -0
  122. package/dist/src/unseen-loads.js +100 -0
  123. package/dist/src/unwrap.js +23 -0
  124. package/dist/src/usage.js +10 -0
  125. package/dist/src/verbs/analyze.js +181 -125
  126. package/dist/src/verbs/describe.js +9 -16
  127. package/dist/src/verbs/explain.js +14 -9
  128. package/dist/src/verbs/print-config.js +2 -1
  129. package/dist/src/verbs/print-projects.js +6 -4
  130. package/dist/src/verbs/print-retained.js +4 -2
  131. package/dist/src/verbs/print-roots.js +4 -2
  132. package/dist/src/verbs/verb.js +1 -5
  133. package/dist/src/verbs/version.js +14 -3
  134. package/dist/src/workflow-steps.js +128 -33
  135. package/jsr.json +1 -1
  136. package/package.json +5 -4
  137. package/src/analysis.ts +333 -457
  138. package/src/augmentations.ts +152 -23
  139. package/src/automatic-types.ts +228 -0
  140. package/src/build-scripts.ts +543 -0
  141. package/src/component-layer.ts +4 -2
  142. package/src/components.ts +9 -10
  143. package/src/config.ts +31 -299
  144. package/src/configuration-files.ts +100 -191
  145. package/src/configured-declarations.ts +18 -23
  146. package/src/conformance-record.ts +2 -2
  147. package/src/consumers.ts +6 -57
  148. package/src/contextual-uses.ts +64 -12
  149. package/src/contract/config.generated.ts +818 -0
  150. package/src/contract/contract.generated.ts +1308 -0
  151. package/src/contract/report.generated.ts +1237 -0
  152. package/src/convention-rows.ts +186 -1
  153. package/src/conventions.ts +166 -90
  154. package/src/decorator.ts +28 -18
  155. package/src/dependencies.ts +356 -78
  156. package/src/edges.ts +41 -140
  157. package/src/element-access.ts +4 -7
  158. package/src/entry-point-gaps.ts +11 -20
  159. package/src/entry-points.ts +124 -28
  160. package/src/enum-group.ts +2 -11
  161. package/src/erased-stores.ts +3 -7
  162. package/src/exempt-classes.ts +1 -75
  163. package/src/exempt.ts +36 -50
  164. package/src/failure.ts +63 -0
  165. package/src/file-facts.ts +9 -24
  166. package/src/finding.ts +24 -9
  167. package/src/findings/boundary.ts +2 -0
  168. package/src/findings/completion.ts +297 -77
  169. package/src/findings/dependencies-and-module-machinery.ts +90 -31
  170. package/src/findings/emitter.ts +8 -9
  171. package/src/findings/emitters.ts +8 -8
  172. package/src/findings/interfaces.ts +4 -6
  173. package/src/findings/intra-function.ts +64 -20
  174. package/src/findings/non-code-artifacts.ts +88 -50
  175. package/src/findings/reads-and-writes.ts +20 -16
  176. package/src/findings/self-check.ts +11 -17
  177. package/src/findings/unused-declarations.ts +65 -28
  178. package/src/findings/visibility-narrowing.ts +51 -19
  179. package/src/findings-pass.ts +10 -19
  180. package/src/framework-lifecycle.ts +7 -7
  181. package/src/fs/document.ts +70 -0
  182. package/src/fs/manifest.ts +92 -0
  183. package/src/fs/tree.ts +110 -0
  184. package/src/generated-file.ts +23 -4
  185. package/src/git-ignore.ts +122 -0
  186. package/src/graph.ts +11 -3
  187. package/src/host.ts +17 -14
  188. package/src/html-entries.ts +6 -33
  189. package/src/import-bindings.ts +128 -0
  190. package/src/in-operands.ts +2 -1
  191. package/src/index.ts +1 -1
  192. package/src/indexed-access.ts +152 -30
  193. package/src/injection-container.ts +1 -1
  194. package/src/interface-satisfaction.ts +33 -13
  195. package/src/intra-function-parts.ts +449 -85
  196. package/src/inventory.ts +73 -27
  197. package/src/javascript-file.ts +37 -0
  198. package/src/json-document.ts +241 -76
  199. package/src/keyof-comparisons.ts +3 -7
  200. package/src/kinds.ts +5 -413
  201. package/src/load/classify.ts +397 -0
  202. package/src/load/diagnostics.ts +144 -0
  203. package/src/load/discover.ts +455 -0
  204. package/src/{emit-map.ts → load/emit-map.ts} +1 -1
  205. package/src/load/expected-module.ts +188 -0
  206. package/src/load/load.ts +73 -0
  207. package/src/load/module-lookup.ts +50 -0
  208. package/src/load/plan.ts +504 -0
  209. package/src/load/specifiers.ts +168 -0
  210. package/src/load/synthesize.ts +211 -0
  211. package/src/load/test-globals.ts +251 -0
  212. package/src/load/test-joins.ts +98 -0
  213. package/src/{workspace-manifests.ts → load/workspace-manifests.ts} +5 -5
  214. package/src/{workspace-programs.ts → load/workspace-programs.ts} +127 -80
  215. package/src/{workspace-resolution.ts → load/workspace-resolution.ts} +73 -64
  216. package/src/{workspace-run.ts → load/workspace-run.ts} +18 -16
  217. package/src/{workspace.ts → load/workspace.ts} +90 -134
  218. package/src/manifest.ts +81 -75
  219. package/src/matrix.ts +154 -18
  220. package/src/mdx-entries.ts +218 -0
  221. package/src/module-declarations.ts +11 -13
  222. package/src/outside-tests.ts +1 -1
  223. package/src/overrides.ts +51 -12
  224. package/src/partial.ts +3 -2
  225. package/src/paths.ts +27 -15
  226. package/src/position.ts +2 -1
  227. package/src/print.ts +12 -91
  228. package/src/product.ts +18 -0
  229. package/src/query.ts +11 -0
  230. package/src/ref.ts +13 -0
  231. package/src/references.ts +62 -32
  232. package/src/report-schema.ts +68 -0
  233. package/src/report.ts +105 -215
  234. package/src/reporters/annotations.ts +22 -11
  235. package/src/reporters/json.ts +1 -1
  236. package/src/reporters/reporter.ts +2 -4
  237. package/src/reporters/reporters.ts +1 -1
  238. package/src/reporters/sarif.ts +29 -16
  239. package/src/reporters/template.ts +4 -20
  240. package/src/reporters/text.ts +3 -3
  241. package/src/resolve.ts +283 -1111
  242. package/src/roots.ts +490 -137
  243. package/src/run.ts +75 -91
  244. package/src/schema.ts +391 -387
  245. package/src/scope.ts +46 -138
  246. package/src/serialization-contract.ts +818 -26
  247. package/src/session.ts +77 -54
  248. package/src/source-files.ts +67 -17
  249. package/src/suppress-file.ts +36 -34
  250. package/src/suppress-inline.ts +20 -3
  251. package/src/suppress.ts +52 -63
  252. package/src/sweep.ts +53 -18
  253. package/src/symbol-keys.ts +131 -7
  254. package/src/template-field.ts +15 -25
  255. package/src/test-support.ts +155 -18
  256. package/src/text/compare.ts +27 -0
  257. package/src/text/glob.ts +210 -0
  258. package/src/text/named-files.inputs.json +1522 -0
  259. package/src/text/named-files.ts +68 -0
  260. package/src/text/shell.ts +161 -0
  261. package/src/text/yaml.ts +423 -0
  262. package/src/type-errors.ts +35 -273
  263. package/src/type-parameter-reads.ts +227 -0
  264. package/src/unseen-loads.ts +134 -0
  265. package/src/unwrap.ts +34 -0
  266. package/src/usage.ts +10 -0
  267. package/src/verbs/analyze.ts +245 -161
  268. package/src/verbs/describe.ts +14 -15
  269. package/src/verbs/explain.ts +15 -13
  270. package/src/verbs/print-config.ts +2 -1
  271. package/src/verbs/print-projects.ts +6 -4
  272. package/src/verbs/print-retained.ts +4 -2
  273. package/src/verbs/print-roots.ts +4 -2
  274. package/src/verbs/verb.ts +2 -8
  275. package/src/verbs/version.ts +15 -3
  276. package/src/workflow-steps.ts +149 -34
  277. package/dist/src/discover.js +0 -347
  278. package/dist/src/glob.js +0 -65
  279. package/dist/src/pnpm-workspace.js +0 -120
  280. package/dist/src/setup-failure.js +0 -23
  281. package/dist/src/test-joins.js +0 -101
  282. package/dist/src/version.js +0 -7
  283. package/src/discover.ts +0 -510
  284. package/src/glob.ts +0 -67
  285. package/src/pnpm-workspace.ts +0 -135
  286. package/src/setup-failure.ts +0 -39
  287. package/src/test-joins.ts +0 -120
  288. package/src/version.ts +0 -7
@@ -6,26 +6,18 @@
6
6
  * which records held something back. It names no class's detector: the composition
7
7
  * hands it the table of the classes it has.
8
8
  */
9
- import { ConfigError } from "./config.js";
10
- import { EXEMPTION_CLASSES, isExemptionClass, TS_EXEMPTION_CLASSES, typescriptVisibilityOf, } from "./exempt-classes.js";
9
+ import { EXEMPTION_CLASSES } from "./contract/contract.generated.js";
10
+ import { TS_EXEMPTION_CLASSES, typescriptVisibilityOf, } from "./exempt-classes.js";
11
11
  import { byPosition, positionKey } from "./position.js";
12
12
  import { Unanswerable } from "./query.js";
13
13
  import { sweep } from "./sweep.js";
14
+ import { bytewise } from "./text/compare.js";
14
15
  /**
15
- * The classes `exemptions.disabled` switches off. A name is refused unless the
16
- * vocabulary holds it; a class that runs on another language only is admitted and
17
- * switches nothing off here, so one configuration serves every analyzer.
16
+ * The classes `exemptions.disabled` switches off. A class that runs on another language
17
+ * only switches nothing off here, so one configuration serves every analyzer.
18
18
  */
19
19
  export function disabledClasses(config) {
20
- const disabled = new Set();
21
- for (const name of config.exemptionsDisabled) {
22
- if (!isExemptionClass(name)) {
23
- const known = EXEMPTION_CLASSES.map((row) => row.class).join(", ");
24
- throw new ConfigError("malformed", "exemptions.disabled", `exemptions.disabled: ${JSON.stringify(name)} is not an exemption class: the classes are ${known}`);
25
- }
26
- disabled.add(name);
27
- }
28
- return disabled;
20
+ return new Set(config.exemptions.disabled);
29
21
  }
30
22
  /**
31
23
  * Whether a class whose row states `allowed` may retain a member of one visibility. A
@@ -40,20 +32,8 @@ function retainable(allowed, visibility) {
40
32
  }
41
33
  return true;
42
34
  }
43
- /**
44
- * The kinds of declaration each class can hold back, which a detector that stops holds
45
- * back whole. A class with no row here holds back every declaration but a file.
46
- */
47
- const RETAINABLE = new Map([
48
- ["interface-satisfaction", new Set(["method", "class-member"])],
49
- ["enum-group", new Set(["enum-member"])],
50
- ["injection-container", new Set(["method", "class-member"])],
51
- ["framework-lifecycle", new Set(["method", "class-member"])],
52
- [
53
- "serialization-contract",
54
- new Set(["method", "class-member", "interface-method", "type-member"]),
55
- ],
56
- ]);
35
+ /** The kinds of declaration each class can hold back, which a detector that stops holds back whole. */
36
+ const RETAINABLE = new Map(EXEMPTION_CLASSES.flatMap((row) => "ts" in row.retainsKinds ? [[row.class, new Set(row.retainsKinds.ts)]] : []));
57
37
  /** The detail of a record a detector that could not finish holds. */
58
38
  const UNANSWERED_DETAIL = "kept live by a question the checker did not answer";
59
39
  /** Whether one record is held by a detector that could not finish. */
@@ -76,7 +56,7 @@ function detected(exemptionClass, detect, input) {
76
56
  }
77
57
  const kinds = RETAINABLE.get(exemptionClass);
78
58
  return input.held.symbols
79
- .filter((symbol) => (kinds === undefined ? symbol.kind !== "file" : kinds.has(symbol.kind)))
59
+ .filter((symbol) => kinds?.has(symbol.kind) === true)
80
60
  .map((symbol) => ({ id: symbol.id, detail: UNANSWERED_DETAIL, site: symbol.position }));
81
61
  }
82
62
  }
@@ -106,11 +86,19 @@ export function evidenceRecords(input, detectors, disabled) {
106
86
  site: evidence.site,
107
87
  ...(evidence.whileLive === undefined ? {} : { whileLive: evidence.whileLive }),
108
88
  ...(evidence.implementing === undefined ? {} : { implementing: evidence.implementing }),
89
+ ...(evidence.whileWritten === undefined ? {} : { whileWritten: evidence.whileWritten }),
109
90
  });
110
91
  }
111
92
  }
112
93
  return found;
113
94
  }
95
+ /**
96
+ * The records that hold given the declarations a write the mode counts stores into: one
97
+ * that holds only while written holds where its declaration is among them.
98
+ */
99
+ export function writtenRecords(records, written) {
100
+ return records.flatMap(({ whileWritten, ...record }) => whileWritten === undefined || written.has(record.id) ? [record] : []);
101
+ }
114
102
  /** The records that hold under one mode, beside the evidence a production mode found in a test file. */
115
103
  export function heldRecords(records, holding) {
116
104
  const found = [];
@@ -125,20 +113,13 @@ export function heldRecords(records, holding) {
125
113
  }
126
114
  return { records: exemptionsOf(found), inTestFiles };
127
115
  }
128
- /** Two strings ordered bytewise. */
129
- function compare(a, b) {
130
- if (a === b) {
131
- return 0;
132
- }
133
- return a < b ? -1 : 1;
134
- }
135
116
  /**
136
117
  * Records as a run holds them: ordered by site, then class, then declaration, and one
137
118
  * per declaration, class, detail and file it holds while live, the one at the first
138
119
  * site. A fact found at many sites, or by several projects, is one record.
139
120
  */
140
121
  export function exemptionsOf(found) {
141
- const ordered = [...found].sort((a, b) => byPosition(a.site, b.site) || compare(a.class, b.class) || compare(a.id, b.id));
122
+ const ordered = [...found].sort((a, b) => byPosition(a.site, b.site) || bytewise(a.class, b.class) || bytewise(a.id, b.id));
142
123
  const seen = new Set();
143
124
  return ordered.filter((record) => {
144
125
  const fact = [record.id, record.class, record.detail, record.whileLive ?? ""].join("\u0000");
@@ -189,8 +170,8 @@ export function retainedIn(matrix, input, writeOnly = new Set()) {
189
170
  return matrix.union.symbols.flatMap((symbol) => held.get(symbol.id) ?? []);
190
171
  }
191
172
  /**
192
- * One line per held-back declaration, in site order: its reference, then each record's
193
- * class, site and detail, all separated by tabs.
173
+ * One line per held-back declaration, in site order: its reference and position, then
174
+ * each record's class, site and detail, all separated by tabs.
194
175
  */
195
176
  export function retainedLines(symbols, retained) {
196
177
  const held = byDeclaration(retained);
@@ -204,6 +185,6 @@ export function retainedLines(symbols, retained) {
204
185
  positionKey(record.site),
205
186
  record.detail,
206
187
  ]);
207
- return [[symbol.ref, ...fields].join("\t")];
188
+ return [[symbol.ref, positionKey(symbol.position), ...fields].join("\t")];
208
189
  });
209
190
  }
@@ -0,0 +1,36 @@
1
+ import { FAILURE_CLASSES, FAILURE_PREFIXES } from "./contract/contract.generated.js";
2
+ import { PRODUCT } from "./product.js";
3
+ /** The failure as a class line of the error stream, opened by its group's prefix. */
4
+ export function failureLine(failure) {
5
+ return `${FAILURE_PREFIXES[FAILURE_CLASSES[failure.failureClass]]}${failure.failureClass}: ${failure.detail}`;
6
+ }
7
+ /**
8
+ * The lines of the error stream that name the failures: the first one's class line, the run's
9
+ * only one, then each further failure as its class and detail with no group prefix.
10
+ */
11
+ export function failureLines([first, ...further]) {
12
+ return [failureLine(first), ...further.map((one) => `${one.failureClass}: ${one.detail}`)];
13
+ }
14
+ /**
15
+ * A run the analysis cannot answer. It ends the run with the failure code and no finding
16
+ * list, printing the compiler's located errors and then the failures' lines.
17
+ */
18
+ export class FailureExit extends Error {
19
+ failures;
20
+ /** The compiler's errors behind the failures, in the order it reported them. */
21
+ diagnostics;
22
+ constructor(failures, diagnostics = []) {
23
+ super(failureLines(failures).join("\n"));
24
+ this.name = "FailureExit";
25
+ this.failures = failures;
26
+ this.diagnostics = diagnostics;
27
+ }
28
+ }
29
+ /** A failure exit of one failure. */
30
+ export function failureExit(failureClass, detail, diagnostics = []) {
31
+ return new FailureExit([{ failureClass, detail }], diagnostics);
32
+ }
33
+ /** A fault of this analyzer itself, `fault` saying what it cannot do. */
34
+ export function productFault(fault) {
35
+ return failureExit("product-fault", `${PRODUCT} ${fault}: reinstall ${PRODUCT}, and report the fault to its maintainers if a reinstalled ${PRODUCT} fails the same way`);
36
+ }
@@ -3,10 +3,10 @@
3
3
  * file below the target root, whether or not a project includes it, and the files a
4
4
  * program holds by inclusion rather than by an import.
5
5
  */
6
- import { IGNORED_DIRECTORIES } from "./discover.js";
7
6
  import { packageScope } from "./inventory.js";
8
7
  import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.js";
9
8
  import { renderRef } from "./ref.js";
9
+ import { filesBelow } from "./fs/tree.js";
10
10
  /** A file name the compiler reads as a source file. */
11
11
  const SOURCE_FILE = /\.(?:ts|tsx|mts|cts)$/u;
12
12
  /** A file name the compiler reads as a declaration file, which no build emits code from. */
@@ -30,30 +30,7 @@ function lastLine(text) {
30
30
  * that cannot be read is skipped, as discovery skips it.
31
31
  */
32
32
  export function sourcePaths(host, targetRoot) {
33
- const found = [];
34
- const pending = [""];
35
- for (let dir = pending.pop(); dir !== undefined; dir = pending.pop()) {
36
- let entries;
37
- try {
38
- entries = host.readDirectory(joinPath(targetRoot, dir));
39
- }
40
- catch {
41
- continue;
42
- }
43
- for (const entry of entries) {
44
- const path = dir === "" ? entry.name : `${dir}/${entry.name}`;
45
- if (entry.directory) {
46
- if (!IGNORED_DIRECTORIES.has(entry.name)) {
47
- pending.push(path);
48
- }
49
- continue;
50
- }
51
- if (SOURCE_FILE.test(entry.name) && !DECLARATION_FILE.test(entry.name)) {
52
- found.push(path);
53
- }
54
- }
55
- }
56
- return found.sort((a, b) => (a < b ? -1 : 1));
33
+ return filesBelow(host, targetRoot, (name) => SOURCE_FILE.test(name) && !DECLARATION_FILE.test(name));
57
34
  }
58
35
  /**
59
36
  * Every source file below the target root, in path order. A file that cannot be read is
@@ -3,14 +3,20 @@
3
3
  * the code's row of the issue-kind vocabulary, and the dials that withhold a finding
4
4
  * together with every finding that falls with a root they withhold.
5
5
  */
6
+ import { CODE, FAMILY_KEY_LENGTH, KIND_ROWS, } from "../contract/contract.generated.js";
6
7
  import { componentId } from "../components.js";
8
+ import { deadComponents } from "../matrix.js";
7
9
  import { OUTSIDE } from "../graph.js";
8
10
  import { KINDS } from "../kinds.js";
9
11
  import { byPosition, positionKey } from "../position.js";
10
12
  import { declarationKey } from "../ref.js";
11
- import { FAMILY_KEY_LENGTH } from "../resolve.js";
12
13
  import { isPartSubject, isRowSubject } from "../suppress.js";
13
- /** The fixability grammar/analysis.md "Fixability" gives a finding about a held declaration. */
14
+ /**
15
+ * The codes whose subject the run that counts test references holds live, so a test
16
+ * file's candidate reported under one carries none of a dead declaration's sweep facts.
17
+ */
18
+ const LIVE_SUBJECT = new Set(KIND_ROWS.filter((row) => row.holdsLive).map((row) => row.code));
19
+ /** The fixability grammar/analysis/fixability.md gives a finding about a held declaration. */
14
20
  const MANUAL = "manual";
15
21
  /** The member kinds an object property can declare, which an `in` expression tests by name. */
16
22
  const IN_TESTED = new Set([
@@ -35,8 +41,9 @@ function everyDeclaredLoaded(boundary) {
35
41
  * naming its family, else the code's default.
36
42
  */
37
43
  function severityOf(input, code, fallback) {
38
- const { config } = input;
39
- return (config.severity.get(code) ?? config.severity.get(code.slice(0, FAMILY_KEY_LENGTH)) ?? fallback);
44
+ const { severity } = input.config;
45
+ const key = [code, code.slice(0, FAMILY_KEY_LENGTH)].find((one) => Object.hasOwn(severity, one));
46
+ return key === undefined ? fallback : (severity[key] ?? fallback);
40
47
  }
41
48
  /**
42
49
  * The declarations the roots name as a library's published API, each public member of a
@@ -72,15 +79,13 @@ function testedByIn(input) {
72
79
  * published API, a public member of a published type included, is `certain` where the
73
80
  * run loaded every consumer the scope declared and declared one, `probable` where one did
74
81
  * not load, and `possible` where it declared none, whatever the configuration says of the
75
- * consumer set. Test-support code test code references, outside the test-of-dead-code
76
- * rule, what it contains, and a member an `in` expression tests by name are `possible`.
82
+ * consumer set. A member an `in` expression tests by name, and one a function outside the
83
+ * analysis fills, is `possible`.
77
84
  */
78
85
  function reachabilityClasses(input) {
79
86
  const tested = testedByIn(input);
87
+ const filled = input.boundary.filledOutside ?? new Set();
80
88
  const published = publishedIds(input.swept.matrix.union);
81
- const usedByTests = new Set(input.swept.sweep.candidates
82
- .filter((candidate) => candidate.heldByTests !== undefined)
83
- .map((candidate) => candidate.id));
84
89
  const { declared } = input.boundary.consumers;
85
90
  const outside = declared.length === 0
86
91
  ? "possible"
@@ -88,12 +93,38 @@ function reachabilityClasses(input) {
88
93
  ? "certain"
89
94
  : "probable";
90
95
  return (id) => {
91
- if (usedByTests.has(id) || tested.has(id)) {
96
+ if (tested.has(id) || filled.has(id)) {
92
97
  return "possible";
93
98
  }
94
99
  return published.has(id) ? outside : "certain";
95
100
  };
96
101
  }
102
+ /**
103
+ * Whether one test of dead code references a target symbol that is published API of a run
104
+ * that declares no consumer, which outside programs the run cannot see may use, so the
105
+ * target counts as live for the test and the test is no test of dead code.
106
+ */
107
+ export function testsUnseenAPI(input) {
108
+ if (input.boundary.consumers.declared.length > 0) {
109
+ return () => false;
110
+ }
111
+ const union = input.swept.matrix.union;
112
+ const published = publishedIds(union);
113
+ return (id) => testTargets(union, id).some((target) => published.has(target));
114
+ }
115
+ /** The target symbols one test of dead code references, by id: production declarations the sweep judges. */
116
+ function testTargets(union, id) {
117
+ return (union.out[union.at(id)] ?? []).flatMap((edge) => {
118
+ const target = union.symbols[edge.to];
119
+ const testCode = union.test[edge.to] === true || union.support[edge.to] === true;
120
+ return target !== undefined &&
121
+ !testCode &&
122
+ union.ambient[edge.to] !== true &&
123
+ union.subject[edge.to] === true
124
+ ? [target.id]
125
+ : [];
126
+ });
127
+ }
97
128
  /**
98
129
  * The confidence ceiling of a test of dead code: the lowest reachability class among the
99
130
  * target symbols it references, because its deadness is theirs, whether or not they share
@@ -102,34 +133,71 @@ function reachabilityClasses(input) {
102
133
  function testCeilings(input, classOf) {
103
134
  const union = input.swept.matrix.union;
104
135
  const tests = new Set(input.swept.sweep.candidates.filter((one) => one.testOfDeadCode).map((one) => one.id));
105
- return (id) => {
106
- let lowest = "certain";
107
- if (!tests.has(id)) {
108
- return lowest;
109
- }
110
- for (const edge of union.out[union.at(id)] ?? []) {
111
- const target = union.symbols[edge.to];
112
- const testCode = union.test[edge.to] === true || union.support[edge.to] === true;
113
- if (target !== undefined && !testCode && union.subject[edge.to] === true) {
114
- lowest = capped(lowest, classOf(target.id));
115
- }
136
+ return (id) => tests.has(id)
137
+ ? testTargets(union, id).reduce((lowest, target) => capped(lowest, classOf(target)), "certain")
138
+ : "certain";
139
+ }
140
+ /**
141
+ * The subjects dead only because production code only tests use is: each declaration the
142
+ * sweep marks test-used, each `DS1602` file of such code, and each test of dead code every
143
+ * target of which is one. Nothing they hold references a dead declaration outside them, so
144
+ * the rest of a component they share is reported without them.
145
+ */
146
+ function testUsedSubjects(input) {
147
+ const { matrix, sweep } = input.swept;
148
+ const union = matrix.union;
149
+ const used = new Set(sweep.candidates.filter((one) => one.testUsed).map((one) => one.id));
150
+ for (const test of sweep.candidates.filter((one) => one.testOfDeadCode)) {
151
+ const targets = testTargets(union, test.id);
152
+ if (targets.length > 0 && targets.every((target) => used.has(target))) {
153
+ used.add(test.id);
116
154
  }
117
- return lowest;
118
- };
155
+ }
156
+ const files = new Set(union.symbols.flatMap((symbol, at) => matrix.testUsed[at] === true ? [symbol.position.path] : []));
157
+ return { declarations: used, files };
158
+ }
159
+ /**
160
+ * The ceiling the roots the run found put on one dead subject: a subject other declarations
161
+ * reference, found dead only because no root reaches them, is `probable` unless the
162
+ * configuration declares the roots complete and no configuration holding the subject loads
163
+ * a module the analysis cannot resolve, because a root the analysis cannot see would make
164
+ * it live; every other declaration has none below `certain`.
165
+ */
166
+ function rootsCeiling(input, finding, candidate, configurations) {
167
+ const unreachable = candidate?.relation === "reachability" || finding.unreachableOnly === true;
168
+ const unseen = new Set((input.files.unseenLoads ?? []).map((load) => load.configuration));
169
+ const complete = input.config.ts.roots_complete && !configurations.some((one) => unseen.has(one));
170
+ return unreachable && !complete ? "probable" : "certain";
119
171
  }
120
172
  /**
121
173
  * The component each dead declaration of the run falls in, and none for a live one, with
122
174
  * its members listed in the canonical order where the run lists a component in full.
123
175
  */
124
- function findingComponents(swept, cascade) {
176
+ function findingComponents(swept, components, cascade) {
125
177
  const held = new Map();
126
- for (const component of swept.sweep.components) {
178
+ for (const component of components) {
127
179
  for (const member of component.members) {
128
180
  held.set(member, component);
129
181
  }
130
182
  }
183
+ const listed = membersListed(swept);
184
+ return (id) => {
185
+ const component = held.get(id);
186
+ return component === undefined
187
+ ? undefined
188
+ : {
189
+ id: component.id,
190
+ root: component.roots.includes(id),
191
+ symbolCount: component.members.length,
192
+ deletableLines: component.deletableLines,
193
+ ...(cascade === "full" ? { members: listed(component.members) } : {}),
194
+ };
195
+ };
196
+ }
197
+ /** The members of a component as the run lists them, positioned and in the canonical order. */
198
+ function membersListed(swept) {
131
199
  const symbols = new Map(swept.matrix.union.symbols.map((symbol) => [symbol.id, symbol]));
132
- const listed = (component) => component.members
200
+ return (members) => members
133
201
  .flatMap((member) => {
134
202
  const symbol = symbols.get(member);
135
203
  return symbol === undefined
@@ -146,17 +214,58 @@ function findingComponents(swept, cascade) {
146
214
  ];
147
215
  })
148
216
  .sort((a, b) => byPosition(a.position, b.position));
149
- return (id) => {
150
- const component = held.get(id);
151
- return component === undefined
152
- ? undefined
217
+ }
218
+ /**
219
+ * The run's dead components with each file a `DS1602` finding reports dead among them, so
220
+ * the file's declarations fall with it, and with the components each subject that is no
221
+ * declaration falls with joined into one.
222
+ */
223
+ function runComponents(swept, findings) {
224
+ const union = swept.matrix.union;
225
+ const reported = new Set(findings.filter((one) => one.code === CODE.fileNeverImported).map((one) => one.position.path));
226
+ const files = new Map(union.symbols.flatMap((symbol, at) => symbol.kind === "file" && reported.has(symbol.position.path) ? [[at, symbol]] : []));
227
+ const computed = files.size === 0
228
+ ? swept.sweep.components
229
+ : deadComponents(swept.matrix, swept.sweep, new Set(files.keys()));
230
+ const place = new Map();
231
+ computed.forEach((component, at) => {
232
+ for (const member of component.members) {
233
+ place.set(member, at);
234
+ }
235
+ });
236
+ const leader = computed.map((_component, at) => at);
237
+ const find = (at) => {
238
+ let top = at;
239
+ while ((leader[top] ?? top) !== top) {
240
+ top = leader[top] ?? top;
241
+ }
242
+ return top;
243
+ };
244
+ for (const { fallsWith } of findings) {
245
+ const joined = (fallsWith ?? []).flatMap((id) => place.get(id) ?? []).map(find);
246
+ const first = Math.min(...joined);
247
+ for (const one of joined) {
248
+ leader[one] = first;
249
+ }
250
+ }
251
+ const merged = new Map();
252
+ computed.forEach((component, at) => {
253
+ const top = find(at);
254
+ const into = merged.get(top);
255
+ merged.set(top, into === undefined
256
+ ? component
153
257
  : {
154
- id: component.id,
155
- root: component.roots.includes(id),
156
- symbolCount: component.members.length,
157
- deletableLines: component.deletableLines,
158
- ...(cascade === "full" ? { members: listed(component) } : {}),
159
- };
258
+ id: into.id,
259
+ members: [...into.members, ...component.members],
260
+ roots: [...into.roots, ...component.roots],
261
+ deletableLines: into.deletableLines + component.deletableLines,
262
+ held: into.held && component.held,
263
+ });
264
+ });
265
+ return {
266
+ components: [...merged.values()],
267
+ computed: computed.length,
268
+ files: new Map([...files.values()].map((symbol) => [symbol.position.path, symbol.id])),
160
269
  };
161
270
  }
162
271
  /**
@@ -174,16 +283,20 @@ export function completed(input, findings, kinds = KINDS) {
174
283
  const candidates = new Map(swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
175
284
  const classOf = reachabilityClasses(input);
176
285
  const ceilingOf = testCeilings(input, classOf);
177
- const componentOf = findingComponents(swept, input.config.reporters.cascade);
178
- const held = new Set(swept.sweep.components.filter((one) => one.held).flatMap((one) => one.members));
179
- let minted = swept.sweep.components.length;
180
- const dead = new Set(swept.sweep.components.map((component) => component.id));
286
+ const testUsed = testUsedSubjects(input);
287
+ const { components, computed, files } = runComponents(swept, findings);
288
+ const componentOf = findingComponents(swept, components, input.config.reporters.cascade);
289
+ const held = new Set(components.filter((one) => one.held).flatMap((one) => one.members));
290
+ // A join keeps its first component's identifier, so a minted one counts past every computed one.
291
+ let minted = computed;
292
+ const dead = new Set(components.map((component) => component.id));
181
293
  const fallingWith = new Map();
182
- for (const { heldByTests } of swept.sweep.candidates) {
294
+ for (const { id, heldByTests } of swept.sweep.candidates) {
183
295
  if (heldByTests !== undefined) {
184
- fallingWith.set(heldByTests, (fallingWith.get(heldByTests) ?? 0) + 1);
296
+ fallingWith.set(heldByTests, [...(fallingWith.get(heldByTests) ?? []), id]);
185
297
  }
186
298
  }
299
+ const listed = membersListed(swept);
187
300
  const done = findings.map((finding) => {
188
301
  const row = kinds.get(finding.code);
189
302
  if (row === undefined) {
@@ -193,18 +306,30 @@ export function completed(input, findings, kinds = KINDS) {
193
306
  // a row is still never the declaration. Two declarations of one file can share a
194
307
  // reference, so the one the finding is positioned at is the subject where there is one.
195
308
  const declared = union.at(positionKey(finding.position));
309
+ const ref = finding.symbol.holder ?? finding.symbol.ref;
196
310
  const at = isRowSubject(finding.symbol.kind)
197
311
  ? OUTSIDE
198
- : union.symbols[declared]?.ref === finding.symbol.ref
312
+ : union.symbols[declared]?.ref === ref
199
313
  ? declared
200
- : (positions.get(declarationKey(finding.symbol.ref, finding.position.path)) ?? OUTSIDE);
314
+ : (positions.get(declarationKey(ref, finding.position.path)) ?? OUTSIDE);
201
315
  // A part names the declaration that holds it, and is decided inside it rather than by
202
316
  // the declaration's liveness, so it carries none of the declaration's sweep facts.
203
317
  const part = isPartSubject(finding.symbol.kind);
204
318
  const symbol = union.symbols[at];
205
319
  const judged = !part && union.subject[at] === true ? symbol : undefined;
206
- const candidate = judged === undefined ? undefined : candidates.get(judged.id);
207
- const found = symbol === undefined || part ? undefined : componentOf(symbol.id);
320
+ const candidate = judged === undefined || LIVE_SUBJECT.has(finding.code)
321
+ ? undefined
322
+ : candidates.get(judged.id);
323
+ const fallsWith = finding.fallsWith?.[0];
324
+ const joined = fallsWith === undefined ? undefined : componentOf(fallsWith);
325
+ const file = finding.code === CODE.fileNeverImported ? files.get(finding.position.path) : undefined;
326
+ const found = joined !== undefined
327
+ ? { ...joined, root: false }
328
+ : file !== undefined
329
+ ? componentOf(file)
330
+ : symbol === undefined || part
331
+ ? undefined
332
+ : componentOf(symbol.id);
208
333
  // A test file's declaration is a component member only as a test of dead code, and
209
334
  // test code keeps the test-support code it references out of every component.
210
335
  if (candidate !== undefined &&
@@ -216,17 +341,40 @@ export function completed(input, findings, kinds = KINDS) {
216
341
  let component = found;
217
342
  if (component === undefined) {
218
343
  minted += 1;
344
+ const falling = symbol?.kind === "interface" ? fallingWith.get(symbol.id) : undefined;
219
345
  component = {
220
346
  id: componentId(minted),
221
347
  root: true,
222
- symbolCount: symbol?.kind === "interface" ? (fallingWith.get(symbol.id) ?? 1) : 1,
348
+ symbolCount: falling?.length ?? 1,
223
349
  deletableLines: 0,
350
+ ...(input.config.reporters.cascade !== "full"
351
+ ? {}
352
+ : falling === undefined
353
+ ? {
354
+ members: [
355
+ {
356
+ ref: finding.symbol.ref,
357
+ name: finding.symbol.name,
358
+ position: finding.position,
359
+ },
360
+ ],
361
+ }
362
+ : { members: listed(falling) }),
224
363
  };
225
364
  }
226
365
  const counts = candidate === undefined
227
366
  ? union.made[at]
228
367
  : { production: candidate.productionRefs, test: candidate.testRefs };
229
- const reachabilityClass = judged === undefined ? (finding.reachabilityClass ?? "certain") : classOf(judged.id);
368
+ const ownClass = judged === undefined ? (finding.reachabilityClass ?? "certain") : classOf(judged.id);
369
+ const used = judged !== undefined
370
+ ? candidate !== undefined && testUsed.declarations.has(judged.id)
371
+ : finding.code === CODE.fileNeverImported && testUsed.files.has(finding.position.path);
372
+ const reachabilityClass = used && candidate?.testOfDeadCode !== true ? "possible" : ownClass;
373
+ const configurations = candidate?.configurations ??
374
+ (judged === undefined && !part
375
+ ? swept.matrix.configurations
376
+ : (swept.matrix.heldIn[at] ?? swept.matrix.configurations));
377
+ const untested = capped(capped(capped(ownClass, row.maxClass), rootsCeiling(input, finding, candidate, configurations)), judged === undefined ? "certain" : ceilingOf(judged.id));
230
378
  const generated = input.files.generated?.has(finding.position.path) === true;
231
379
  const fixability = generated
232
380
  ? "none"
@@ -236,9 +384,13 @@ export function completed(input, findings, kinds = KINDS) {
236
384
  row.findingFixabilities?.includes(MANUAL) === true
237
385
  ? MANUAL
238
386
  : row.fixability;
239
- const lastUses = fixability === "deletable" && candidate !== undefined
240
- ? input.dependencies.lastUses.get(candidate.id)
241
- : undefined;
387
+ const lastUses = fixability !== "deletable"
388
+ ? undefined
389
+ : candidate !== undefined
390
+ ? input.dependencies.lastUses.get(candidate.id)
391
+ : finding.code === CODE.fileNeverImported
392
+ ? input.dependencies.fileLastUses.get(finding.position.path)
393
+ : undefined;
242
394
  return {
243
395
  code: finding.code,
244
396
  kind: row.name,
@@ -246,16 +398,14 @@ export function completed(input, findings, kinds = KINDS) {
246
398
  position: finding.position,
247
399
  symbol: finding.symbol,
248
400
  reachabilityClass,
249
- confidence: capped(capped(reachabilityClass, row.maxClass), judged === undefined ? "certain" : ceilingOf(judged.id)),
401
+ confidence: used ? "possible" : untested,
402
+ ...(used ? { componentConfidence: untested } : {}),
250
403
  ...(candidate === undefined ? {} : { livenessRelation: candidate.relation }),
251
404
  testOnly: judged !== undefined && counts?.production === 0 && counts.test > 0,
252
405
  generated,
253
406
  component,
254
407
  retainedBy: [],
255
- configurations: candidate?.configurations ??
256
- (judged === undefined && !part
257
- ? swept.matrix.configurations
258
- : (swept.matrix.heldIn[at] ?? swept.matrix.configurations)),
408
+ configurations,
259
409
  consumersLoaded: input.boundary.consumers.loaded,
260
410
  fixability,
261
411
  severity: severityOf(input, finding.code, row.defaultSeverity),
@@ -270,7 +420,7 @@ export function completed(input, findings, kinds = KINDS) {
270
420
  /**
271
421
  * The findings with every finding of a dead component capped by the lowest confidence
272
422
  * among the component's root members, so the minimum confidence reports or withholds a
273
- * component whole.
423
+ * component whole but for the findings test-used code lowers on their own.
274
424
  */
275
425
  function componentCapped(findings, dead) {
276
426
  const lowest = new Map();
@@ -278,16 +428,27 @@ function componentCapped(findings, dead) {
278
428
  const { id, root } = finding.component;
279
429
  if (root && dead.has(id)) {
280
430
  const held = lowest.get(id);
281
- lowest.set(id, held === undefined ? finding.confidence : capped(held, finding.confidence));
431
+ const own = componentConfidenceOf(finding);
432
+ lowest.set(id, held === undefined ? own : capped(held, own));
282
433
  }
283
434
  }
284
435
  return findings.map((finding) => {
285
436
  const cap = lowest.get(finding.component.id);
286
- return cap === undefined || RANK[cap] >= RANK[finding.confidence]
287
- ? finding
288
- : { ...finding, confidence: cap };
437
+ if (cap === undefined) {
438
+ return finding;
439
+ }
440
+ const own = finding.componentConfidence;
441
+ return {
442
+ ...finding,
443
+ confidence: capped(finding.confidence, cap),
444
+ ...(own === undefined ? {} : { componentConfidence: capped(own, cap) }),
445
+ };
289
446
  });
290
447
  }
448
+ /** The confidence a finding gives its component's cap and the dials. */
449
+ function componentConfidenceOf(finding) {
450
+ return finding.componentConfidence ?? finding.confidence;
451
+ }
291
452
  /** Whether the dials withhold a finding by its own severity or confidence. */
292
453
  function dialed(finding, minConfidence) {
293
454
  return finding.severity === "allow" || RANK[finding.confidence] < RANK[minConfidence];
@@ -300,9 +461,10 @@ function dialed(finding, minConfidence) {
300
461
  * root withholds itself alone.
301
462
  */
302
463
  export function dialsOf(input, findings) {
303
- const minConfidence = input.config.analysis.minConfidence;
464
+ const minConfidence = input.config.analysis.min_confidence;
304
465
  const components = new Set(findings
305
- .filter((finding) => finding.component.root && dialed(finding, minConfidence))
466
+ .filter((finding) => finding.component.root &&
467
+ dialed({ ...finding, confidence: componentConfidenceOf(finding) }, minConfidence))
306
468
  .map((finding) => finding.component.id));
307
469
  return {
308
470
  allows: (code) => {