@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
@@ -6,6 +6,7 @@
6
6
 
7
7
  import type { RunSweep } from "../analysis.ts";
8
8
  import { componentId, type Component } from "../components.ts";
9
+ import { deadComponents } from "../matrix.ts";
9
10
  import type { Cascade, Confidence, Severity } from "../config.ts";
10
11
  import type { CompletedFinding, Finding, FindingComponent, PositionedSymbol } from "../finding.ts";
11
12
  import { OUTSIDE, type Graph } from "../graph.ts";
@@ -15,9 +16,26 @@ import { byPosition, positionKey } from "../position.ts";
15
16
  import { declarationKey } from "../ref.ts";
16
17
  import { FAMILY_KEY_LENGTH } from "../resolve.ts";
17
18
  import { isPartSubject, isRowSubject, type Dials } from "../suppress.ts";
19
+ import type { Candidate } from "../sweep.ts";
18
20
  import type { Boundary } from "./boundary.ts";
19
21
  import type { EmitterInput } from "./emitter.ts";
20
22
 
23
+ /** The unused-file code, whose deletion removes every use its file writes. */
24
+ const FILE_NEVER_IMPORTED = "DS1502";
25
+
26
+ /**
27
+ * The codes whose subject the run that counts test references holds live, as
28
+ * finding.schema.json lists them, so a test file's candidate reported under one carries
29
+ * none of a dead declaration's sweep facts.
30
+ */
31
+ const LIVE_SUBJECT: ReadonlySet<string> = new Set([
32
+ "DS1101",
33
+ "DS1102",
34
+ "DS1104",
35
+ "DS1204",
36
+ "DS1301",
37
+ ]);
38
+
21
39
  /** The fixability grammar/analysis.md "Fixability" gives a finding about a held declaration. */
22
40
  const MANUAL = "manual";
23
41
 
