@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
@@ -1,3 +1,4 @@
1
+ import { CODE } from "../contract/contract.generated.ts";
1
2
  import { REPOSITORY_DOCUMENT } from "../config.ts";
2
3
  import type { Finding, FindingDetails, SuppressionEntry } from "../finding.ts";
3
4
  import type { InventorySymbol } from "../inventory.ts";
@@ -6,19 +7,12 @@ import type { Position } from "../position.ts";
6
7
  import {
7
8
  isDependencyCode,
8
9
  isUnsuppressibleCode,
9
- STALE_SUPPRESSION,
10
- SUPPRESSION_WITHOUT_REASON,
11
10
  type Mechanism,
12
11
  type Refusal,
13
12
  type SuppressionRecord,
14
13
  } from "../suppress.ts";
15
14
  import type { Emitter } from "./emitter.ts";
16
-
17
- /** The code a configured root that matches nothing is reported under. */
18
- const UNMATCHED_ROOT = "DS1704";
19
-
20
- /** The code a configured declaration that names nothing is reported under. */
21
- const UNMATCHED_DECLARATION = "DS1706";
15
+ import { bytewise } from "../text/compare.ts";
22
16
 
23
17
  /** The subject kind of a finding about a configured declaration. */
24
18
  const DECLARATION_SUBJECT = "configured-declaration";
@@ -39,7 +33,7 @@ export interface StaleRecord {
39
33
  readonly reports: readonly string[];
40
34
  }
41
35
 
