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

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 (122) hide show
  1. package/dist/src/analysis.js +108 -10
  2. package/dist/src/augmentations.js +62 -18
  3. package/dist/src/automatic-types.js +189 -0
  4. package/dist/src/base-url.js +18 -0
  5. package/dist/src/components.js +7 -6
  6. package/dist/src/config.js +1 -0
  7. package/dist/src/configuration-files.js +50 -12
  8. package/dist/src/conformance-record.js +2 -2
  9. package/dist/src/convention-rows.js +98 -0
  10. package/dist/src/conventions.js +101 -5
  11. package/dist/src/decorator.js +21 -18
  12. package/dist/src/dependencies.js +183 -28
  13. package/dist/src/discover.js +132 -11
  14. package/dist/src/entry-point-gaps.js +7 -10
  15. package/dist/src/entry-points.js +112 -11
  16. package/dist/src/findings/completion.js +172 -40
  17. package/dist/src/findings/dependencies-and-module-machinery.js +71 -25
  18. package/dist/src/findings/emitters.js +1 -1
  19. package/dist/src/findings/intra-function.js +44 -13
  20. package/dist/src/findings/non-code-artifacts.js +66 -33
  21. package/dist/src/findings/unused-declarations.js +30 -5
  22. package/dist/src/findings/visibility-narrowing.js +29 -5
  23. package/dist/src/generated-file.js +11 -0
  24. package/dist/src/git-ignore.js +86 -0
  25. package/dist/src/graph.js +4 -1
  26. package/dist/src/import-bindings.js +97 -0
  27. package/dist/src/indexed-access.js +130 -28
  28. package/dist/src/interface-satisfaction.js +30 -12
  29. package/dist/src/intra-function-parts.js +297 -45
  30. package/dist/src/inventory.js +52 -8
  31. package/dist/src/kinds.js +13 -13
  32. package/dist/src/matrix.js +57 -10
  33. package/dist/src/mdx-entries.js +197 -0
  34. package/dist/src/module-declarations.js +9 -13
  35. package/dist/src/overrides.js +41 -10
  36. package/dist/src/print.js +1 -0
  37. package/dist/src/ref.js +10 -0
  38. package/dist/src/references.js +38 -4
  39. package/dist/src/report.js +4 -2
  40. package/dist/src/resolve.js +2 -0
  41. package/dist/src/roots.js +110 -38
  42. package/dist/src/schema.js +1 -0
  43. package/dist/src/serialization-contract.js +54 -8
  44. package/dist/src/session.js +7 -0
  45. package/dist/src/source-files.js +39 -14
  46. package/dist/src/suppress-file.js +10 -2
  47. package/dist/src/suppress-inline.js +18 -2
  48. package/dist/src/suppress.js +8 -5
  49. package/dist/src/sweep.js +27 -10
  50. package/dist/src/symbol-keys.js +103 -2
  51. package/dist/src/test-support.js +119 -18
  52. package/dist/src/type-errors.js +34 -7
  53. package/dist/src/type-parameter-reads.js +172 -0
  54. package/dist/src/verbs/analyze.js +16 -0
  55. package/dist/src/verbs/explain.js +4 -2
  56. package/dist/src/version.js +1 -1
  57. package/dist/src/workflow-steps.js +119 -22
  58. package/dist/src/workspace-programs.js +7 -3
  59. package/dist/src/workspace-resolution.js +53 -10
  60. package/jsr.json +1 -1
  61. package/package.json +2 -2
  62. package/src/analysis.ts +146 -11
  63. package/src/augmentations.ts +76 -22
  64. package/src/automatic-types.ts +246 -0
  65. package/src/base-url.ts +24 -0
  66. package/src/components.ts +7 -6
  67. package/src/config.ts +3 -0
  68. package/src/configuration-files.ts +66 -12
  69. package/src/conformance-record.ts +2 -2
  70. package/src/convention-rows.ts +128 -0
  71. package/src/conventions.ts +121 -7
  72. package/src/decorator.ts +28 -18
  73. package/src/dependencies.ts +247 -41
  74. package/src/discover.ts +156 -13
  75. package/src/entry-point-gaps.ts +8 -11
  76. package/src/entry-points.ts +133 -11
  77. package/src/finding.ts +15 -0
  78. package/src/findings/completion.ts +222 -46
  79. package/src/findings/dependencies-and-module-machinery.ts +90 -28
  80. package/src/findings/emitters.ts +1 -1
  81. package/src/findings/intra-function.ts +56 -14
  82. package/src/findings/non-code-artifacts.ts +81 -41
  83. package/src/findings/unused-declarations.ts +36 -5
  84. package/src/findings/visibility-narrowing.ts +40 -5
  85. package/src/generated-file.ts +16 -0
  86. package/src/git-ignore.ts +98 -0
  87. package/src/graph.ts +6 -0
  88. package/src/host.ts +5 -9
  89. package/src/import-bindings.ts +128 -0
  90. package/src/indexed-access.ts +152 -30
  91. package/src/interface-satisfaction.ts +33 -13
  92. package/src/intra-function-parts.ts +394 -59
  93. package/src/inventory.ts +71 -7
  94. package/src/kinds.ts +13 -13
  95. package/src/matrix.ts +104 -15
  96. package/src/mdx-entries.ts +241 -0
  97. package/src/module-declarations.ts +11 -13
  98. package/src/overrides.ts +51 -12
  99. package/src/print.ts +1 -0
  100. package/src/ref.ts +12 -0
  101. package/src/references.ts +48 -3
  102. package/src/report.ts +16 -3
  103. package/src/resolve.ts +3 -0
  104. package/src/roots.ts +163 -45
  105. package/src/schema.ts +1 -0
  106. package/src/serialization-contract.ts +55 -4
  107. package/src/session.ts +7 -0
  108. package/src/source-files.ts +66 -16
  109. package/src/suppress-file.ts +12 -1
  110. package/src/suppress-inline.ts +18 -1
  111. package/src/suppress.ts +10 -5
  112. package/src/sweep.ts +32 -11
  113. package/src/symbol-keys.ts +129 -2
  114. package/src/test-support.ts +155 -18
  115. package/src/type-errors.ts +38 -6
  116. package/src/type-parameter-reads.ts +227 -0
  117. package/src/verbs/analyze.ts +22 -0
  118. package/src/verbs/explain.ts +5 -2
  119. package/src/version.ts +1 -1
  120. package/src/workflow-steps.ts +127 -22
  121. package/src/workspace-programs.ts +7 -3
  122. package/src/workspace-resolution.ts +62 -10