@@ -94,17 +112,11 @@ function testedByIn(input: EmitterInput): ReadonlySet<string> {
94
112
  * published API, a public member of a published type included, is `certain` where the
95
113
  * run loaded every consumer the scope declared and declared one, `probable` where one did
96
114
  * not load, and `possible` where it declared none, whatever the configuration says of the
97
- * consumer set. Test-support code test code references, outside the test-of-dead-code
98
- * rule, what it contains, and a member an `in` expression tests by name are `possible`.
115
+ * consumer set. A member an `in` expression tests by name is `possible`.
99
116
  */
100
117
  function reachabilityClasses(input: EmitterInput): (id: string) => Confidence {
101
118
  const tested = testedByIn(input);
102
119
  const published = publishedIds(input.swept.matrix.union);
103
- const usedByTests = new Set(
104
- input.swept.sweep.candidates
105
- .filter((candidate) => candidate.heldByTests !== undefined)
106
- .map((candidate) => candidate.id),
107
- );
108
120
  const { declared } = input.boundary.consumers;
109
121
  const outside: Confidence =
110
122
  declared.length === 0
@@ -113,7 +125,7 @@ function reachabilityClasses(input: EmitterInput): (id: string) => Confidence {
113
125
  ? "certain"
114
126
  : "probable";
115
127
  return (id) => {
116
- if (usedByTests.has(id) || tested.has(id)) {
128
+ if (tested.has(id)) {
117
129
  return "possible";
118
130
  }
119
131
  return published.has(id) ? outside : "certain";
@@ -141,7 +153,12 @@ function testCeilings(
141
153
  for (const edge of union.out[union.at(id)] ?? []) {
142
154
  const target = union.symbols[edge.to];
143
155
  const testCode = union.test[edge.to] === true || union.support[edge.to] === true;
144
- if (target !== undefined && !testCode && union.subject[edge.to] === true) {
156
+ if (
157
+ target !== undefined &&
158
+ !testCode &&
159
+ union.ambient[edge.to] !== true &&
160
+ union.subject[edge.to] === true
161
+ ) {
145
162
  lowest = capped(lowest, classOf(target.id));
146
163
  }
147
164
  }
@@ -149,16 +166,32 @@ function testCeilings(
149
166
  };
150
167
  }
151
168
 
169
+ /**
170
+ * The ceiling the roots the run found put on one dead subject: a subject other declarations
171
+ * reference, found dead only because no root reaches them, is `probable` unless
172
+ * the configuration declares the roots complete, because a root the analysis cannot see
173
+ * would make it live; every other declaration has none below `certain`.
174
+ */
175
+ function rootsCeiling(
176
+ input: EmitterInput,
177
+ finding: Finding,
178
+ candidate: Candidate | undefined,
179
+ ): Confidence {
180
+ const unreachable = candidate?.relation === "reachability" || finding.unreachableOnly === true;
181
+ return unreachable && !input.config.ts.rootsComplete ? "probable" : "certain";
182
+ }
183
+
152
184
  /**
153
185
  * The component each dead declaration of the run falls in, and none for a live one, with
154
186
  * its members listed in the canonical order where the run lists a component in full.
155
187
  */
156
188
  function findingComponents(
157
189
  swept: RunSweep,
190
+ components: readonly Component[],
158
191
  cascade: Cascade,
159
192
  ): (id: string) => FindingComponent | undefined {
160
193
  const held = new Map<string, Component>();
161
- for (const component of swept.sweep.components) {
194
+ for (const component of components) {
162
195
  for (const member of component.members) {
163
196
  held.set(member, component);
164
197
  }
@@ -196,6 +229,77 @@ function findingComponents(
196
229
  };
197
230
  }
198
231
 
232
+ /**
233
+ * The run's dead components with each file a `DS1502` finding reports dead among them, so
234
+ * the file's declarations fall with it, and with the components each subject that is no
235
+ * declaration falls with joined into one.
236
+ */
237
+ function runComponents(
238
+ swept: RunSweep,
239
+ findings: readonly Finding[],
240
+ ): {
241
+ readonly components: readonly Component[];
242
+ readonly computed: number;
243
+ readonly files: ReadonlyMap<string, string>;
244
+ } {
245
+ const union = swept.matrix.union;
246
+ const reported = new Set(
247
+ findings.filter((one) => one.code === FILE_NEVER_IMPORTED).map((one) => one.position.path),
248
+ );
249
+ const files = new Map(
250
+ union.symbols.flatMap((symbol, at) =>
251
+ symbol.kind === "file" && reported.has(symbol.position.path) ? [[at, symbol] as const] : [],
252
+ ),
253
+ );
254
+ const computed =
255
+ files.size === 0
256
+ ? swept.sweep.components
257
+ : deadComponents(swept.matrix, swept.sweep, new Set(files.keys()));
258
+ const place = new Map<string, number>();
259
+ computed.forEach((component, at) => {
260
+ for (const member of component.members) {
261
+ place.set(member, at);
262
+ }
263
+ });
264
+ const leader = computed.map((_component, at) => at);
265
+ const find = (at: number): number => {
266
+ let top = at;
267
+ while ((leader[top] ?? top) !== top) {
268
+ top = leader[top] ?? top;
269
+ }
270
+ return top;
271
+ };
272
+ for (const { fallsWith } of findings) {
273
+ const joined = (fallsWith ?? []).flatMap((id) => place.get(id) ?? []).map(find);
274
+ const first = Math.min(...joined);
275
+ for (const one of joined) {
276
+ leader[one] = first;
277
+ }
278
+ }
279
+ const merged = new Map<number, Component>();
280
+ computed.forEach((component, at) => {
281
+ const top = find(at);
282
+ const into = merged.get(top);
283
+ merged.set(
284
+ top,
285
+ into === undefined
286
+ ? component
287
+ : {
288
+ id: into.id,
289
+ members: [...into.members, ...component.members],
290
+ roots: [...into.roots, ...component.roots],
291
+ deletableLines: into.deletableLines + component.deletableLines,
292
+ held: into.held && component.held,
293
+ },
294
+ );
295
+ });
296
+ return {
297
+ components: [...merged.values()],
298
+ computed: computed.length,
299
+ files: new Map([...files.values()].map((symbol) => [symbol.position.path, symbol.id])),
300
+ };
301
+ }
302
+
199
303
  /**
200
304
  * Every finding of the run completed, in the order given. A dead declaration carries its
201
305
  * candidate's relation, counts, configurations and component; a live one carries no relation and
@@ -217,12 +321,12 @@ export function completed(
217
321
  const candidates = new Map(swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
218
322
  const classOf = reachabilityClasses(input);
219
323
  const ceilingOf = testCeilings(input, classOf);
220
- const componentOf = findingComponents(swept, input.config.reporters.cascade);
221
- const held = new Set(
222
- swept.sweep.components.filter((one) => one.held).flatMap((one) => one.members),
223
- );
224
- let minted = swept.sweep.components.length;
225
- const dead = new Set(swept.sweep.components.map((component) => component.id));
324
+ const { components, computed, files } = runComponents(swept, findings);
325
+ const componentOf = findingComponents(swept, components, input.config.reporters.cascade);
326
+ const held = new Set(components.filter((one) => one.held).flatMap((one) => one.members));
327
+ // A join keeps its first component's identifier, so a minted one counts past every computed one.
328
+ let minted = computed;
329
+ const dead = new Set(components.map((component) => component.id));
226
330
  const fallingWith = new Map<string, number>();
227
331
  for (const { heldByTests } of swept.sweep.candidates) {
228
332
  if (heldByTests !== undefined) {
@@ -239,18 +343,33 @@ export function completed(
239
343
  // a row is still never the declaration. Two declarations of one file can share a
240
344
  // reference, so the one the finding is positioned at is the subject where there is one.
241
345
  const declared = union.at(positionKey(finding.position));
346
+ const ref = finding.symbol.holder ?? finding.symbol.ref;
242
347
  const at = isRowSubject(finding.symbol.kind)
243
348
  ? OUTSIDE
244
- : union.symbols[declared]?.ref === finding.symbol.ref
349
+ : union.symbols[declared]?.ref === ref
245
350
  ? declared
246
- : (positions.get(declarationKey(finding.symbol.ref, finding.position.path)) ?? OUTSIDE);
351
+ : (positions.get(declarationKey(ref, finding.position.path)) ?? OUTSIDE);
247
352
  // A part names the declaration that holds it, and is decided inside it rather than by
248
353
  // the declaration's liveness, so it carries none of the declaration's sweep facts.
249
354
  const part = isPartSubject(finding.symbol.kind);
250
355
  const symbol = union.symbols[at];
251
356
  const judged = !part && union.subject[at] === true ? symbol : undefined;
252
- const candidate = judged === undefined ? undefined : candidates.get(judged.id);
253
- const found = symbol === undefined || part ? undefined : componentOf(symbol.id);
357
+ const candidate =
358
+ judged === undefined || LIVE_SUBJECT.has(finding.code)
359
+ ? undefined
360
+ : candidates.get(judged.id);
361
+ const fallsWith = finding.fallsWith?.[0];
362
+ const joined = fallsWith === undefined ? undefined : componentOf(fallsWith);
363
+ const file =
364
+ finding.code === FILE_NEVER_IMPORTED ? files.get(finding.position.path) : undefined;
365
+ const found =
366
+ joined !== undefined
367
+ ? { ...joined, root: false }
368
+ : file !== undefined
369
+ ? componentOf(file)
370
+ : symbol === undefined || part
371
+ ? undefined
372
+ : componentOf(symbol.id);
254
373
  // A test file's declaration is a component member only as a test of dead code, and
255
374
  // test code keeps the test-support code it references out of every component.
256
375
  if (
@@ -287,9 +406,13 @@ export function completed(
287
406
  ? MANUAL
288
407
  : row.fixability;
289
408
  const lastUses =
290
- fixability === "deletable" && candidate !== undefined
291
- ? input.dependencies.lastUses.get(candidate.id)
292
- : undefined;
409
+ fixability !== "deletable"
410
+ ? undefined
411
+ : candidate !== undefined
412
+ ? input.dependencies.lastUses.get(candidate.id)
413
+ : finding.code === FILE_NEVER_IMPORTED
414
+ ? input.dependencies.fileLastUses.get(finding.position.path)
415
+ : undefined;
293
416
  return {
294
417
  code: finding.code,
295
418
  kind: row.name,
@@ -298,7 +421,7 @@ export function completed(
298
421
  symbol: finding.symbol,
299
422
  reachabilityClass,
300
423
  confidence: capped(
301
- capped(reachabilityClass, row.maxClass),
424
+ capped(capped(reachabilityClass, row.maxClass), rootsCeiling(input, finding, candidate)),
302
425
  judged === undefined ? "certain" : ceilingOf(judged.id),
303
426
  ),
304
427
  ...(candidate === undefined ? {} : { livenessRelation: candidate.relation }),
@@ -1,48 +1,110 @@
1
1
  import { declarationKey, type Dependencies } from "../dependencies.ts";
2
2
  import type { Finding } from "../finding.ts";
3
3
  import { renderRef, type DependencySection } from "../ref.ts";
4
- import type { Emitter } from "./emitter.ts";
4
+ import type { Emitter, EmitterInput } from "./emitter.ts";
5
+ import { nonCodeArtifacts } from "./non-code-artifacts.ts";
5
6
 
6
7
  const UNUSED_DEPENDENCY = "DS1601";
7
8
 
9
+ const FILE_NEVER_IMPORTED = "DS1502";
10
+
8
11
  const MESSAGES: Readonly<Record<DependencySection, string>> = {
9
12
  dependency: "no import in the target needs the declared dependency",
10
13
  "dev-dependency": "no import in the target needs the declared development dependency",
11
14
  "peer-dependency": "no import in the target needs the declared peer dependency",
12
15
  };
13
16
 
17
+ const DEAD_USERS_MESSAGES: Readonly<Record<DependencySection, string>> = {
18
+ dependency: "only code that is itself dead uses the declared dependency",
19
+ "dev-dependency": "only code that is itself dead uses the declared development dependency",
20
+ "peer-dependency": "only code that is itself dead uses the declared peer dependency",
21
+ };
22
+
23
+ /** How the run judged the declarations and files a dependency's uses may sit in. */
24
+ interface DeadCode {
25
+ /** The members of the dead components, and the top level of each file reported unimported. */
26
+ readonly dead: ReadonlySet<string>;
27
+ /** Those found dead under reachability alone. */
28
+ readonly reachedOnly: ReadonlySet<string>;
29
+ /** The declarations of a library's published API, whose findings sit at the consumers' class. */
30
+ readonly published: ReadonlySet<string>;
31
+ }
32
+
33
+ /**
34
+ * The run's dead code: test code keeps the code it references out of every dead component
35
+ * unless that code is dead as well, and a file's top level is dead where the file is.
36
+ */
37
+ function deadCode(input: EmitterInput): DeadCode {
38
+ const { swept } = input;
39
+ const union = swept.matrix.union;
40
+ const files = new Map(
41
+ union.symbols.filter((one) => one.kind === "file").map((one) => [one.position.path, one.id]),
42
+ );
43
+ const dead = new Set(swept.sweep.components.flatMap((component) => component.members));
44
+ const reachedOnly = new Set(
45
+ swept.sweep.candidates.filter((one) => one.relation === "reachability").map((one) => one.id),
46
+ );
47
+ for (const finding of nonCodeArtifacts(input)) {
48
+ const id = files.get(finding.position.path);
49
+ if (finding.code === FILE_NEVER_IMPORTED && id !== undefined) {
50
+ dead.add(id);
51
+ if (finding.unreachableOnly === true) {
52
+ reachedOnly.add(id);
53
+ }
54
+ }
55
+ }
56
+ const published = new Set(
57
+ union.rooted
58
+ .filter((root) => root.kind === "published-api")
59
+ .flatMap((root) => union.symbols[root.at]?.id ?? []),
60
+ );
61
+ return { dead, reachedOnly, published };
62
+ }
63
+
14
64
  /**
15
65
  * A dependency, development dependency or peer dependency of a manifest the run reads
16
- * that the target does not need, at the position of its key.
66
+ * that the target does not need, at the position of its key, and one whose every use is
67
+ * held by dead code, which is dead only through that code and falls with its component:
68
+ * at `possible` where one of its holders is published, and found under reachability alone
69
+ * where one of them is.
17
70
  */
18
- function unusedDependencies(dependencies: Dependencies): Finding[] {
19
- return dependencies.declared
20
- .filter(
21
- (dependency) =>
22
- !dependencies.needed.has(declarationKey(dependency)) &&
23
- !dependencies.ran.has(declarationKey(dependency)),
24
- )
25
- .map((dependency) => ({
26
- code: UNUSED_DEPENDENCY,
27
- position: dependency.position,
28
- symbol: {
29
- ref: renderRef(dependency.manifest, {
30
- of: "dependency",
71
+ function unusedDependencies(input: EmitterInput, dependencies: Dependencies): Finding[] {
72
+ const { dead, reachedOnly, published } = deadCode(input);
73
+ return dependencies.declared.flatMap((dependency) => {
74
+ const key = declarationKey(dependency);
75
+ const held = dependencies.heldBy.get(key);
76
+ const users = held?.every((id) => dead.has(id)) === true ? held : undefined;
77
+ if (dependencies.ran.has(key) || (dependencies.needed.has(key) && users === undefined)) {
78
+ return [];
79
+ }
80
+ const unbuilt = dependencies.unbuilt.has(dependency.position.path);
81
+ const atPossible = unbuilt || users?.some((id) => published.has(id)) === true;
82
+ return [
83
+ {
84
+ code: UNUSED_DEPENDENCY,
85
+ position: dependency.position,
86
+ symbol: {
87
+ ref: renderRef(dependency.manifest, {
88
+ of: "dependency",
89
+ name: dependency.name,
90
+ section: dependency.section,
91
+ }),
92
+ kind: "dependency",
31
93
  name: dependency.name,
32
- section: dependency.section,
33
- }),
34
- kind: "dependency",
35
- name: dependency.name,
36
- sizeLines: 1,
94
+ sizeLines: 1,
95
+ },
96
+ message: (users === undefined ? MESSAGES : DEAD_USERS_MESSAGES)[dependency.section],
97
+ details: { dependencyClass: dependency.section },
98
+ ...(atPossible ? { reachabilityClass: "possible" as const } : {}),
99
+ ...(users?.some((id) => reachedOnly.has(id)) === true
100
+ ? { unreachableOnly: true as const }
101
+ : {}),
102
+ ...(users === undefined ? {} : { fallsWith: users }),
37
103
  },
38
- message: MESSAGES[dependency.section],
39
- details: { dependencyClass: dependency.section },
40
- ...(dependencies.unbuilt.has(dependency.position.path)
41
- ? { reachabilityClass: "possible" as const }
42
- : {}),
43
- }));
104
+ ];
105
+ });
44
106
  }
45
107
 
46
108
  /** The findings of the dependencies-and-module-machinery family, `DS1600` to `DS1699`. */
47
- export const dependenciesAndModuleMachinery: Emitter = ({ dependencies }) =>
48
- unusedDependencies(dependencies);
109
+ export const dependenciesAndModuleMachinery: Emitter = (input) =>
110
+ unusedDependencies(input, input.dependencies);
@@ -165,7 +165,7 @@ function dialledOver(
165
165
  const unanswered = heldKeys(input);
166
166
  const markup = markupKeys(input);
167
167
  const candidates = found.filter((finding) => {
168
- const key = declarationKey(finding.symbol.ref, finding.position.path);
168
+ const key = declarationKey(finding.symbol.holder ?? finding.symbol.ref, finding.position.path);
169
169
  return (
170
170
  !ledger.withheld(finding) &&
171
171
  !unanswered.has(key) &&
@@ -1,5 +1,7 @@
1
+ import type { Confidence } from "../config.ts";
1
2
  import type { Finding } from "../finding.ts";
2
3
  import { KINDS } from "../kinds.ts";
4
+ import { parameterRef } from "../ref.ts";
3
5
  import { UNUSED_PARAMETER, UNUSED_RESULT, type PartFact } from "../intra-function-parts.ts";
4
6
  import { reachedWithTheTests } from "../sweep.ts";
5
7
  import type { Emitter, EmitterInput } from "./emitter.ts";
@@ -12,29 +14,56 @@ function spelled(rules: readonly string[]): string {
12
14
  return `${rules.slice(0, -1).join(", ")} and ${rules.at(-1) ?? ""}`;
13
15
  }
14
16
 
15
- /** The clause a message ends with: the other rules that report the kind, or that none is known. */
16
- function overlapClause(overlap: readonly string[]): string {
17
- return overlap.length === 1 && overlap[0] === "none known"
17
+ /** The compiler's rule of the parameter kind's overlap list, which reports only in a file it checks. */
18
+ const COMPILER_PARAMETER_RULE = "tsc --noUnusedParameters";
19
+
20
+ /**
21
+ * The clause a message ends with: the other rules that report the part, or that none is
22
+ * known. A parameter of a file the compiler does not check is no report of the compiler's.
23
+ */
24
+ function overlapClause(overlap: readonly string[], part: PartFact): string {
25
+ const rules =
26
+ part.parameter?.checked === false
27
+ ? overlap.filter((rule) => rule !== COMPILER_PARAMETER_RULE)
28
+ : overlap;
29
+ return rules.length === 1 && rules[0] === "none known"
18
30
  ? "no other rule is known to report it"
19
- : `also reported by ${spelled(overlap)}`;
31
+ : `also reported by ${spelled(rules)}`;
20
32
  }
21
33
 
34
+ /** What a parameter's finding asks: its deletion, or, where a read parameter follows it, its rename. */
35
+ type ParameterVerdict =
36
+ | { readonly fix: "delete"; readonly confidence: Confidence }
37
+ | { readonly fix: "rename" }
38
+ | undefined;
39
+
22
40
  /**
23
- * Whether one parameter's name is reported: every unread one of a function whose
24
- * signature is free, and of a function used as a value only one no read parameter follows,
25
- * because a caller may pass such a function more arguments than it declares.
41
+ * Whether one unread parameter is reported, and how. Of a free signature it is deleted. A
42
+ * caller may pass a function used as a value more arguments than it declares, so there one
43
+ * a read parameter follows is renamed to an underscore name, which the compilers read as
44
+ * declared unread, and one no read parameter follows is deleted: kept where the call's
45
+ * inference may read its annotation, at `probable` where nothing reads the annotation.
26
46
  */
27
- function parameterReported(
47
+ function parameterVerdict(
28
48
  part: PartFact,
29
49
  free: ReadonlySet<string>,
30
50
  freeButValued: ReadonlySet<string>,
31
- ): boolean {
51
+ ): ParameterVerdict {
32
52
  const place = part.parameter;
33
53
  if (place?.inValue !== true && free.has(part.declaration)) {
34
- return true;
54
+ return place?.annotated === undefined ? { fix: "delete", confidence: "certain" } : undefined;
35
55
  }
36
56
  const valued = place?.inValue === true || freeButValued.has(part.declaration);
37
- return valued && place?.beforeARead !== true;
57
+ if (!valued) {
58
+ return undefined;
59
+ }
60
+ if (place?.beforeARead === true) {
61
+ return { fix: "rename" };
62
+ }
63
+ if (place?.annotated === "steers") {
64
+ return undefined;
65
+ }
66
+ return { fix: "delete", confidence: place?.annotated === "free" ? "probable" : "certain" };
38
67
  }
39
68
 
40
69
  /**
@@ -55,7 +84,7 @@ function reported(
55
84
  const { free, freeButValued, discardedEverywhere } = input.intraFunction;
56
85
  switch (part.code) {
57
86
  case UNUSED_PARAMETER:
58
- return parameterReported(part, free, freeButValued);
87
+ return parameterVerdict(part, free, freeButValued) !== undefined;
59
88
  case UNUSED_RESULT:
60
89
  return (
61
90
  free.has(part.declaration) &&
@@ -119,20 +148,33 @@ export const intraFunction: Emitter = (input) => {
119
148
  if (declaration === undefined || !judged(declaration.id) || !reported(input, part, unknown)) {
120
149
  continue;
121
150
  }
151
+ const verdict =
152
+ part.code === UNUSED_PARAMETER
153
+ ? parameterVerdict(part, input.intraFunction.free, input.intraFunction.freeButValued)
154
+ : undefined;
155
+ const message =
156
+ verdict?.fix === "rename"
157
+ ? `${part.message}, and a read parameter follows it, so rename it to _${part.name}`
158
+ : part.message;
122
159
  found.push({
123
160
  code: part.code,
124
161
  position: part.position,
125
162
  symbol: {
126
- ref: declaration.ref,
163
+ ...(part.code === UNUSED_PARAMETER
164
+ ? { ref: parameterRef(declaration.ref, part.name), holder: declaration.ref }
165
+ : { ref: declaration.ref }),
127
166
  kind: part.kind,
128
167
  name: part.name,
129
168
  sizeLines: part.position.endLine - part.position.line + 1,
130
169
  },
131
- message: `${part.message}; ${overlapClause(overlap)}`,
170
+ message: `${message}; ${overlapClause(overlap, part)}`,
132
171
  details: {
133
172
  ...(part.kind === "store" ? { writePositions: [part.position] } : {}),
134
173
  overlap,
135
174
  },
175
+ ...(verdict?.fix === "delete" && verdict.confidence !== "certain"
176
+ ? { reachabilityClass: verdict.confidence }
177
+ : {}),
136
178
  });
137
179
  }
138
180
  return found;