42
- /** What the self-check family reads: the run's refused suppressions and its configured roots. */
36
+ /** What the configuration family reads: the run's refused suppressions and its configured roots. */
43
37
  export interface SelfCheckFacts {
44
38
  /** The suppressions the grammar refused with a finding, in reading order. */
45
39
  readonly refusals: readonly Refusal[];
@@ -135,7 +129,7 @@ function refusedSuppressions(refusals: readonly Refusal[], files: FileRefs): rea
135
129
  suppressionRef(refused.symbol, refused.path, files),
136
130
  refused.site,
137
131
  `${WRITTEN[refused.mechanism]} for ${refused.code} ${
138
- refused.reported === SUPPRESSION_WITHOUT_REASON
132
+ refused.reported === CODE.suppressionWithoutReason
139
133
  ? "carries no reason"
140
134
  : "names a symbol and no path"
141
135
  }`,
@@ -146,7 +140,7 @@ function refusedSuppressions(refusals: readonly Refusal[], files: FileRefs): rea
146
140
 
147
141
  /** One stale suppression as the report's stale-suppression record carries it. */
148
142
  export interface StaleSuppression {
149
- readonly code: typeof STALE_SUPPRESSION;
143
+ readonly code: typeof CODE.staleSuppression;
150
144
  readonly mechanism: Mechanism;
151
145
  readonly entry: {
152
146
  readonly code: string;
@@ -178,10 +172,10 @@ export function staleSuppressions(
178
172
  bySite.set(key, held);
179
173
  }
180
174
  return [...bySite.values()].map(({ first, records }) => {
181
- const codes = [...new Set(records.map((one) => one.record.code))].sort();
175
+ const codes = [...new Set(records.map((one) => one.record.code))].sort(bytewise);
182
176
  const reports = [...new Set(records.flatMap((one) => one.reports))]
183
177
  .filter((code) => !codes.includes(code))
184
- .sort();
178
+ .sort(bytewise);
185
179
  const rows = codes.filter(isUnsuppressibleCode);
186
180
  const dependencies = first.mechanism === "inline" ? codes.filter(isDependencyCode) : [];
187
181
  const settable = rows.filter((code) => !FIXED_SEVERITY_CODES.includes(code));
@@ -209,7 +203,7 @@ export function staleSuppressions(
209
203
  ? ""
210
204
  : `, and ${declared}`;
211
205
  return {
212
- code: STALE_SUPPRESSION,
206
+ code: CODE.staleSuppression,
213
207
  mechanism: first.mechanism,
214
208
  entry: {
215
209
  code: first.code,
@@ -242,7 +236,7 @@ function unmatchedDeclarationFindings(
242
236
  unmatched: readonly UnmatchedDeclaration[],
243
237
  ): readonly Finding[] {
244
238
  return unmatched.map((one) => ({
245
- code: UNMATCHED_DECLARATION,
239
+ code: CODE.unmatchedConfiguredDeclaration,
246
240
  position: documentPosition(one.document),
247
241
  symbol: { ref: one.ref, kind: DECLARATION_SUBJECT, name: one.ref, sizeLines: 1 },
248
242
  message: `${one.key} entry names no declaration in any project`,
@@ -262,7 +256,7 @@ export function unmatchedRootFindings(
262
256
  ): readonly Finding[] {
263
257
  const position = documentPosition(document);
264
258
  return sources.map((source) => ({
265
- code: UNMATCHED_ROOT,
259
+ code: CODE.unmatchedRoot,
266
260
  position,
267
261
  symbol: { ref: source, kind: ROOT_SUBJECT, name: source, sizeLines: 1 },
268
262
  message:
@@ -273,7 +267,7 @@ export function unmatchedRootFindings(
273
267
  }
274
268
 
275
269
  /**
276
- * The findings of the self-check family, `DS1700` to `DS1799`: a suppression refused for
270
+ * The findings of the configuration family, `DS9000` to `DS9099`: a suppression refused for
277
271
  * carrying no reason or naming no path, and a configured root or declaration that named
278
272
  * nothing. A
279
273
  * suppression that matched nothing is a record of the report's stale suppressions, which
@@ -1,7 +1,9 @@
1
+ import { CODE } from "../contract/contract.generated.ts";
1
2
  import type { Finding } from "../finding.ts";
2
3
  import type { InventorySymbol, SymbolKind } from "../inventory.ts";
3
4
  import type { MatrixCandidate } from "../matrix.ts";
4
5
  import type { Emitter, EmitterInput } from "./emitter.ts";
6
+ import { testsUnseenAPI } from "./completion.ts";
5
7
  import { unreachableExports } from "./visibility-narrowing.ts";
6
8
 
7
9
  /** The kinds of declaration that are members of a class, an interface, a type or an enum. */
@@ -13,18 +15,18 @@ const MEMBERS: ReadonlySet<SymbolKind> = new Set([
13
15
  ]);
14
16
 
15
17
  /**
16
- * The kinds of declaration no code of this family reports: an interface and a method an
18
+ * The kinds of declaration no code of this emitter reports: an interface and a method an
17
19
  * interface declares are the interface kinds' subjects, and a type parameter is the
18
20
  * subject of the unused-type-parameter kind or of none.
19
21
  */
20
- const NOT_THIS_FAMILY: ReadonlySet<SymbolKind> = new Set([
22
+ const NOT_REPORTED_HERE: ReadonlySet<SymbolKind> = new Set([
21
23
  "file",
22
24
  "interface",
23
25
  "interface-method",
24
26
  "type-parameter",
25
27
  ]);
26
28
 
27
- /** The reader's word for each kind of declaration a finding of the family names. */
29
+ /** The reader's word for each kind of declaration a finding of this emitter names. */
28
30
  const WORDS: Readonly<Record<SymbolKind, string>> = {
29
31
  file: "file",
30
32
  function: "function",
@@ -55,21 +57,24 @@ interface Judged {
55
57
  readonly deprecated: boolean;
56
58
  /** Whether the unreachable-export kind reports it. */
57
59
  readonly unreachable: boolean;
60
+ /** Whether it is a test with a target that is published API no declared consumer judges. */
61
+ readonly testsUnseenAPI: boolean;
58
62
  }
59
63
 
60
64
  /**
61
65
  * The code one candidate is reported under, or none, each symbol once under the most
62
66
  * specific code: a test of dead code first; no other test-file declaration or enum
63
67
  * member nothing names; a test-support declaration test code references as test-only
64
- * use; then deprecation, a test reference, and the unreferenced codes, an unimportable
65
- * unused export being the unreachable kind's.
68
+ * use; then deprecation, a test reference, or only dead production references beside a
69
+ * test that stays, and the unreferenced codes, an unimportable unused export being the
70
+ * unreachable kind's.
66
71
  */
67
72
  function codeOf(judged: Judged): string | undefined {
68
73
  const { candidate, symbol } = judged;
69
74
  if (candidate.testOfDeadCode) {
70
- return "DS1005";
75
+ return judged.testsUnseenAPI ? undefined : CODE.testOfDeadCode;
71
76
  }
72
- if ((judged.test && !candidate.unreferencedTest) || NOT_THIS_FAMILY.has(symbol.kind)) {
77
+ if ((judged.test && !candidate.unreferencedTest) || NOT_REPORTED_HERE.has(symbol.kind)) {
73
78
  return undefined;
74
79
  }
75
80
  const referenced = candidate.productionRefs + candidate.testRefs > 0;
@@ -80,29 +85,33 @@ function codeOf(judged: Judged): string | undefined {
80
85
  return unreferencedCode(symbol);
81
86
  }
82
87
  if (candidate.heldByTests === candidate.id) {
83
- return "DS1004";
88
+ return CODE.testOnlyUse;
84
89
  }
85
90
  if (candidate.productionRefs === 0 && judged.deprecated) {
86
- return "DS1006";
91
+ return CODE.deprecatedUnused;
87
92
  }
88
- if (candidate.productionRefs === 0 && candidate.testRefs > 0) {
89
- return "DS1004";
93
+ if (
94
+ candidate.testRefs > 0 &&
95
+ (candidate.productionRefs === 0 ||
96
+ (candidate.relation === "reachability" && candidate.keptByTests))
97
+ ) {
98
+ return CODE.testOnlyUse;
90
99
  }
91
100
  if (MEMBERS.has(symbol.kind)) {
92
- return "DS1003";
101
+ return CODE.unusedMember;
93
102
  }
94
103
  if (!symbol.exported) {
95
- return "DS1002";
104
+ return CODE.unusedUnexported;
96
105
  }
97
- return judged.unreachable ? undefined : "DS1001";
106
+ return judged.unreachable ? undefined : CODE.unusedExported;
98
107
  }
99
108
 
100
109
  /** The code a declaration nothing references carries, by its shape and its visibility. */
101
110
  function unreferencedCode(symbol: InventorySymbol): string {
102
111
  if (MEMBERS.has(symbol.kind)) {
103
- return "DS1003";
112
+ return CODE.unusedMember;
104
113
  }
105
- return symbol.exported ? "DS1001" : "DS1002";
114
+ return symbol.exported ? CODE.unusedExported : CODE.unusedUnexported;
106
115
  }
107
116
 
108
117
  /** The visibility a member's message names, where it narrows the member's reach. */
@@ -127,15 +136,17 @@ function messageOf(code: string, judged: Judged): string {
127
136
  const { candidate, symbol } = judged;
128
137
  const word = WORDS[symbol.kind];
129
138
  switch (code) {
130
- case "DS1005":
139
+ case CODE.testOfDeadCode:
131
140
  return TEST_OF_DEAD_CODE;
132
- case "DS1006":
141
+ case CODE.deprecatedUnused:
133
142
  return `deprecated ${word} has no production reference`;
134
- case "DS1004":
135
- return `${word} is referenced only from test files and never from production code`;
143
+ case CODE.testOnlyUse:
144
+ return candidate.productionRefs === 0
145
+ ? `${word} is referenced only from test files and never from production code`
146
+ : `${word} is referenced only from test files and from declarations that are themselves dead`;
136
147
  }
137
148
  const subject =
138
- code === "DS1003"
149
+ code === CODE.unusedMember
139
150
  ? `${visibilityWord(symbol)}${word}`
140
151
  : symbol.kind === "export-alias"
141
152
  ? word
@@ -143,7 +154,7 @@ function messageOf(code: string, judged: Judged): string {
143
154
  if (candidate.relation === "reachability") {
144
155
  return `${subject} is referenced only from declarations that are themselves dead`;
145
156
  }
146
- if (code === "DS1001") {
157
+ if (code === CODE.unusedExported) {
147
158
  return `${subject} has no reference in the target and none from any loaded consumer`;
148
159
  }
149
160
  return `${subject} has no reference in the target`;
@@ -195,21 +206,46 @@ function brandMembers(input: EmitterInput): ReadonlySet<string> {
195
206
  /** The kinds of member a brand is: a data member of a type or of a class. */
196
207
  const BRANDS: ReadonlySet<SymbolKind> = new Set(["type-member", "class-member"]);
197
208
 
198
- /** The findings of the unused-declarations family, `DS1000` to `DS1099`. */
199
- export const unusedDeclarations: Emitter = (input) => familyFindings(input, false);
209
+ /** The findings of the unused-declaration kinds, `DS1001` to `DS1004` and `DS1102` to `DS1104`. */
210
+ export const unusedDeclarations: Emitter = (input) => [
211
+ ...emitterFindings(input, false),
212
+ ...unusedImports(input),
213
+ ];
214
+
215
+ /**
216
+ * The import bindings their file never reads. A generated file's are the generator's to
217
+ * remove unless the run includes generated files.
218
+ */
219
+ function unusedImports(input: EmitterInput): Finding[] {
220
+ const generated =
221
+ input.config.analysis.generated_files === "include" ? undefined : input.files.generated;
222
+ return input.intraFunction.imports.flatMap((binding) =>
223
+ binding.used || generated?.has(binding.position.path) === true
224
+ ? []
225
+ : [
226
+ {
227
+ code: CODE.unusedImport,
228
+ position: binding.position,
229
+ symbol: { ref: binding.ref, kind: "import", name: binding.name, sizeLines: 1 },
230
+ message: `import binding ${binding.name} is never read in its file`,
231
+ },
232
+ ],
233
+ );
234
+ }
200
235
 
201
236
  /**
202
237
  * The finding each member of a dead container would carry were its container live: what
203
238
  * a record on such a member is matched against, since the report carries the container's
204
239
  * finding rather than one of the member's own.
205
240
  */
206
- export const fallenMembers: Emitter = (input) => familyFindings(input, true);
241
+ export const fallenMembers: Emitter = (input) => emitterFindings(input, true);
207
242
 
208
- /** The family's findings, or, where `fallen` is set, its dead containers' members' alone. */
209
- function familyFindings(input: EmitterInput, fallen: boolean): Finding[] {
243
+ /** This emitter's findings, or, where `fallen` is set, its dead containers' members' alone. */
244
+ function emitterFindings(input: EmitterInput, fallen: boolean): Finding[] {
210
245
  const union = input.swept.matrix.union;
211
246
  const dead = new Set(input.swept.sweep.candidates.map((candidate) => candidate.id));
212
247
  const unreachable = unreachableExports(input);
248
+ const unseenAPI = testsUnseenAPI(input);
213
249
  let brands: ReadonlySet<string> | undefined;
214
250
 
215
251
  const found: Finding[] = [];
@@ -229,11 +265,12 @@ function familyFindings(input: EmitterInput, fallen: boolean): Finding[] {
229
265
  test: union.test[at] === true,
230
266
  deprecated: input.deprecated.has(symbol.id),
231
267
  unreachable: unreachable.has(symbol.id),
268
+ testsUnseenAPI: candidate.testOfDeadCode && unseenAPI(candidate.id),
232
269
  };
233
270
  const code = codeOf(judged);
234
271
  if (
235
272
  code === undefined ||
236
- (code === "DS1003" && (brands ??= brandMembers(input)).has(symbol.id))
273
+ (code === CODE.unusedMember && (brands ??= brandMembers(input)).has(symbol.id))
237
274
  ) {
238
275
  continue;
239
276
  }
@@ -1,16 +1,14 @@
1
+ import { CODE } from "../contract/contract.generated.ts";
1
2
  import type { Finding } from "../finding.ts";
2
3
  import { OUTSIDE, type Graph } from "../graph.ts";
3
4
  import type { InventorySymbol, SymbolKind } from "../inventory.ts";
4
5
  import type { MatrixCandidate } from "../matrix.ts";
5
6
  import { declarationKey } from "../ref.ts";
6
7
  import type { RootKind } from "../roots.ts";
8
+ import { runnerCalls } from "../sweep.ts";
7
9
  import type { Emitter, EmitterInput } from "./emitter.ts";
8
10
  import { writeOnlyDeclarations } from "./reads-and-writes.ts";
9
11
 
10
- const UNNECESSARY_EXPORT = "DS1101";
11
- const UNREACHABLE_EXPORT = "DS1103";
12
- const REDUNDANT_EXPORT_KEYWORD = "DS1104";
13
-
14
12
  /** How far the references to one declaration reach, from the narrowest. */
15
13
  type Reach = "file" | "package" | "outside";
16
14
 
@@ -21,7 +19,7 @@ type Reach = "file" | "package" | "outside";
21
19
  */
22
20
  const PUBLISHING: ReadonlySet<RootKind> = new Set<RootKind>(["manifest-entry", "published-api"]);
23
21
 
24
- /** The reader's word for each kind of declaration this family reports. */
22
+ /** The reader's word for each kind of declaration this emitter reports. */
25
23
  const WORDS: Partial<Readonly<Record<SymbolKind, string>>> = {
26
24
  function: "function",
27
25
  class: "class",
@@ -51,8 +49,37 @@ function publishes(kinds: ReadonlySet<RootKind> | undefined): boolean {
51
49
  return [...(kinds ?? [])].some((kind) => PUBLISHING.has(kind));
52
50
  }
53
51
 
52
+ /**
53
+ * The root kinds that name a file without entering it by its exports: a configuration
54
+ * string or a script token runs the file, and no tool reads a name it exports.
55
+ */
56
+ const RUN_ONLY: ReadonlySet<RootKind> = new Set<RootKind>(["configuration-string", "script"]);
57
+
58
+ /** Whether a root makes its file an entry file, or its declaration an entry, which a tool may read by name. */
54
59
  function entersByName(kinds: ReadonlySet<RootKind> | undefined): boolean {
55
- return [...(kinds ?? [])].some((kind) => !PUBLISHING.has(kind));
60
+ return [...(kinds ?? [])].some((kind) => !PUBLISHING.has(kind) && !RUN_ONLY.has(kind));
61
+ }
62
+
63
+ /**
64
+ * The root kinds whose root of a file enters no export by name: a document's import roots
65
+ * each export it imports, so an export it does not import is judged as in any file.
66
+ */
67
+ const PER_NAME: ReadonlySet<RootKind> = new Set<RootKind>(["document-import"]);
68
+
69
+ /**
70
+ * Whether the roots of a declaration's file enter it by name: a test runner's root
71
+ * enters only an export the runner calls, so another export of a test file is judged.
72
+ */
73
+ function fileEntersByName(
74
+ kinds: ReadonlySet<RootKind> | undefined,
75
+ symbol: InventorySymbol,
76
+ ): boolean {
77
+ const entering = [...(kinds ?? [])].filter(
78
+ (kind) => !PUBLISHING.has(kind) && !RUN_ONLY.has(kind) && !PER_NAME.has(kind),
79
+ );
80
+ return (
81
+ entering.some((kind) => kind !== "test-runner") || (entering.length > 0 && runnerCalls(symbol))
82
+ );
56
83
  }
57
84
 
58
85
  /**
@@ -121,8 +148,8 @@ function wordOf(symbol: InventorySymbol): string {
121
148
 
122
149
  /** The visibility each narrowing code names, which only the narrowing codes carry. */
123
150
  const NARROWER: Readonly<Record<string, "file" | "package">> = {
124
- [REDUNDANT_EXPORT_KEYWORD]: "file",
125
- [UNNECESSARY_EXPORT]: "package",
151
+ [CODE.redundantExportKeyword]: "file",
152
+ [CODE.unnecessaryExport]: "package",
126
153
  };
127
154
 
128
155
  function findingOf(symbol: InventorySymbol, code: string, message: string): Finding {
@@ -163,7 +190,7 @@ function narrowing(
163
190
  world.writeOnly.has(declarationKey(symbol.ref, symbol.position.path)) ||
164
191
  symbol.kind === "export-alias" ||
165
192
  entersByName(facts.rootedBy[at]) ||
166
- entersByName(facts.rootedBy[file]) ||
193
+ fileEntersByName(facts.rootedBy[file], symbol) ||
167
194
  (facts.named[at] ?? 0) === 0
168
195
  ) {
169
196
  return undefined;
@@ -176,7 +203,7 @@ function narrowing(
176
203
  }
177
204
  return findingOf(
178
205
  symbol,
179
- REDUNDANT_EXPORT_KEYWORD,
206
+ CODE.redundantExportKeyword,
180
207
  `exported ${word} is referenced only inside the file that declares it`,
181
208
  );
182
209
  case "package":
@@ -185,7 +212,7 @@ function narrowing(
185
212
  }
186
213
  return findingOf(
187
214
  symbol,
188
- UNNECESSARY_EXPORT,
215
+ CODE.unnecessaryExport,
189
216
  `exported ${word} is referenced only inside the package that declares it`,
190
217
  );
191
218
  default:
@@ -198,7 +225,7 @@ function narrowing(
198
225
  * export no code outside the target can import, because the manifest declares
199
226
  * `exports` and no root reaches it. Each arm yields to a more specific kind, so one
200
227
  * declaration is reported once: a test whose every subject is dead, an interface, a
201
- * declaration only a test file references, and a deprecated one.
228
+ * declaration only a test file references or a test that stays keeps, and a deprecated one.
202
229
  */
203
230
  function unreachable(
204
231
  facts: Facts,
@@ -213,7 +240,7 @@ function unreachable(
213
240
  (facts.rootedBy[at]?.size ?? 0) > 0 ||
214
241
  candidate.testOfDeadCode ||
215
242
  symbol.kind === "interface" ||
216
- (candidate.productionRefs === 0 && candidate.testRefs > 0) ||
243
+ (candidate.testRefs > 0 && (candidate.productionRefs === 0 || candidate.keptByTests)) ||
217
244
  (candidate.productionRefs === 0 && input.deprecated.has(symbol.id))
218
245
  ) {
219
246
  return undefined;
@@ -224,13 +251,13 @@ function unreachable(
224
251
  : "has no reference in the target";
225
252
  return findingOf(
226
253
  symbol,
227
- UNREACHABLE_EXPORT,
254
+ CODE.unreachableExport,
228
255
  `exported ${wordOf(symbol)} ${found}, and nothing outside can import the file that declares it`,
229
256
  );
230
257
  }
231
258
 
232
259
  /**
233
- * The visibility-narrowing family over one run: `DS1101`, `DS1103` and `DS1104`. A finding
260
+ * The visibility-narrowing kinds over one run: `DS1501`, `DS1101` and `DS1503`. A finding
234
261
  * about a symbol a declared cross-language edge names is reported here like any other, and
235
262
  * the edge's evaluation then holds it pending, because the edge stands for a reference from
236
263
  * outside the file and the package whose use is decided in the other language. A
@@ -239,13 +266,13 @@ function unreachable(
239
266
  export function narrowings(input: EmitterInput): readonly Finding[] {
240
267
  const facts = factsOf(input.swept.matrix.union);
241
268
  const { declared, loaded } = input.boundary.consumers;
242
- const closed = input.config.consumersComplete && declared.every((id) => loaded.includes(id));
269
+ const closed = input.config.consumers.complete && declared.every((id) => loaded.includes(id));
243
270
  const world = { closed, writeOnly: writeOnlyDeclarations(input) };
244
271
  const candidates = new Map(
245
272
  input.swept.sweep.candidates.map((candidate) => [candidate.id, candidate]),
246
273
  );
247
274
  const generated =
248
- input.config.analysis.generatedFiles === "include" ? undefined : input.files.generated;
275
+ input.config.analysis.generated_files === "include" ? undefined : input.files.generated;
249
276
 
250
277
  const findings: Finding[] = [];
251
278
  facts.graph.symbols.forEach((symbol, at) => {
@@ -253,8 +280,13 @@ export function narrowings(input: EmitterInput): readonly Finding[] {
253
280
  return;
254
281
  }
255
282
  const candidate = candidates.get(symbol.id);
283
+ // A declaration of test code, a test file's or test-support code's, that only test code
284
+ // references is a candidate, live in the run that counts test references.
285
+ const live =
286
+ candidate === undefined ||
287
+ (facts.graph.test[at] === true && !candidate.unreferencedTest && !candidate.testOfDeadCode);
256
288
  const found =
257
- candidate === undefined
289
+ candidate === undefined || live
258
290
  ? narrowing(facts, at, symbol, world)
259
291
  : unreachable(facts, at, symbol, candidate, input);
260
292
  if (found !== undefined) {
@@ -281,5 +313,5 @@ export function unreachableExports(input: EmitterInput): ReadonlySet<string> {
281
313
  return found;
282
314
  }
283
315
 
284
- /** The findings of the visibility-narrowing family, `DS1100` to `DS1199`. */
316
+ /** The findings of the visibility-narrowing kinds, `DS1501`, `DS1101` and `DS1503`. */
285
317
  export const visibilityNarrowing: Emitter = narrowings;
@@ -4,8 +4,9 @@
4
4
  * verdict a report carries beside them.
5
5
  */
6
6
 
7
+ import { EXIT_CLEAN, EXIT_FINDINGS, EXIT_PENDING } from "./contract/contract.generated.ts";
7
8
  import type { AnalysisInputs } from "./analysis.ts";
8
- import { fails, type Config } from "./config.ts";
9
+ import { fails, type ResolvedConfig } from "./config.ts";
9
10
  import type { EdgeEvaluation } from "./edges.ts";
10
11
  import type { CompletedFinding, Finding } from "./finding.ts";
11
12
  import { decidedFindings, type Withheld } from "./findings/emitters.ts";
@@ -23,7 +24,8 @@ import {
23
24
  type Ledger,
24
25
  } from "./suppress.ts";
25
26
  import type { Provenance, Recorded } from "./suppress-file.ts";
26
- import { EXIT_CLEAN, EXIT_FINDINGS } from "./verbs/verb.ts";
27
+ import { bytewise } from "./text/compare.ts";
28
+ import { PRODUCT } from "./product.ts";
27
29
 
28
30
  /** The counts a report's totals carry beside its findings. */
29
31
  interface PassTotals {
@@ -49,25 +51,14 @@ export interface PassResult {
49
51
  readonly ledger: Ledger;
50
52
  }
51
53
 
52
- /** The exit code of a report holding at least one pending finding. */
53
- export const EXIT_PENDING = 4;
54
-
55
- /** Two strings ordered bytewise. */
56
- function compare(a: string, b: string): number {
57
- if (a === b) {
58
- return 0;
59
- }
60
- return a < b ? -1 : 1;
61
- }
62
-
63
54
  /** The canonical order: path, line, column, code, then the symbol reference. */
64
55
  function canonical(a: Finding, b: Finding): number {
65
56
  return (
66
- compare(a.position.path, b.position.path) ||
57
+ bytewise(a.position.path, b.position.path) ||
67
58
  a.position.line - b.position.line ||
68
59
  a.position.column - b.position.column ||
69
- compare(a.code, b.code) ||
70
- compare(a.symbol.ref, b.symbol.ref)
60
+ bytewise(a.code, b.code) ||
61
+ bytewise(a.symbol.ref, b.symbol.ref)
71
62
  );
72
63
  }
73
64
 
@@ -118,7 +109,7 @@ export function findingsPass(inputs: AnalysisInputs): PassResult {
118
109
  * suppression is stale or a finding's severity reaches the configured failing severity;
119
110
  * clean otherwise.
120
111
  */
121
- export function verdictOf(result: PassResult, config: Config): number {
112
+ export function verdictOf(result: PassResult, config: ResolvedConfig): number {
122
113
  if (result.totals.pending > 0) {
123
114
  return EXIT_PENDING;
124
115
  }
@@ -126,7 +117,7 @@ export function verdictOf(result: PassResult, config: Config): number {
126
117
  return EXIT_FINDINGS;
127
118
  }
128
119
  const failing = result.findings.some((finding) =>
129
- fails(finding.severity, config.reporters.failOn),
120
+ fails(finding.severity, config.reporters.fail_on),
130
121
  );
131
122
  return failing ? EXIT_FINDINGS : EXIT_CLEAN;
132
123
  }
@@ -164,5 +155,5 @@ export function rowKey(row: Recorded): string {
164
155
 
165
156
  /** The analyzer identity a baseline this analyzer writes names as its rows' provenance. */
166
157
  export function analyzerProvenance(version: string): Provenance {
167
- return { analyzer: "deadset-ts", version };
158
+ return { analyzer: PRODUCT, version };
168
159
  }
@@ -55,8 +55,8 @@ function resolveContracts<Brand>(
55
55
  contracts: readonly LifecycleContract[],
56
56
  ): readonly ResolvedContract[] {
57
57
  const entries: DeclarationEntry[] = contracts.flatMap((contract) => [
58
- ...contract.components,
59
- ...contract.bases,
58
+ ...(contract.components ?? []),
59
+ ...(contract.bases ?? []),
60
60
  ]);
61
61
  const resolved = resolveEntries(project, held, entries);
62
62
  let next = 0;
@@ -67,8 +67,8 @@ function resolveContracts<Brand>(
67
67
  };
68
68
  return contracts.map((contract) => ({
69
69
  contract,
70
- components: take(contract.components.length),
71
- bases: take(contract.bases.length),
70
+ components: take(contract.components?.length ?? 0),
71
+ bases: take(contract.bases?.length ?? 0),
72
72
  }));
73
73
  }
74
74
 
@@ -243,11 +243,11 @@ function byBases<Brand>(
243
243
  */
244
244
  export function frameworkLifecycle<Brand>(input: DetectorInput<Brand>): readonly Evidence[] {
245
245
  const { project, held, targetRoot } = input;
246
- if (input.ts.lifecycleContracts.length === 0) {
246
+ if (input.ts.lifecycle_contracts.length === 0) {
247
247
  return [];
248
248
  }
249
249
  const chains = aliasChains(project, held);
250
- const contracts = resolveContracts(project, held, input.ts.lifecycleContracts);
250
+ const contracts = resolveContracts(project, held, input.ts.lifecycle_contracts);
251
251
  const components = [
252
252
  ...byComponents(project, held, chains, contracts),
253
253
  ...byBases(project, held, chains, contracts),
@@ -256,7 +256,7 @@ export function frameworkLifecycle<Brand>(input: DetectorInput<Brand>): readonly
256
256
  return [];
257
257
  }
258
258
  const ids = new Set(components.map((one) => one.id));
259
- const sides: readonly ClassSide[] = input.ts.lifecycleContracts.some((contract) =>
259
+ const sides: readonly ClassSide[] = input.ts.lifecycle_contracts.some((contract) =>
260
260
  contract.members.some((name) => name.endsWith(":static")),
261
261
  )
262
262
  ? ["instance", "static"]
@@ -0,0 +1,70 @@
1
+ /**
2
+ * The one read of a document the target holds at its root, which a checkout under analysis
3
+ * controls: the repository configuration, the ignore file, the baseline and the edges
4
+ * document. The read stays inside the target root, takes a regular file alone, and holds no
5
+ * more of it than the Contract's document bound.
6
+ */
7
+
8
+ import { CONFIG_MAX_BYTES } from "../contract/config.generated.ts";
9
+ import type { Host } from "../host.ts";
10
+ import { failureExit } from "../failure.ts";
11
+ import { joinPath, relativePath } from "../paths.ts";
12
+ import { message } from "../product.ts";
13
+ import { utf8Bytes } from "../reporters/sha256.ts";
14
+ import { UsageError } from "../usage.ts";
15
+
16
+ function unreadable(label: string, reason: string): Error {
17
+ return failureExit(
18
+ "unreadable-document",
19
+ `${label}: ${reason}: make it a regular file inside the target root that the run can read, or remove it`,
20
+ );
21
+ }
22
+
23
+ /** Refuses a document past the Contract's bound with the usage code, naming it `label`. */
24
+ export function refuseOverBound(label: string, text: string): void {
25
+ if (utf8Bytes(text).length > CONFIG_MAX_BYTES) {
26
+ throw new UsageError(`${label}: holds more than ${String(CONFIG_MAX_BYTES)} bytes`);
27
+ }
28
+ }
29
+
30
+ /**
31
+ * The text of the document `name` at the absolute `targetRoot`, undefined where nothing is
32
+ * there. One whose name resolves outside the target root or to anything but a regular file,
33
+ * or whose read fails, is refused by `refuse` naming it `label`, nothing read from it. A
34
+ * document past the bound is read one byte past it and refused as {@link refuseOverBound} does.
35
+ */
36
+ export function readTargetDocument(
37
+ host: Host,
38
+ targetRoot: string,
39
+ name: string,
40
+ label = name,
41
+ refuse: (label: string, reason: string) => Error = unreadable,
42
+ ): string | undefined {
43
+ const attempt = <T>(read: () => T): T => {
44
+ try {
45
+ return read();
46
+ } catch (error: unknown) {
47
+ throw refuse(label, message(error));
48
+ }
49
+ };
50
+ const path = joinPath(targetRoot, name);
51
+ const kind = attempt(() => host.kindOf(path));
52
+ if (kind === "absent") {
53
+ return undefined;
54
+ }
55
+ const real = attempt(() => host.realPath(path));
56
+ if (
57
+ relativePath(
58
+ attempt(() => host.realPath(targetRoot)),
59
+ real,
60
+ ) === undefined
61
+ ) {
62
+ throw refuse(label, `resolves to ${real}, outside the target root`);
63
+ }
64
+ if (kind !== "file") {
65
+ throw refuse(label, "is not a regular file");
66
+ }
67
+ const text = attempt(() => host.readFile(real, CONFIG_MAX_BYTES + 1));
68
+ refuseOverBound(label, text);
69
+ return text;
70
+ }