@@ -10,11 +10,12 @@
10
10
  * value that is not a literal is not evaluated and not guessed, so a file it would
11
11
  * have named is entered by nothing here.
12
12
  */
13
- import { isArrayLiteralExpression, isAsExpression, isCallExpression, isExportAssignment, isIdentifier, isMetaProperty, isNewExpression, isNoSubstitutionTemplateLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isPropertyAssignment, isSatisfiesExpression, isStringLiteral, } from "@typescript/native/ast";
13
+ import { isArrayLiteralExpression, isAsExpression, isBinaryExpression, isCallExpression, isExportAssignment, isIdentifier, isMetaProperty, isNewExpression, isNoSubstitutionTemplateLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isPropertyAssignment, isSatisfiesExpression, isStringLiteral, isTemplateExpression, SyntaxKind, } from "@typescript/native/ast";
14
14
  import { SymbolFlags } from "@typescript/native/sync";
15
15
  import { configurationFiles, filesNamedBy, moduleStrings, } from "./configuration-files.js";
16
16
  import { globExpression } from "./glob.js";
17
17
  import { htmlEntries } from "./html-entries.js";
18
+ import { documentEntries } from "./mdx-entries.js";
18
19
  import { dirnamePath, relativePath, resolvePath } from "./paths.js";
19
20
  import { UNANSWERED } from "./query.js";
20
21
  import { workflowEntries } from "./workflow-steps.js";
@@ -37,8 +38,11 @@ const BROWSER_TEST_MATCH = "**/*.{spec,test}.{js,ts,jsx,tsx,cjs,cts,cjsx,ctsx,mj
37
38
  const SETUP_MEMBERS = ["setupFiles", "globalSetup"];
38
39
  /** A member that moves the directory every path of a test-runner configuration is read from. */
39
40
  const ROOT_MEMBER = "root";
40
- /** The prefixes a literal is relative with, in a module specifier and a URL reference alike. */
41
- const RELATIVE_PREFIXES = ["./", "../"];
41
+ /**
42
+ * A URL reference's scheme. A reference with none that does not start with a solidus is
43
+ * relative, the bare name included: https://url.spec.whatwg.org/#relative-url-string
44
+ */
45
+ const URL_SCHEME = /^[a-z][a-z\d+.-]*:/iu;
42
46
  /** The constructors whose first argument addresses a worker script. */
43
47
  const WORKER_CONSTRUCTORS = new Set(["Worker", "SharedWorker"]);
44
48
  /** The file's name, the last segment of its path. */
@@ -138,9 +142,16 @@ function literalsOf(value) {
138
142
  }
139
143
  return found;
140
144
  }
