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

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 +106 -10
  2. package/dist/src/augmentations.js +21 -8
  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 +75 -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 +116 -21
  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 +21 -1
  22. package/dist/src/findings/visibility-narrowing.js +22 -3
  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 +18 -9
  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 +4 -1
  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 +7 -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 +121 -17
  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 +143 -10
  63. package/src/augmentations.ts +21 -10
  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 +103 -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 +10 -0
  78. package/src/findings/completion.ts +147 -24
  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 +25 -1
  84. package/src/findings/visibility-narrowing.ts +32 -3
  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 +32 -14
  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 +10 -1
  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 +9 -5
  112. package/src/sweep.ts +32 -11
  113. package/src/symbol-keys.ts +129 -2
  114. package/src/test-support.ts +149 -16
  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,7 +99,7 @@ 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";
@@ -110,20 +121,33 @@ function testCeilings(input, classOf) {
110
121
  for (const edge of union.out[union.at(id)] ?? []) {
111
122
  const target = union.symbols[edge.to];
112
123
  const testCode = union.test[edge.to] === true || union.support[edge.to] === true;
113
- if (target !== undefined && !testCode && union.subject[edge.to] === true) {
124
+ if (target !== undefined &&
125
+ !testCode &&
126
+ union.ambient[edge.to] !== true &&
127
+ union.subject[edge.to] === true) {
114
128
  lowest = capped(lowest, classOf(target.id));
115
129
  }
116
130
  }
117
131
  return lowest;
118
132
  };
119
133
  }
134
+ /**
135
+ * The ceiling the roots the run found put on one dead subject: a subject other declarations
136
+ * reference, found dead only because no root reaches them, is `probable` unless
137
+ * the configuration declares the roots complete, because a root the analysis cannot see
138
+ * would make it live; every other declaration has none below `certain`.
139
+ */
140
+ function rootsCeiling(input, finding, candidate) {
141
+ const unreachable = candidate?.relation === "reachability" || finding.unreachableOnly === true;
142
+ return unreachable && !input.config.ts.rootsComplete ? "probable" : "certain";
143
+ }
120
144
  /**
121
145
  * The component each dead declaration of the run falls in, and none for a live one, with
122
146
  * its members listed in the canonical order where the run lists a component in full.
123
147
  */