141
- /** Whether a literal is relative in the sense a module specifier and a URL reference share. */
145
+ /**
146
+ * Whether a literal is a relative URL reference, which is also how the test runner reads a
147
+ * setup file's path against its root.
148
+ */
142
149
  function isRelative(text) {
143
- return RELATIVE_PREFIXES.some((prefix) => text.startsWith(prefix));
150
+ return text !== "" && !text.startsWith("/") && !URL_SCHEME.test(text);
151
+ }
152
+ /** Whether a module specifier is relative, which a bare name is not: it names a package. */
153
+ function isModuleRelative(text) {
154
+ return text.startsWith("./") || text.startsWith("../");
144
155
  }
145
156
  /** A URL reference with its query and its fragment removed, which name no part of the file. */
146
157
  function pathPart(text) {
@@ -348,10 +359,10 @@ function workerSitesOf(file) {
348
359
  return found;
349
360
  }
350
361
  /**
351
- * The workers one file addresses. A call's names must resolve to the platform's own
352
- * declarations, which no file of the target declares and no import brings in; a
353
- * name the target binds itself is the target's own constructor or object, which
354
- * addresses no worker this rule knows. One batch per file that writes such a call.
362
+ * The workers one file addresses. A call's names must resolve to a symbol the platform
363
+ * declares, which a file of the target may augment and no import brings in; a name only
364
+ * the target declares is the target's own constructor or object, which addresses no
365
+ * worker this rule knows. One batch per file that writes such a call.
355
366
  */
356
367
  function workersOf(project, file, files) {
357
368
  const sites = workerSitesOf(file);
@@ -371,8 +382,7 @@ function workersOf(project, file, files) {
371
382
  }
372
383
  if (symbol !== undefined &&
373
384
  (symbol.flags & SymbolFlags.Alias) === 0 &&
374
- symbol.declarations.length > 0 &&
375
- symbol.declarations.every((handle) => !files.byName.has(handle.path))) {
385
+ symbol.declarations.some((handle) => !files.byName.has(handle.path))) {
376
386
  platform.add(name);
377
387
  }
378
388
  });
@@ -388,6 +398,87 @@ function workersOf(project, file, files) {
388
398
  }
389
399
  return found;
390
400
  }
401
+ /** The literal text a computed path starts with: a template's head, or a concatenation's first literal. */
402
+ function leadingLiteral(node) {
403
+ const at = unwrapped(node);
404
+ if (isTemplateExpression(at)) {
405
+ return at.head.text;
406
+ }
407
+ if (isBinaryExpression(at) && at.operatorToken.kind === SyntaxKind.PlusToken) {
408
+ return literalOf(unwrapped(at.left)) ?? leadingLiteral(at.left);
409
+ }
410
+ return undefined;
411
+ }
412
+ /** Whether one call is `import.meta.glob(...)`. */
413
+ function isMetaGlob(node) {
414
+ if (!isCallExpression(node) || !isPropertyAccessExpression(node.expression)) {
415
+ return false;
416
+ }
417
+ const { expression, name } = node.expression;
418
+ return (isIdentifier(name) &&
419
+ name.text === "glob" &&
420
+ isMetaProperty(expression) &&
421
+ expression.keywordToken === SyntaxKind.ImportKeyword);
422
+ }
423
+ /**
424
+ * The files one file loads by a computed path, each with what exports: every own file
425
+ * below the directory a relative literal start of an `import()` or `require()` argument
426
+ * names, other than the calling file's own directory or one holding it, which would name
427
+ * every sibling, and
428
+ * every own file a relative literal glob of `import.meta.glob` matches, both read against
429
+ * the calling file's directory, as a bundler's context module reads them.
430
+ */
431
+ function computedImportsOf(file, files) {
432
+ const dir = dirnamePath(file.fileName);
433
+ const found = [];
434
+ const below = (prefix) => {
435
+ const target = resolvePath(dir, prefix);
436
+ if (!isModuleRelative(prefix) ||
437
+ !prefix.endsWith("/") ||
438
+ target === dir ||
439
+ relativePath(target, dir) !== undefined) {
440
+ return;
441
+ }
442
+ for (const candidate of files.byName.values()) {
443
+ if (candidate !== file && relativePath(target, candidate.fileName) !== undefined) {
444
+ found.push({ file: candidate, rule: "computed-import", source: prefix, exports: true });
445
+ }
446
+ }
447
+ };
448
+ const globbed = (pattern) => {
449
+ if (!isModuleRelative(pattern)) {
450
+ return;
451
+ }
452
+ const expression = globExpression(resolvePath(dir, pattern));
453
+ for (const candidate of files.byName.values()) {
454
+ if (candidate !== file && expression.test(candidate.fileName)) {
455
+ found.push({ file: candidate, rule: "computed-import", source: pattern, exports: true });
456
+ }
457
+ }
458
+ };
459
+ const visit = (node) => {
460
+ if (isCallExpression(node)) {
461
+ const [first] = node.arguments;
462
+ const callee = node.expression;
463
+ const loads = callee.kind === SyntaxKind.ImportKeyword ||
464
+ (isIdentifier(callee) && callee.text === "require");
465
+ if (loads && first !== undefined) {
466
+ const prefix = leadingLiteral(first);
467
+ if (prefix !== undefined) {
468
+ below(prefix);
469
+ }
470
+ }
471
+ else if (isMetaGlob(node) && first !== undefined) {
472
+ for (const pattern of literalsOf(first) ?? []) {
473
+ globbed(pattern);
474
+ }
475
+ }
476
+ }
477
+ node.forEachChild(visit);
478
+ };
479
+ file.forEachChild(visit);
480
+ return found;
481
+ }
391
482
  /**
392
483
  * Every file of one project the rules enter, in no particular order and possibly more
393
484
  * than once: a file two rules enter appears once per rule.
@@ -417,10 +508,20 @@ export function entryPoints(project, files, testFiles, host, targetRoot, parse,
417
508
  }
418
509
  }
419
510
  found.push(...workersOf(project, file, files));
511
+ found.push(...computedImportsOf(file, files));
420
512
  }
421
513
  for (const { file, source } of htmlEntries(host, targetRoot, files, parse)) {
422
514
  found.push({ file, rule: "html-entry", source, exports: true });
423
515
  }
516
+ for (const { file, source, names } of documentEntries(host, targetRoot, files)) {
517
+ found.push({
518
+ file,
519
+ rule: "document-import",
520
+ source,
521
+ exports: true,
522
+ ...(names === undefined ? {} : { names }),
523
+ });
524
+ }
424
525
  for (const { file, source } of workflowEntries(host, targetRoot, files)) {
425
526
  found.push({ file, rule: "workflow-step", source, exports: true });
426
527
  }
@@ -4,12 +4,27 @@
4
4
  * together with every finding that falls with a root they withhold.
5
5
  */
6
6
  import { componentId } from "../components.js";
7
+ import { deadComponents } from "../matrix.js";
7
8
  import { OUTSIDE } from "../graph.js";
8
9
  import { KINDS } from "../kinds.js";
9
10
  import { byPosition, positionKey } from "../position.js";
10
11
  import { declarationKey } from "../ref.js";
11
12
  import { FAMILY_KEY_LENGTH } from "../resolve.js";
12
13
  import { isPartSubject, isRowSubject } from "../suppress.js";
14
+ /** The unused-file code, whose deletion removes every use its file writes. */
15
+ const FILE_NEVER_IMPORTED = "DS1502";
16
+ /**
17
+ * The codes whose subject the run that counts test references holds live, as
18
+ * finding.schema.json lists them, so a test file's candidate reported under one carries
19
+ * none of a dead declaration's sweep facts.
20
+ */
21
+ const LIVE_SUBJECT = new Set([
22
+ "DS1101",
23
+ "DS1102",
24
+ "DS1104",
25
+ "DS1204",
26
+ "DS1301",
27
+ ]);
13
28
  /** The fixability grammar/analysis.md "Fixability" gives a finding about a held declaration. */
14
29
  const MANUAL = "manual";
15
30
  /** The member kinds an object property can declare, which an `in` expression tests by name. */