124
- function findingComponents(swept, cascade) {
148
+ function findingComponents(swept, components, cascade) {
125
149
  const held = new Map();
126
- for (const component of swept.sweep.components) {
150
+ for (const component of components) {
127
151
  for (const member of component.members) {
128
152
  held.set(member, component);
129
153
  }
@@ -159,6 +183,59 @@ function findingComponents(swept, cascade) {
159
183
  };
160
184
  };
161
185
  }
186
+ /**
187
+ * The run's dead components with each file a `DS1502` finding reports dead among them, so
188
+ * the file's declarations fall with it, and with the components each subject that is no
189
+ * declaration falls with joined into one.
190
+ */
191
+ function runComponents(swept, findings) {
192
+ const union = swept.matrix.union;
193
+ const reported = new Set(findings.filter((one) => one.code === FILE_NEVER_IMPORTED).map((one) => one.position.path));
194
+ const files = new Map(union.symbols.flatMap((symbol, at) => symbol.kind === "file" && reported.has(symbol.position.path) ? [[at, symbol]] : []));
195
+ const computed = files.size === 0
196
+ ? swept.sweep.components
197
+ : deadComponents(swept.matrix, swept.sweep, new Set(files.keys()));
198
+ const place = new Map();
199
+ computed.forEach((component, at) => {
200
+ for (const member of component.members) {
201
+ place.set(member, at);
202
+ }
203
+ });
204
+ const leader = computed.map((_component, at) => at);
205
+ const find = (at) => {
206
+ let top = at;
207
+ while ((leader[top] ?? top) !== top) {
208
+ top = leader[top] ?? top;
209
+ }
210
+ return top;
211
+ };
212
+ for (const { fallsWith } of findings) {
213
+ const joined = (fallsWith ?? []).flatMap((id) => place.get(id) ?? []).map(find);
214
+ const first = Math.min(...joined);
215
+ for (const one of joined) {
216
+ leader[one] = first;
217
+ }
218
+ }
219
+ const merged = new Map();
220
+ computed.forEach((component, at) => {
221
+ const top = find(at);
222
+ const into = merged.get(top);
223
+ merged.set(top, into === undefined
224
+ ? component
225
+ : {
226
+ id: into.id,
227
+ members: [...into.members, ...component.members],
228
+ roots: [...into.roots, ...component.roots],
229
+ deletableLines: into.deletableLines + component.deletableLines,
230
+ held: into.held && component.held,
231
+ });
232
+ });
233
+ return {
234
+ components: [...merged.values()],
235
+ computed: computed.length,
236
+ files: new Map([...files.values()].map((symbol) => [symbol.position.path, symbol.id])),
237
+ };
238
+ }
162
239
  /**
163
240
  * Every finding of the run completed, in the order given. A dead declaration carries its
164
241
  * candidate's relation, counts, configurations and component; a live one carries no relation and
@@ -174,10 +251,12 @@ export function completed(input, findings, kinds = KINDS) {
174
251
  const candidates = new Map(swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
175
252
  const classOf = reachabilityClasses(input);
176
253
  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));
254
+ const { components, computed, files } = runComponents(swept, findings);
255
+ const componentOf = findingComponents(swept, components, input.config.reporters.cascade);
256
+ const held = new Set(components.filter((one) => one.held).flatMap((one) => one.members));
257
+ // A join keeps its first component's identifier, so a minted one counts past every computed one.
258
+ let minted = computed;
259
+ const dead = new Set(components.map((component) => component.id));
181
260
  const fallingWith = new Map();
182
261
  for (const { heldByTests } of swept.sweep.candidates) {
183
262
  if (heldByTests !== undefined) {
@@ -193,18 +272,30 @@ export function completed(input, findings, kinds = KINDS) {
193
272
  // a row is still never the declaration. Two declarations of one file can share a
194
273
  // reference, so the one the finding is positioned at is the subject where there is one.
195
274
  const declared = union.at(positionKey(finding.position));
275
+ const ref = finding.symbol.holder ?? finding.symbol.ref;
196
276
  const at = isRowSubject(finding.symbol.kind)
197
277
  ? OUTSIDE
198
- : union.symbols[declared]?.ref === finding.symbol.ref
278
+ : union.symbols[declared]?.ref === ref
199
279
  ? declared
200
- : (positions.get(declarationKey(finding.symbol.ref, finding.position.path)) ?? OUTSIDE);
280
+ : (positions.get(declarationKey(ref, finding.position.path)) ?? OUTSIDE);
201
281
  // A part names the declaration that holds it, and is decided inside it rather than by
202
282
  // the declaration's liveness, so it carries none of the declaration's sweep facts.
203
283
  const part = isPartSubject(finding.symbol.kind);
204
284
  const symbol = union.symbols[at];
205
285
  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);
286
+ const candidate = judged === undefined || LIVE_SUBJECT.has(finding.code)
287
+ ? undefined
288
+ : candidates.get(judged.id);
289
+ const fallsWith = finding.fallsWith?.[0];
290
+ const joined = fallsWith === undefined ? undefined : componentOf(fallsWith);
291
+ const file = finding.code === FILE_NEVER_IMPORTED ? files.get(finding.position.path) : undefined;
292
+ const found = joined !== undefined
293
+ ? { ...joined, root: false }
294
+ : file !== undefined
295
+ ? componentOf(file)
296
+ : symbol === undefined || part
297
+ ? undefined
298
+ : componentOf(symbol.id);
208
299
  // A test file's declaration is a component member only as a test of dead code, and
209
300
  // test code keeps the test-support code it references out of every component.
210
301
  if (candidate !== undefined &&
@@ -236,9 +327,13 @@ export function completed(input, findings, kinds = KINDS) {
236
327
  row.findingFixabilities?.includes(MANUAL) === true
237
328
  ? MANUAL
238
329
  : row.fixability;
239
- const lastUses = fixability === "deletable" && candidate !== undefined
240
- ? input.dependencies.lastUses.get(candidate.id)
241
- : undefined;
330
+ const lastUses = fixability !== "deletable"
331
+ ? undefined
332
+ : candidate !== undefined
333
+ ? input.dependencies.lastUses.get(candidate.id)
334
+ : finding.code === FILE_NEVER_IMPORTED
335
+ ? input.dependencies.fileLastUses.get(finding.position.path)
336
+ : undefined;
242
337
  return {
243
338
  code: finding.code,
244
339
  kind: row.name,
@@ -246,7 +341,7 @@ export function completed(input, findings, kinds = KINDS) {
246
341
  position: finding.position,
247
342
  symbol: finding.symbol,
248
343
  reachabilityClass,
249
- confidence: capped(capped(reachabilityClass, row.maxClass), judged === undefined ? "certain" : ceilingOf(judged.id)),
344
+ confidence: capped(capped(capped(reachabilityClass, row.maxClass), rootsCeiling(input, finding, candidate)), judged === undefined ? "certain" : ceilingOf(judged.id)),
250
345
  ...(candidate === undefined ? {} : { livenessRelation: candidate.relation }),
251
346
  testOnly: judged !== undefined && counts?.production === 0 && counts.test > 0,
252
347
  generated,
@@ -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) &&
@@ -1,4 +1,5 @@
1
1
  import { KINDS } from "../kinds.js";
2
+ import { parameterRef } from "../ref.js";
2
3
  import { UNUSED_PARAMETER, UNUSED_RESULT } from "../intra-function-parts.js";
3
4
  import { reachedWithTheTests } from "../sweep.js";
4
5
  /** A list of rules as a sentence names them. */
@@ -8,24 +9,43 @@ function spelled(rules) {
8
9
  }
9
10
  return `${rules.slice(0, -1).join(", ")} and ${rules.at(-1) ?? ""}`;
10
11
  }
11
- /** The clause a message ends with: the other rules that report the kind, or that none is known. */
12
- function overlapClause(overlap) {
13
- return overlap.length === 1 && overlap[0] === "none known"
12
+ /** The compiler's rule of the parameter kind's overlap list, which reports only in a file it checks. */
13
+ const COMPILER_PARAMETER_RULE = "tsc --noUnusedParameters";
14
+ /**
15
+ * The clause a message ends with: the other rules that report the part, or that none is
16
+ * known. A parameter of a file the compiler does not check is no report of the compiler's.
17
+ */
18
+ function overlapClause(overlap, part) {
19
+ const rules = part.parameter?.checked === false
20
+ ? overlap.filter((rule) => rule !== COMPILER_PARAMETER_RULE)
21
+ : overlap;
22
+ return rules.length === 1 && rules[0] === "none known"
14
23
  ? "no other rule is known to report it"
15
- : `also reported by ${spelled(overlap)}`;
24
+ : `also reported by ${spelled(rules)}`;
16
25
  }
17
26
  /**
18
- * Whether one parameter's name is reported: every unread one of a function whose
19
- * signature is free, and of a function used as a value only one no read parameter follows,
20
- * because a caller may pass such a function more arguments than it declares.
27
+ * Whether one unread parameter is reported, and how. Of a free signature it is deleted. A
28
+ * caller may pass a function used as a value more arguments than it declares, so there one
29
+ * a read parameter follows is renamed to an underscore name, which the compilers read as
30
+ * declared unread, and one no read parameter follows is deleted: kept where the call's
31
+ * inference may read its annotation, at `probable` where nothing reads the annotation.
21
32
  */
22
- function parameterReported(part, free, freeButValued) {
33
+ function parameterVerdict(part, free, freeButValued) {
23
34
  const place = part.parameter;
24
35
  if (place?.inValue !== true && free.has(part.declaration)) {
25
- return true;
36
+ return place?.annotated === undefined ? { fix: "delete", confidence: "certain" } : undefined;
26
37
  }
27
38
  const valued = place?.inValue === true || freeButValued.has(part.declaration);
28
- return valued && place?.beforeARead !== true;
39
+ if (!valued) {
40
+ return undefined;
41
+ }
42
+ if (place?.beforeARead === true) {
43
+ return { fix: "rename" };
44
+ }
45
+ if (place?.annotated === "steers") {
46
+ return undefined;
47
+ }
48
+ return { fix: "delete", confidence: place?.annotated === "free" ? "probable" : "certain" };
29
49
  }
30
50
  /**
31
51
  * Whether one part is reported. A parameter needs its function's signature free to
@@ -41,7 +61,7 @@ function reported(input, part, callersUnknown) {
41
61
  const { free, freeButValued, discardedEverywhere } = input.intraFunction;
42
62
  switch (part.code) {
43
63
  case UNUSED_PARAMETER:
44
- return parameterReported(part, free, freeButValued);
64
+ return parameterVerdict(part, free, freeButValued) !== undefined;
45
65
  case UNUSED_RESULT:
46
66
  return (free.has(part.declaration) &&
47
67
  discardedEverywhere.has(part.declaration) &&
@@ -96,20 +116,31 @@ export const intraFunction = (input) => {
96
116
  if (declaration === undefined || !judged(declaration.id) || !reported(input, part, unknown)) {
97
117
  continue;
98
118
  }
119
+ const verdict = part.code === UNUSED_PARAMETER
120
+ ? parameterVerdict(part, input.intraFunction.free, input.intraFunction.freeButValued)
121
+ : undefined;
122
+ const message = verdict?.fix === "rename"
123
+ ? `${part.message}, and a read parameter follows it, so rename it to _${part.name}`
124
+ : part.message;
99
125
  found.push({
100
126
  code: part.code,
101
127
  position: part.position,
102
128
  symbol: {
103
- ref: declaration.ref,
129
+ ...(part.code === UNUSED_PARAMETER
130
+ ? { ref: parameterRef(declaration.ref, part.name), holder: declaration.ref }
131
+ : { ref: declaration.ref }),
104
132
  kind: part.kind,
105
133
  name: part.name,
106
134
  sizeLines: part.position.endLine - part.position.line + 1,
107
135
  },
108
- message: `${part.message}; ${overlapClause(overlap)}`,
136
+ message: `${message}; ${overlapClause(overlap, part)}`,
109
137
  details: {
110
138
  ...(part.kind === "store" ? { writePositions: [part.position] } : {}),
111
139
  overlap,
112
140
  },
141
+ ...(verdict?.fix === "delete" && verdict.confidence !== "certain"
142
+ ? { reachabilityClass: verdict.confidence }
143
+ : {}),
113
144
  });
114
145
  }
115
146
  return found;