@@ -72,15 +87,11 @@ function testedByIn(input) {
72
87
  * published API, a public member of a published type included, is `certain` where the
73
88
  * run loaded every consumer the scope declared and declared one, `probable` where one did
74
89
  * 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`.
90
+ * consumer set. A member an `in` expression tests by name is `possible`.
77
91
  */
78
92
  function reachabilityClasses(input) {
79
93
  const tested = testedByIn(input);
80
94
  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
95
  const { declared } = input.boundary.consumers;
85
96
  const outside = declared.length === 0
86
97
  ? "possible"
@@ -88,12 +99,25 @@ function reachabilityClasses(input) {
88
99
  ? "certain"
89
100
  : "probable";
90
101
  return (id) => {
91
- if (usedByTests.has(id) || tested.has(id)) {
102
+ if (tested.has(id)) {
92
103
  return "possible";
93
104
  }
94
105
  return published.has(id) ? outside : "certain";
95
106
  };
96
107
  }
108
+ /** The target symbols one test of dead code references, by id: production declarations the sweep judges. */
109
+ function testTargets(union, id) {
110
+ return (union.out[union.at(id)] ?? []).flatMap((edge) => {
111
+ const target = union.symbols[edge.to];
112
+ const testCode = union.test[edge.to] === true || union.support[edge.to] === true;
113
+ return target !== undefined &&
114
+ !testCode &&
115
+ union.ambient[edge.to] !== true &&
116
+ union.subject[edge.to] === true
117
+ ? [target.id]
118
+ : [];
119
+ });
120
+ }
97
121
  /**
98
122
  * The confidence ceiling of a test of dead code: the lowest reachability class among the
99
123
  * target symbols it references, because its deadness is theirs, whether or not they share
@@ -102,28 +126,46 @@ function reachabilityClasses(input) {
102
126
  function testCeilings(input, classOf) {
103
127
  const union = input.swept.matrix.union;
104
128
  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
- }
129
+ return (id) => tests.has(id)
130
+ ? testTargets(union, id).reduce((lowest, target) => capped(lowest, classOf(target)), "certain")
131
+ : "certain";
132
+ }
133
+ /**
134
+ * The subjects dead only because production code only tests use is: each declaration the
135
+ * sweep marks test-used, each `DS1502` file of such code, and each test of dead code every
136
+ * target of which is one. Nothing they hold references a dead declaration outside them, so
137
+ * the rest of a component they share is reported without them.
138
+ */
139
+ function testUsedSubjects(input) {
140
+ const { matrix, sweep } = input.swept;
141
+ const union = matrix.union;
142
+ const used = new Set(sweep.candidates.filter((one) => one.testUsed).map((one) => one.id));
143
+ for (const test of sweep.candidates.filter((one) => one.testOfDeadCode)) {
144
+ const targets = testTargets(union, test.id);
145
+ if (targets.length > 0 && targets.every((target) => used.has(target))) {
146
+ used.add(test.id);
116
147
  }
117
- return lowest;
118
- };
148
+ }
149
+ const files = new Set(union.symbols.flatMap((symbol, at) => matrix.testUsed[at] === true ? [symbol.position.path] : []));
150
+ return { declarations: used, files };
151
+ }
152
+ /**
153
+ * The ceiling the roots the run found put on one dead subject: a subject other declarations
154
+ * reference, found dead only because no root reaches them, is `probable` unless
155
+ * the configuration declares the roots complete, because a root the analysis cannot see
156
+ * would make it live; every other declaration has none below `certain`.
157
+ */
158
+ function rootsCeiling(input, finding, candidate) {
159
+ const unreachable = candidate?.relation === "reachability" || finding.unreachableOnly === true;
160
+ return unreachable && !input.config.ts.rootsComplete ? "probable" : "certain";
119
161
  }
120
162
  /**
121
163
  * The component each dead declaration of the run falls in, and none for a live one, with
122
164
  * its members listed in the canonical order where the run lists a component in full.
123
165
  */
124
- function findingComponents(swept, cascade) {
166
+ function findingComponents(swept, components, cascade) {
125
167
  const held = new Map();
126
- for (const component of swept.sweep.components) {
168
+ for (const component of components) {
127
169
  for (const member of component.members) {
128
170
  held.set(member, component);
129
171
  }
@@ -159,6 +201,59 @@ function findingComponents(swept, cascade) {
159
201
  };
160
202
  };
161
203
  }
204
+ /**
205
+ * The run's dead components with each file a `DS1502` finding reports dead among them, so
206
+ * the file's declarations fall with it, and with the components each subject that is no
207
+ * declaration falls with joined into one.
208
+ */
209
+ function runComponents(swept, findings) {
210
+ const union = swept.matrix.union;
211
+ const reported = new Set(findings.filter((one) => one.code === FILE_NEVER_IMPORTED).map((one) => one.position.path));
212
+ const files = new Map(union.symbols.flatMap((symbol, at) => symbol.kind === "file" && reported.has(symbol.position.path) ? [[at, symbol]] : []));
213
+ const computed = files.size === 0
214
+ ? swept.sweep.components
215
+ : deadComponents(swept.matrix, swept.sweep, new Set(files.keys()));
216
+ const place = new Map();
217
+ computed.forEach((component, at) => {
218
+ for (const member of component.members) {
219
+ place.set(member, at);
220
+ }
221
+ });
222
+ const leader = computed.map((_component, at) => at);
223
+ const find = (at) => {
224
+ let top = at;
225
+ while ((leader[top] ?? top) !== top) {
226
+ top = leader[top] ?? top;
227
+ }
228
+ return top;
229
+ };
230
+ for (const { fallsWith } of findings) {
231
+ const joined = (fallsWith ?? []).flatMap((id) => place.get(id) ?? []).map(find);
232
+ const first = Math.min(...joined);
233
+ for (const one of joined) {
234
+ leader[one] = first;
235
+ }
236
+ }
237
+ const merged = new Map();
238
+ computed.forEach((component, at) => {
239
+ const top = find(at);
240
+ const into = merged.get(top);
241
+ merged.set(top, into === undefined
242
+ ? component
243
+ : {
244
+ id: into.id,
245
+ members: [...into.members, ...component.members],
246
+ roots: [...into.roots, ...component.roots],
247
+ deletableLines: into.deletableLines + component.deletableLines,
248
+ held: into.held && component.held,
249
+ });
250
+ });
251
+ return {
252
+ components: [...merged.values()],
253
+ computed: computed.length,
254
+ files: new Map([...files.values()].map((symbol) => [symbol.position.path, symbol.id])),
255
+ };
256
+ }
162
257
  /**
163
258
  * Every finding of the run completed, in the order given. A dead declaration carries its
164
259
  * candidate's relation, counts, configurations and component; a live one carries no relation and
@@ -174,10 +269,13 @@ export function completed(input, findings, kinds = KINDS) {
174
269
  const candidates = new Map(swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
175
270
  const classOf = reachabilityClasses(input);
176
271
  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));
272
+ const testUsed = testUsedSubjects(input);
273
+ const { components, computed, files } = runComponents(swept, findings);
274
+ const componentOf = findingComponents(swept, components, input.config.reporters.cascade);
275
+ const held = new Set(components.filter((one) => one.held).flatMap((one) => one.members));
276
+ // A join keeps its first component's identifier, so a minted one counts past every computed one.
277
+ let minted = computed;
278
+ const dead = new Set(components.map((component) => component.id));
181
279
  const fallingWith = new Map();
182
280
  for (const { heldByTests } of swept.sweep.candidates) {
183
281
  if (heldByTests !== undefined) {
@@ -193,18 +291,30 @@ export function completed(input, findings, kinds = KINDS) {
193
291
  // a row is still never the declaration. Two declarations of one file can share a
194
292
  // reference, so the one the finding is positioned at is the subject where there is one.
195
293
  const declared = union.at(positionKey(finding.position));
294
+ const ref = finding.symbol.holder ?? finding.symbol.ref;
196
295
  const at = isRowSubject(finding.symbol.kind)
197
296
  ? OUTSIDE
198
- : union.symbols[declared]?.ref === finding.symbol.ref
297
+ : union.symbols[declared]?.ref === ref
199
298
  ? declared
200
- : (positions.get(declarationKey(finding.symbol.ref, finding.position.path)) ?? OUTSIDE);
299
+ : (positions.get(declarationKey(ref, finding.position.path)) ?? OUTSIDE);
201
300
  // A part names the declaration that holds it, and is decided inside it rather than by
202
301
  // the declaration's liveness, so it carries none of the declaration's sweep facts.
203
302
  const part = isPartSubject(finding.symbol.kind);
204
303
  const symbol = union.symbols[at];
205
304
  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);
305
+ const candidate = judged === undefined || LIVE_SUBJECT.has(finding.code)
306
+ ? undefined
307
+ : candidates.get(judged.id);
308
+ const fallsWith = finding.fallsWith?.[0];
309
+ const joined = fallsWith === undefined ? undefined : componentOf(fallsWith);
310
+ const file = finding.code === FILE_NEVER_IMPORTED ? files.get(finding.position.path) : undefined;
311
+ const found = joined !== undefined
312
+ ? { ...joined, root: false }
313
+ : file !== undefined
314
+ ? componentOf(file)
315
+ : symbol === undefined || part
316
+ ? undefined
317
+ : componentOf(symbol.id);
208
318
  // A test file's declaration is a component member only as a test of dead code, and
209
319
  // test code keeps the test-support code it references out of every component.
210
320
  if (candidate !== undefined &&
@@ -226,7 +336,12 @@ export function completed(input, findings, kinds = KINDS) {
226
336
  const counts = candidate === undefined
227
337
  ? union.made[at]
228
338
  : { production: candidate.productionRefs, test: candidate.testRefs };
229
- const reachabilityClass = judged === undefined ? (finding.reachabilityClass ?? "certain") : classOf(judged.id);
339
+ const ownClass = judged === undefined ? (finding.reachabilityClass ?? "certain") : classOf(judged.id);
340
+ const used = judged !== undefined
341
+ ? candidate !== undefined && testUsed.declarations.has(judged.id)
342
+ : finding.code === FILE_NEVER_IMPORTED && testUsed.files.has(finding.position.path);
343
+ const reachabilityClass = used && candidate?.testOfDeadCode !== true ? "possible" : ownClass;
344
+ const untested = capped(capped(capped(ownClass, row.maxClass), rootsCeiling(input, finding, candidate)), judged === undefined ? "certain" : ceilingOf(judged.id));
230
345
  const generated = input.files.generated?.has(finding.position.path) === true;
231
346
  const fixability = generated
232
347
  ? "none"
@@ -236,9 +351,13 @@ export function completed(input, findings, kinds = KINDS) {
236
351
  row.findingFixabilities?.includes(MANUAL) === true
237
352
  ? MANUAL
238
353
  : row.fixability;
239
- const lastUses = fixability === "deletable" && candidate !== undefined
240
- ? input.dependencies.lastUses.get(candidate.id)
241
- : undefined;
354
+ const lastUses = fixability !== "deletable"
355
+ ? undefined
356
+ : candidate !== undefined
357
+ ? input.dependencies.lastUses.get(candidate.id)
358
+ : finding.code === FILE_NEVER_IMPORTED
359
+ ? input.dependencies.fileLastUses.get(finding.position.path)
360
+ : undefined;
242
361
  return {
243
362
  code: finding.code,
244
363
  kind: row.name,
@@ -246,7 +365,8 @@ export function completed(input, findings, kinds = KINDS) {
246
365
  position: finding.position,
247
366
  symbol: finding.symbol,
248
367
  reachabilityClass,
249
- confidence: capped(capped(reachabilityClass, row.maxClass), judged === undefined ? "certain" : ceilingOf(judged.id)),
368
+ confidence: used ? "possible" : untested,
369
+ ...(used ? { componentConfidence: untested } : {}),
250
370
  ...(candidate === undefined ? {} : { livenessRelation: candidate.relation }),
251
371
  testOnly: judged !== undefined && counts?.production === 0 && counts.test > 0,
252
372
  generated,
@@ -270,7 +390,7 @@ export function completed(input, findings, kinds = KINDS) {
270
390
  /**
271
391
  * The findings with every finding of a dead component capped by the lowest confidence
272
392
  * among the component's root members, so the minimum confidence reports or withholds a
273
- * component whole.
393
+ * component whole but for the findings test-used code lowers on their own.
274
394
  */
275
395
  function componentCapped(findings, dead) {
276
396
  const lowest = new Map();
@@ -278,16 +398,27 @@ function componentCapped(findings, dead) {
278
398
  const { id, root } = finding.component;
279
399
  if (root && dead.has(id)) {
280
400
  const held = lowest.get(id);
281
- lowest.set(id, held === undefined ? finding.confidence : capped(held, finding.confidence));
401
+ const own = componentConfidenceOf(finding);
402
+ lowest.set(id, held === undefined ? own : capped(held, own));
282
403
  }
283
404
  }
284
405
  return findings.map((finding) => {
285
406
  const cap = lowest.get(finding.component.id);
286
- return cap === undefined || RANK[cap] >= RANK[finding.confidence]
287
- ? finding
288
- : { ...finding, confidence: cap };
407
+ if (cap === undefined) {
408
+ return finding;
409
+ }
410
+ const own = finding.componentConfidence;
411
+ return {
412
+ ...finding,
413
+ confidence: capped(finding.confidence, cap),
414
+ ...(own === undefined ? {} : { componentConfidence: capped(own, cap) }),
415
+ };
289
416
  });
290
417
  }
418
+ /** The confidence a finding gives its component's cap and the dials. */
419
+ function componentConfidenceOf(finding) {
420
+ return finding.componentConfidence ?? finding.confidence;
421
+ }
291
422
  /** Whether the dials withhold a finding by its own severity or confidence. */
292
423
  function dialed(finding, minConfidence) {
293
424
  return finding.severity === "allow" || RANK[finding.confidence] < RANK[minConfidence];
@@ -302,7 +433,8 @@ function dialed(finding, minConfidence) {
302
433
  export function dialsOf(input, findings) {
303
434
  const minConfidence = input.config.analysis.minConfidence;
304
435
  const components = new Set(findings
305
- .filter((finding) => finding.component.root && dialed(finding, minConfidence))
436
+ .filter((finding) => finding.component.root &&
437
+ dialed({ ...finding, confidence: componentConfidenceOf(finding) }, minConfidence))
306
438
  .map((finding) => finding.component.id));
307
439
  return {
308
440
  allows: (code) => {
@@ -1,38 +1,84 @@
1
1
  import { declarationKey } from "../dependencies.js";
2
2
  import { renderRef } from "../ref.js";
3
+ import { nonCodeArtifacts } from "./non-code-artifacts.js";
3
4
  const UNUSED_DEPENDENCY = "DS1601";
5
+ const FILE_NEVER_IMPORTED = "DS1502";
4
6
  const MESSAGES = {
5
7
  dependency: "no import in the target needs the declared dependency",
6
8
  "dev-dependency": "no import in the target needs the declared development dependency",
7
9
  "peer-dependency": "no import in the target needs the declared peer dependency",
8
10
  };
11
+ const DEAD_USERS_MESSAGES = {
12
+ dependency: "only code that is itself dead uses the declared dependency",
13
+ "dev-dependency": "only code that is itself dead uses the declared development dependency",
14
+ "peer-dependency": "only code that is itself dead uses the declared peer dependency",
15
+ };
16
+ /**
17
+ * The run's dead code: test code keeps the code it references out of every dead component
18
+ * unless that code is dead as well, and a file's top level is dead where the file is.
19
+ */
20
+ function deadCode(input) {
21
+ const { swept } = input;
22
+ const union = swept.matrix.union;
23
+ const files = new Map(union.symbols.filter((one) => one.kind === "file").map((one) => [one.position.path, one.id]));
24
+ const dead = new Set(swept.sweep.components.flatMap((component) => component.members));
25
+ const reachedOnly = new Set(swept.sweep.candidates.filter((one) => one.relation === "reachability").map((one) => one.id));
26
+ for (const finding of nonCodeArtifacts(input)) {
27
+ const id = files.get(finding.position.path);
28
+ if (finding.code === FILE_NEVER_IMPORTED && id !== undefined) {
29
+ dead.add(id);
30
+ if (finding.unreachableOnly === true) {
31
+ reachedOnly.add(id);
32
+ }
33
+ }
34
+ }
35
+ const published = new Set(union.rooted
36
+ .filter((root) => root.kind === "published-api")
37
+ .flatMap((root) => union.symbols[root.at]?.id ?? []));
38
+ return { dead, reachedOnly, published };
39
+ }
9
40
  /**
10
41
  * A dependency, development dependency or peer dependency of a manifest the run reads
11
- * that the target does not need, at the position of its key.
42
+ * that the target does not need, at the position of its key, and one whose every use is
43
+ * held by dead code, which is dead only through that code and falls with its component:
44
+ * at `possible` where one of its holders is published, and found under reachability alone
45
+ * where one of them is.
12
46
  */
13
- function unusedDependencies(dependencies) {
14
- return dependencies.declared
15
- .filter((dependency) => !dependencies.needed.has(declarationKey(dependency)) &&
16
- !dependencies.ran.has(declarationKey(dependency)))
17
- .map((dependency) => ({
18
- code: UNUSED_DEPENDENCY,
19
- position: dependency.position,
20
- symbol: {
21
- ref: renderRef(dependency.manifest, {
22
- of: "dependency",
23
- name: dependency.name,
24
- section: dependency.section,
25
- }),
26
- kind: "dependency",
27
- name: dependency.name,
28
- sizeLines: 1,
29
- },
30
- message: MESSAGES[dependency.section],
31
- details: { dependencyClass: dependency.section },
32
- ...(dependencies.unbuilt.has(dependency.position.path)
33
- ? { reachabilityClass: "possible" }
34
- : {}),
35
- }));
47
+ function unusedDependencies(input, dependencies) {
48
+ const { dead, reachedOnly, published } = deadCode(input);
49
+ return dependencies.declared.flatMap((dependency) => {
50
+ const key = declarationKey(dependency);
51
+ const held = dependencies.heldBy.get(key);
52
+ const users = held?.every((id) => dead.has(id)) === true ? held : undefined;
53
+ if (dependencies.ran.has(key) || (dependencies.needed.has(key) && users === undefined)) {
54
+ return [];
55
+ }
56
+ const unbuilt = dependencies.unbuilt.has(dependency.position.path);
57
+ const atPossible = unbuilt || users?.some((id) => published.has(id)) === true;
58
+ return [
59
+ {
60
+ code: UNUSED_DEPENDENCY,
61
+ position: dependency.position,
62
+ symbol: {
63
+ ref: renderRef(dependency.manifest, {
64
+ of: "dependency",
65
+ name: dependency.name,
66
+ section: dependency.section,
67
+ }),
68
+ kind: "dependency",
69
+ name: dependency.name,
70
+ sizeLines: 1,
71
+ },
72
+ message: (users === undefined ? MESSAGES : DEAD_USERS_MESSAGES)[dependency.section],
73
+ details: { dependencyClass: dependency.section },
74
+ ...(atPossible ? { reachabilityClass: "possible" } : {}),
75
+ ...(users?.some((id) => reachedOnly.has(id)) === true
76
+ ? { unreachableOnly: true }
77
+ : {}),
78
+ ...(users === undefined ? {} : { fallsWith: users }),
79
+ },
80
+ ];
81
+ });
36
82
  }
37
83
  /** The findings of the dependencies-and-module-machinery family, `DS1600` to `DS1699`. */
38
- export const dependenciesAndModuleMachinery = ({ dependencies }) => unusedDependencies(dependencies);
84
+ export const dependenciesAndModuleMachinery = (input) => unusedDependencies(input, input.dependencies);
@@ -100,7 +100,7 @@ function dialledOver(input, suppressed, found, unmarked) {
100
100
  const unanswered = heldKeys(input);
101
101
  const markup = markupKeys(input);
102
102
  const candidates = found.filter((finding) => {
103
- const key = declarationKey(finding.symbol.ref, finding.position.path);
103
+ const key = declarationKey(finding.symbol.holder ?? finding.symbol.ref, finding.position.path);
104
104
  return (!ledger.withheld(finding) &&
105
105
  !unanswered.has(key) &&
106
106
  !markup.has(key) &&