@cplieger/deadset-ts 6.0.0 → 6.1.0

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 (39) hide show
  1. package/README.md +1 -1
  2. package/dist/src/configured-declarations.js +7 -5
  3. package/dist/src/conformance-record.js +2 -2
  4. package/dist/src/convention-rows.js +67 -1
  5. package/dist/src/conventions.js +51 -9
  6. package/dist/src/edges.js +19 -16
  7. package/dist/src/findings/completion.js +9 -4
  8. package/dist/src/findings/emitters.js +35 -34
  9. package/dist/src/findings/reads-and-writes.js +3 -2
  10. package/dist/src/findings/visibility-narrowing.js +2 -1
  11. package/dist/src/inventory.js +6 -1
  12. package/dist/src/module-declarations.js +3 -3
  13. package/dist/src/outside-tests.js +5 -11
  14. package/dist/src/overrides.js +22 -10
  15. package/dist/src/ref.js +18 -2
  16. package/dist/src/reporters/sarif.js +3 -1
  17. package/dist/src/suppress-file.js +3 -5
  18. package/dist/src/version.js +1 -1
  19. package/dist/src/workspace-programs.js +6 -31
  20. package/jsr.json +1 -1
  21. package/package.json +2 -2
  22. package/src/configured-declarations.ts +7 -5
  23. package/src/conformance-record.ts +2 -2
  24. package/src/convention-rows.ts +84 -1
  25. package/src/conventions.ts +77 -9
  26. package/src/edges.ts +20 -17
  27. package/src/findings/completion.ts +11 -4
  28. package/src/findings/emitters.ts +38 -51
  29. package/src/findings/reads-and-writes.ts +7 -2
  30. package/src/findings/visibility-narrowing.ts +2 -1
  31. package/src/inventory.ts +6 -1
  32. package/src/module-declarations.ts +3 -3
  33. package/src/outside-tests.ts +5 -11
  34. package/src/overrides.ts +26 -11
  35. package/src/ref.ts +27 -4
  36. package/src/reporters/sarif.ts +5 -1
  37. package/src/suppress-file.ts +3 -6
  38. package/src/version.ts +1 -1
  39. package/src/workspace-programs.ts +6 -35
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  deadset-ts finds dead code in TypeScript projects, down to class members, type members and dead stores inside functions, and writes a report your CI can fail on.
6
6
 
7
- It reads your `tsconfig.json` projects, `allowJs` JavaScript and Vue, Svelte and Astro scripts included, with the type information of TypeScript 7.1.0-dev.20261006.1, its one dependency. Your project keeps its own TypeScript version. A type error under that version skips the function or statement that holds it. deadset-ts reports and never edits your code. It is pre-release, tested only on Linux, needs Node.js 24 or later and is licensed under GPL-3.0-or-later.
7
+ It reads your `tsconfig.json` projects, `allowJs` JavaScript and Vue, Svelte and Astro scripts included, with the type information of TypeScript 7.1.0-dev.20261007.1, its one dependency. Your project keeps its own TypeScript version. A type error under that version skips the function or statement that holds it. deadset-ts reports and never edits your code. It is pre-release, tested only on Linux, needs Node.js 24 or later and is licensed under GPL-3.0-or-later.
8
8
 
9
9
  ## Why use it
10
10
 
@@ -143,8 +143,8 @@ function modulesNamed(project, specifiers) {
143
143
  return modules;
144
144
  }
145
145
  /**
146
- * The declaration each entry names in one project. A symbol entry names the
147
- * inventory's declaration whose reference it spells exactly; a module entry names the
146
+ * The declaration each entry names in one project. A symbol entry names every
147
+ * declaration of the inventory whose reference it spells exactly; a module entry names the
148
148
  * declaration its path reaches from the exports of the module its specifier resolves
149
149
  * to; a global entry names the declaration its path reaches from the global scope.
150
150
  * Every alias along a path is followed to the declaration it stands for.
@@ -153,12 +153,14 @@ export function resolveEntries(project, held, entries) {
153
153
  if (entries.length === 0) {
154
154
  return [];
155
155
  }
156
- const byRef = new Map(held.symbols.map((symbol) => [symbol.ref, symbol.id]));
156
+ const byRef = new Map();
157
+ for (const symbol of held.symbols) {
158
+ byRef.set(symbol.ref, [...(byRef.get(symbol.ref) ?? []), symbol.id]);
159
+ }
157
160
  const modules = modulesNamed(project, new Set(entries.flatMap((entry) => (entry.shape === "module" ? [entry.module] : []))));
158
161
  return entries.map((entry) => {
159
162
  if (entry.shape === "symbol") {
160
- const id = byRef.get(entry.symbol);
161
- return { entry, handles: new Set(), ids: new Set(id === undefined ? [] : [id]) };
163
+ return { entry, handles: new Set(), ids: new Set(byRef.get(entry.symbol) ?? []) };
162
164
  }
163
165
  const { components, static: isStatic } = componentsOf(entry.shape === "module" ? entry.name : entry.global);
164
166
  const [first = "", ...rest] = components;
@@ -2,9 +2,9 @@
2
2
  // Regenerate it with `UPDATE_GOLDEN=1 npx vitest --run src/conformance.corpus.test.ts`.
3
3
  export const RECORD = {
4
4
  conformance: {
5
- corpusVersion: "4.0.0",
5
+ corpusVersion: "4.1.0",
6
6
  result: "pass",
7
- digest: "sha256:8250b61deb71467139bf4cbcaf10802cdabb1a2c2261a11a571fc1ff55bfc3dc",
7
+ digest: "sha256:167f770cdff2776fa7ab28b7ff6121c4d7bdb01d83c5ba0057d9303281614008",
8
8
  },
9
9
  gaps: [],
10
10
  };
@@ -31,6 +31,11 @@ const APP_DIRECTORY_ENTRIES = [
31
31
  ];
32
32
  const SOLIDSTART_PLUGIN = { module: "@solidjs/start/config", export: "solidStart" };
33
33
  const NUXT_CONFIG = [`nuxt.config.${ANY_MODULE}`];
34
+ const BABEL_CONFIG = [
35
+ "babel.config.{json,js,cjs,mjs,cts,mts,ts}",
36
+ ".babelrc",
37
+ ".babelrc.{json,js,cjs,mjs,cts}",
38
+ ];
34
39
  const STRYKER_CONFIG = "{stryker,.stryker}.{conf,config}.{json,js,mjs,cjs}";
35
40
  const STRYKER_MODULE = "{stryker,.stryker}.{conf,config}.{js,mjs,cjs}";
36
41
  const VITEST_CONFIG = `{vitest,vite}.config.${ANY_MODULE}`;
@@ -57,6 +62,19 @@ export const CONVENTION_ROWS = [
57
62
  },
58
63
  ],
59
64
  },
65
+ {
66
+ name: "babel",
67
+ package: "@babel/core",
68
+ range: ">=7.0.0 <9.0.0",
69
+ entries: [],
70
+ moves: [],
71
+ helpers: ["@babel/plugin-transform-runtime", "@babel/transform-runtime"].map((value) => ({
72
+ property: "plugins",
73
+ files: BABEL_CONFIG,
74
+ value,
75
+ package: "@babel/runtime",
76
+ })),
77
+ },
60
78
  {
61
79
  name: "eslint",
62
80
  package: "eslint",
@@ -229,6 +247,21 @@ export const CONVENTION_ROWS = [
229
247
  },
230
248
  ],
231
249
  },
250
+ {
251
+ name: "rolldown",
252
+ package: "rolldown",
253
+ range: ">=1.0.0 <2.0.0",
254
+ entries: [],
255
+ moves: [],
256
+ helpers: [
257
+ {
258
+ property: "transform.helpers.mode",
259
+ files: [`rolldown.config.${ANY_MODULE}`],
260
+ value: "Runtime",
261
+ package: "@oxc-project/runtime",
262
+ },
263
+ ],
264
+ },
232
265
  {
233
266
  name: "solidstart",
234
267
  package: "@solidjs/start",
@@ -275,7 +308,10 @@ export const CONVENTION_ROWS = [
275
308
  range: ">=7.0.0 <11.0.0",
276
309
  entries: [STRYKER_MODULE],
277
310
  moves: [],
278
- selections: [{ property: "mutate", files: [STRYKER_CONFIG] }],
311
+ selections: [
312
+ { property: "mutate", files: [STRYKER_CONFIG] },
313
+ { property: "ignorePatterns", files: [STRYKER_CONFIG] },
314
+ ],
279
315
  shortNames: [
280
316
  {
281
317
  property: "testRunner",
@@ -406,6 +442,36 @@ export const CONVENTION_ROWS = [
406
442
  },
407
443
  ],
408
444
  },
445
+ {
446
+ name: "swc",
447
+ package: "@swc/core",
448
+ range: ">=1.0.0 <2.0.0",
449
+ entries: [],
450
+ moves: [],
451
+ helpers: [
452
+ {
453
+ property: "jsc.externalHelpers",
454
+ files: [".swcrc"],
455
+ value: true,
456
+ package: "@swc/helpers",
457
+ },
458
+ ],
459
+ },
460
+ {
461
+ name: "vite",
462
+ package: "vite",
463
+ range: ">=8.0.0 <9.0.0",
464
+ entries: [],
465
+ moves: [],
466
+ helpers: [
467
+ {
468
+ property: "oxc.helpers.mode",
469
+ files: [VITE_CONFIG],
470
+ value: "Runtime",
471
+ package: "@oxc-project/runtime",
472
+ },
473
+ ],
474
+ },
409
475
  {
410
476
  name: "vitest",
411
477
  package: "vitest",
@@ -13,6 +13,8 @@ import { globExpression } from "./glob.js";
13
13
  import { dirnamePath, joinPath, normalizePath, relativePath, resolvePath } from "./paths.js";
14
14
  import { satisfies } from "./version-range.js";
15
15
  const JSON_EXTENSION = ".json";
16
+ /** A configuration file with no extension that its tool reads as a JSON document. */
17
+ const JSON_RC = /^\.[\w-]+rc$/u;
16
18
  /** The sections an installed package is required by, so its absence fails the run. */
17
19
  const REQUIRED_SECTIONS = ["dependencies", "devDependencies"];
18
20
  const DEPENDENCY_SECTIONS = [
@@ -351,11 +353,10 @@ function readPath(object, keys, values, elements) {
351
353
  return undefined;
352
354
  }
353
355
  /**
354
- * Every string literal one file writes at one property's path inside the objects it is
355
- * read from, as {@link readProperty} reads them: the value itself, or each element of an
356
- * array value, whatever else the path holds.
356
+ * Every value one file writes at one property's path inside the objects it is read from,
357
+ * as {@link readProperty} reads them, whatever else the path holds.
357
358
  */
358
- function stringsAt(file, property, call) {
359
+ function valuesAt(file, property, call) {
359
360
  const declared = declaredValues(file);
360
361
  const objects = call === undefined
361
362
  ? defaultExports(file).map((one) => objectOf(one, declared, true))
@@ -374,12 +375,9 @@ function stringsAt(file, property, call) {
374
375
  if (rest.length > 0) {
375
376
  visit(held, rest);
376
377
  }
377
- else if (literalText(held) !== undefined) {
378
+ else {
378
379
  found.push(held);
379
380
  }
380
- else if (isArrayLiteralExpression(held)) {
381
- found.push(...held.elements.map(unwrapped).filter((one) => literalText(one) !== undefined));
382
- }
383
381
  }
384
382
  };
385
383
  for (const object of objects) {
@@ -389,6 +387,32 @@ function stringsAt(file, property, call) {
389
387
  }
390
388
  return found;
391
389
  }
390
+ /** The string literals among values: each value itself, or each element of an array value. */
391
+ function stringsAt(file, property, call) {
392
+ return valuesAt(file, property, call).flatMap((held) => {
393
+ if (literalText(held) !== undefined) {
394
+ return [held];
395
+ }
396
+ return isArrayLiteralExpression(held)
397
+ ? held.elements.map(unwrapped).filter((one) => literalText(one) !== undefined)
398
+ : [];
399
+ });
400
+ }
401
+ /** Whether one value holds a helper setting's value, as {@link HelperReading} states it. */
402
+ function holdsValue(held, value) {
403
+ if (value === true) {
404
+ return held.kind === SyntaxKind.TrueKeyword;
405
+ }
406
+ if (literalText(held) === value) {
407
+ return true;
408
+ }
409
+ return (isArrayLiteralExpression(held) &&
410
+ held.elements.some((element) => {
411
+ const one = unwrapped(element);
412
+ const first = isArrayLiteralExpression(one) ? one.elements[0] : one;
413
+ return first !== undefined && literalText(first) === value;
414
+ }));
415
+ }
392
416
  /** The strings an applied row's selection keys hold, added per configuration file to `into`. */
393
417
  function selectedStrings(host, parse, dir, readings, into) {
394
418
  for (const reading of readings) {
@@ -527,7 +551,11 @@ function readingFiles(host, parse, dir, files) {
527
551
  catch {
528
552
  continue;
529
553
  }
530
- found.push({ name, text, file: parse(path, text) });
554
+ found.push({
555
+ name,
556
+ text,
557
+ file: parse(JSON_RC.test(name) ? `${path}${JSON_EXTENSION}` : path, text),
558
+ });
531
559
  }
532
560
  }
533
561
  return found;
@@ -563,6 +591,17 @@ function shortNameUses(host, parse, dir, manifest, readings) {
563
591
  }
564
592
  return [...used].filter((name) => declared.has(name)).sort(compare);
565
593
  }
594
+ /** The declared dependencies an applied row's helper settings use. */
595
+ function helperUses(host, parse, dir, manifest, readings) {
596
+ const declared = new Set(DEPENDENCY_SECTIONS.flatMap((section) => {
597
+ const held = isRecord(manifest) ? manifest[section] : undefined;
598
+ return isRecord(held) ? Object.keys(held) : [];
599
+ }));
600
+ return readings
601
+ .filter((reading) => declared.has(reading.package) &&
602
+ readingFiles(host, parse, dir, reading.files).some(({ file }) => valuesAt(file, reading.property, reading.call).some((held) => holdsValue(held, reading.value))))
603
+ .map((reading) => reading.package);
604
+ }
566
605
  /** Two strings ordered bytewise. */
567
606
  function compare(a, b) {
568
607
  if (a === b) {
@@ -644,6 +683,9 @@ export function readConventions(host, parse, manifests, disabled, rows = CONVENT
644
683
  for (const name of manifestKeyUses(declared, row.manifestKeys ?? [])) {
645
684
  uses.add(name);
646
685
  }
686
+ for (const name of helperUses(host, parse, manifest.dir, declared, row.helpers ?? [])) {
687
+ uses.add(name);
688
+ }
647
689
  selectedStrings(host, parse, manifest.dir, row.selections ?? [], selected);
648
690
  const excludes = [
649
691
  ...new Set((row.excludes ?? []).flatMap((one) => expand(one, resolved))),
package/dist/src/edges.js CHANGED
@@ -154,9 +154,10 @@ function compare(a, b) {
154
154
  }
155
155
  /**
156
156
  * Evaluates every declared side of this language over the run's findings. A side naming a
157
- * symbol the run enumerates nothing under is `absent`; one naming a symbol the run holds a
158
- * finding about is `dead` and carries that finding, pending until a merge reads the other
159
- * side and reported nowhere else, a narrowing finding included; any other is `live`.
157
+ * symbol the run enumerates nothing under is `absent`; otherwise it is evaluated once per
158
+ * declaration its reference spells. A declaration the run holds a finding about is `dead`
159
+ * and carries that finding, pending until a merge reads the other side and reported
160
+ * nowhere else, a narrowing finding included; any other is `live`.
160
161
  */
161
162
  export function evaluateEdges(findings, sides, symbols) {
162
163
  if (sides.length === 0) {
@@ -164,8 +165,8 @@ export function evaluateEdges(findings, sides, symbols) {
164
165
  }
165
166
  const byRef = new Map();
166
167
  for (const symbol of symbols) {
167
- if (symbol.kind !== "file" && !byRef.has(symbol.ref)) {
168
- byRef.set(symbol.ref, symbol.id);
168
+ if (symbol.kind !== "file") {
169
+ byRef.set(symbol.ref, [...(byRef.get(symbol.ref) ?? []), symbol.id]);
169
170
  }
170
171
  }
171
172
  const held = new Map();
@@ -175,18 +176,20 @@ export function evaluateEdges(findings, sides, symbols) {
175
176
  }
176
177
  });
177
178
  const pending = new Set();
178
- const evaluations = sides.map((side) => {
179
- const id = byRef.get(side.symbol);
180
- if (id === undefined) {
181
- return { ...side, state: "absent" };
179
+ const evaluations = sides.flatMap((side) => {
180
+ const ids = byRef.get(side.symbol);
181
+ if (ids === undefined) {
182
+ return [{ ...side, state: "absent" }];
182
183
  }
183
- const at = held.get(id);
184
- const finding = at === undefined ? undefined : findings[at];
185
- if (at === undefined || finding === undefined) {
186
- return { ...side, state: "live" };
187
- }
188
- pending.add(at);
189
- return { ...side, state: "dead", finding };
184
+ return ids.map((id) => {
185
+ const at = held.get(id);
186
+ const finding = at === undefined ? undefined : findings[at];
187
+ if (at === undefined || finding === undefined) {
188
+ return { ...side, state: "live" };
189
+ }
190
+ pending.add(at);
191
+ return { ...side, state: "dead", finding };
192
+ });
190
193
  });
191
194
  evaluations.sort((a, b) => compare(a.edge, b.edge) || compare(a.side, b.side));
192
195
  return { findings: findings.filter((_finding, at) => !pending.has(at)), evaluations };
@@ -6,7 +6,8 @@
6
6
  import { componentId } from "../components.js";
7
7
  import { OUTSIDE } from "../graph.js";
8
8
  import { KINDS } from "../kinds.js";
9
- import { byPosition } from "../position.js";
9
+ import { byPosition, positionKey } from "../position.js";
10
+ import { declarationKey } from "../ref.js";
10
11
  import { FAMILY_KEY_LENGTH } from "../resolve.js";
11
12
  import { isPartSubject, isRowSubject } from "../suppress.js";
12
13
  /** The classes ranked by the claim each makes, the strongest highest. */
@@ -117,7 +118,7 @@ function findingComponents(swept, cascade) {
117
118
  export function completed(input, findings, kinds = KINDS) {
118
119
  const { swept } = input;
119
120
  const union = swept.matrix.union;
120
- const positions = new Map(union.symbols.map((symbol, at) => [symbol.ref, at]));
121
+ const positions = new Map(union.symbols.map((symbol, at) => [declarationKey(symbol.ref, symbol.position.path), at]));
121
122
  const candidates = new Map(swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
122
123
  const classOf = reachabilityClasses(input);
123
124
  const componentOf = findingComponents(swept, input.config.reporters.cascade);
@@ -135,10 +136,14 @@ export function completed(input, findings, kinds = KINDS) {
135
136
  throw new Error(`${finding.code} names no live row of the issue-kind vocabulary`);
136
137
  }
137
138
  // A row's reference may spell a declaration's, as an ignore entry naming one does, and
138
- // a row is still never the declaration.
139
+ // a row is still never the declaration. Two declarations of one file can share a
140
+ // reference, so the one the finding is positioned at is the subject where there is one.
141
+ const declared = union.at(positionKey(finding.position));
139
142
  const at = isRowSubject(finding.symbol.kind)
140
143
  ? OUTSIDE
141
- : (positions.get(finding.symbol.ref) ?? OUTSIDE);
144
+ : union.symbols[declared]?.ref === finding.symbol.ref
145
+ ? declared
146
+ : (positions.get(declarationKey(finding.symbol.ref, finding.position.path)) ?? OUTSIDE);
142
147
  // A part names the declaration that holds it, and is decided inside it rather than by
143
148
  // the declaration's liveness, so it carries none of the declaration's sweep facts.
144
149
  const part = isPartSubject(finding.symbol.kind);
@@ -1,5 +1,6 @@
1
1
  import { evaluateEdges } from "../edges.js";
2
2
  import { liveFilesAt } from "../matrix.js";
3
+ import { declarationKey } from "../ref.js";
3
4
  import { ledgerOf } from "../suppress.js";
4
5
  import { insideSkipped } from "../type-errors.js";
5
6
  import { completed, dialsOf, reportable } from "./completion.js";
@@ -23,24 +24,24 @@ export const EMITTERS = new Map([
23
24
  ["intra-function", intraFunction],
24
25
  ]);
25
26
  /**
26
- * The references of every declaration an unanswered question could have kept live, which
27
- * the run reports nothing about: a finding about one would be computed from the gap.
27
+ * The keys of every declaration an unanswered question could have kept live, which the run
28
+ * reports nothing about: a finding about one would be computed from the gap.
28
29
  */
29
- function heldRefs(input) {
30
+ function heldKeys(input) {
30
31
  const held = new Set(input.swept.heldByUnanswered);
31
32
  if (held.size === 0) {
32
33
  return held;
33
34
  }
34
35
  return new Set(input.swept.matrix.union.symbols
35
36
  .filter((symbol) => held.has(symbol.id))
36
- .map((symbol) => symbol.ref));
37
+ .map((symbol) => declarationKey(symbol.ref, symbol.position.path)));
37
38
  }
38
39
  /**
39
- * The references of every top-level declaration of a live component file. The file's
40
+ * The keys of every top-level declaration of a live component file. The file's
40
41
  * markup may use each of them, which no analysis of the markup can rule out, so the run
41
42
  * reports nothing about one while the file is live.
42
43
  */
43
- function markupRefs(input) {
44
+ function markupKeys(input) {
44
45
  const { swept } = input;
45
46
  if (swept.componentFiles.length === 0) {
46
47
  return new Set();
@@ -48,7 +49,7 @@ function markupRefs(input) {
48
49
  const live = new Set(liveFilesAt(swept.matrix, swept.sweep, new Set(swept.componentFiles)).map((symbol) => symbol.id));
49
50
  return new Set(swept.matrix.union.symbols
50
51
  .filter((symbol) => live.has(symbol.parent))
51
- .map((symbol) => symbol.ref));
52
+ .map((symbol) => declarationKey(symbol.ref, symbol.position.path)));
52
53
  }
53
54
  /** Every family's findings over one sweep, and those of any further emitter, completed by one step. */
54
55
  function completedOver(input, ...more) {
@@ -72,21 +73,13 @@ function dialsOver(input, found, suppressed, unmarked) {
72
73
  * family's findings completed by one step, so every component number is minted once, the
73
74
  * finding each record claims withheld, the rest withheld by the dials, so a root one family
74
75
  * reports withholds what another reports in its component, and every declared edge
75
- * evaluated over what remains, so a pending finding is in its evaluation alone. What the
76
- * minimum confidence withheld is what the same decision at the lowest minimum adds.
76
+ * evaluated over what remains, so a pending finding is in its evaluation alone.
77
77
  */
78
78
  export function decidedFindings(input, suppressed = { unmarked: input, records: [] }) {
79
79
  const found = completedOver(input);
80
80
  const unmarked = suppressed.records.length === 0 ? [] : completedOver(suppressed.unmarked, fallenMembers);
81
81
  const would = [...unmarked, ...found];
82
- const decided = dialledOver(input, suppressed, found, unmarked);
83
- const shown = input.config.analysis.minConfidence === "possible"
84
- ? decided
85
- : dialledOver(atPossible(input), {
86
- unmarked: atPossible(suppressed.unmarked),
87
- records: suppressed.records,
88
- }, found, unmarked);
89
- return { ...decided, would, withheld: withheldOf(decided.findings, shown.findings) };
82
+ return { ...dialledOver(input, suppressed, found, unmarked), would };
90
83
  }
91
84
  /** The run's input with the minimum confidence at its lowest, which withholds nothing. */
92
85
  function atPossible(input) {
@@ -104,31 +97,39 @@ function atPossible(input) {
104
97
  function dialledOver(input, suppressed, found, unmarked) {
105
98
  const would = [...unmarked, ...found];
106
99
  const ledger = ledgerOf(suppressed.records, would, dialsOver(input, found, suppressed, unmarked));
107
- const unanswered = heldRefs(input);
108
- const markup = markupRefs(input);
109
- const kept = reportable(input, found.filter((finding) => !ledger.withheld(finding) &&
110
- !unanswered.has(finding.symbol.ref) &&
111
- !markup.has(finding.symbol.ref) &&
112
- !insideSkipped(input.swept.skipped, finding.position)));
100
+ const unanswered = heldKeys(input);
101
+ const markup = markupKeys(input);
102
+ const candidates = found.filter((finding) => {
103
+ const key = declarationKey(finding.symbol.ref, finding.position.path);
104
+ return (!ledger.withheld(finding) &&
105
+ !unanswered.has(key) &&
106
+ !markup.has(key) &&
107
+ !insideSkipped(input.swept.skipped, finding.position));
108
+ });
109
+ const kept = reportable(input, candidates);
113
110
  const evaluated = evaluateEdges(kept, input.boundary.edges, input.swept.matrix.union.symbols);
114
- return { findings: evaluated.findings, evaluations: evaluated.evaluations, ledger };
111
+ return {
112
+ findings: evaluated.findings,
113
+ evaluations: evaluated.evaluations,
114
+ ledger,
115
+ withheld: withheldOf(input, candidates),
116
+ };
115
117
  }
116
- /** The findings the lowest minimum would report that the run does not, counted by confidence. */
117
- function withheldOf(reported, shown) {
118
- const kept = new Set(reported.map(findingKey));
118
+ /**
119
+ * The findings the dials withhold only because their confidence is below the minimum, each
120
+ * counted at the confidence this run gives it: the dials at the lowest minimum keep them.
121
+ */
122
+ function withheldOf(input, candidates) {
123
+ const dials = dialsOf(input, candidates);
124
+ const lowest = dialsOf(atPossible(input), candidates);
119
125
  const counts = { certain: 0, probable: 0, possible: 0 };
120
- for (const finding of shown) {
121
- if (!kept.has(findingKey(finding))) {
126
+ for (const finding of candidates) {
127
+ if (dials.withholds(finding) && !lowest.withholds(finding)) {
122
128
  counts[finding.confidence] += 1;
123
129
  }
124
130
  }
125
131
  return counts;
126
132
  }
127
- /** A finding's identity across two decisions of one run: its position, code and subject. */
128
- function findingKey(finding) {
129
- const { path, line, column } = finding.position;
130
- return JSON.stringify([path, line, column, finding.code, finding.symbol.ref]);
131
- }
132
133
  /** Every finding the run reports, each family's in table order. */
133
134
  export function findingsOf(input) {
134
135
  return decidedFindings(input).findings;
@@ -1,4 +1,5 @@
1
1
  import { positionKey } from "../position.js";
2
+ import { declarationKey } from "../ref.js";
2
3
  /** The containers whose type parameters `DS1303` reports: a function and a class method. */
3
4
  const SIGNATURES = new Set(["function", "method"]);
4
5
  function indexed(input) {
@@ -175,12 +176,12 @@ function unusedTypeParameters(run) {
175
176
  .map((symbol) => findingAbout(symbol, "DS1303", `type parameter ${symbol.name} is named in neither its signature nor its body`));
176
177
  }
177
178
  /**
178
- * The references of the declarations the write-only kind reports, whatever the
179
+ * The keys of the declarations the write-only kind reports, whatever the
179
180
  * severity: a narrowing kind yields each to it, since deleting the declaration
180
181
  * supersedes narrowing it.
181
182
  */
182
183
  export function writeOnlyDeclarations(input) {
183
- return new Set(writeOnlySymbols(indexed(input)).map((finding) => finding.symbol.ref));
184
+ return new Set(writeOnlySymbols(indexed(input)).map((finding) => declarationKey(finding.symbol.ref, finding.position.path)));
184
185
  }
185
186
  /**
186
187
  * The findings of the reads-and-writes family, `DS1300` to `DS1399`, in code order and
@@ -1,4 +1,5 @@
1
1
  import { OUTSIDE } from "../graph.js";
2
+ import { declarationKey } from "../ref.js";
2
3
  import { writeOnlyDeclarations } from "./reads-and-writes.js";
3
4
  const UNNECESSARY_EXPORT = "DS1101";
4
5
  const UNREACHABLE_EXPORT = "DS1103";
@@ -119,7 +120,7 @@ function narrowing(facts, at, symbol, world) {
119
120
  const { closed } = world;
120
121
  const file = facts.fileOf[at] ?? OUTSIDE;
121
122
  if (!topLevelExport(facts, at, symbol) ||
122
- world.writeOnly.has(symbol.ref) ||
123
+ world.writeOnly.has(declarationKey(symbol.ref, symbol.position.path)) ||
123
124
  symbol.kind === "export-alias" ||
124
125
  entersByName(facts.rootedBy[at]) ||
125
126
  entersByName(facts.rootedBy[file]) ||
@@ -787,7 +787,12 @@ function refOf(record, chain) {
787
787
  return renderRef(record.module, { of: "module" });
788
788
  }
789
789
  if (record.kind === "export-alias") {
790
- return renderRef(record.module, { of: "alias", name: record.component.text });
790
+ const block = record.parentChain.at(-1);
791
+ return renderRef(record.module, {
792
+ of: "alias",
793
+ name: record.component.text,
794
+ ...(block === undefined ? {} : { block: block.text }),
795
+ });
791
796
  }
792
797
  return renderRef(record.module, {
793
798
  of: "declaration",
@@ -9,7 +9,7 @@ import { isCallExpression, isExportDeclaration, isExternalModuleReference, isImp
9
9
  import { nodeKey } from "./inventory.js";
10
10
  import { renderPosition } from "./position.js";
11
11
  import { UNANSWERED } from "./query.js";
12
- import { nameComponent } from "./ref.js";
12
+ import { aliasFragment, nameComponent } from "./ref.js";
13
13
  /** Whether a node is a specifier's text: a string literal or a template with no substitution. */
14
14
  function literalText(node) {
15
15
  return node !== undefined && (isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node));
@@ -78,7 +78,7 @@ function namesModule(specifier, name) {
78
78
  * The module declarations of an inventory a specifier names by name or pattern, each with
79
79
  * the members that hold the export names given.
80
80
  */
81
- export function namedBy(held) {
81
+ function namedBy(held) {
82
82
  if (held.modules.size === 0) {
83
83
  return () => [];
84
84
  }
@@ -103,7 +103,7 @@ export function namedBy(held) {
103
103
  const scope = ref.slice(0, ref.indexOf("#"));
104
104
  const wanted = new Set(names.flatMap((one) => [
105
105
  `${ref}.${nameComponent(one)}`,
106
- `${scope}#${nameComponent(one)}:alias`,
106
+ `${scope}#${aliasFragment(one, name)}`,
107
107
  ]));
108
108
  const members = (children.get(id) ?? []).filter((child) => wanted.has(child.ref));
109
109
  return [{ id, members: members.map((child) => child.id) }];
@@ -1,15 +1,15 @@
1
1
  /**
2
2
  * The test files no compiler configuration of a run holds. The runner executes them all
3
3
  * the same, so each module one names is a test reference: the own file a relative
4
- * specifier names, and the declarations that file exports under the names read from it,
5
- * or a module declaration the specifier names by name or pattern and its members of
6
- * those names. Such a file is read without a program, so what its body references
7
- * beyond the modules it names is not known.
4
+ * specifier names, and the declarations that file exports under the names read from it.
5
+ * No compiler run reads a module declaration for such a file, so its imports name none.
6
+ * Such a file is read without a program, so what its body references beyond the modules
7
+ * it names is not known.
8
8
  */
9
9
  import { isCallExpression, isExportDeclaration, isImportDeclaration, isNamespaceExport, isNamespaceImport, isNoSubstitutionTemplateLiteral, isStringLiteral, SyntaxKind, } from "@typescript/native/unstable/ast";
10
10
  import { RESOLVED_EXTENSIONS } from "./configuration-files.js";
11
11
  import { emittedFrom } from "./emit-map.js";
12
- import { namedBy, namesImported } from "./module-declarations.js";
12
+ import { namesImported } from "./module-declarations.js";
13
13
  import { dirnamePath, joinPath, normalizePath, queriedModulePath } from "./paths.js";
14
14
  import { renderPosition } from "./position.js";
15
15
  import { nameComponent } from "./ref.js";
@@ -102,7 +102,6 @@ export function outsideTestReferences(tests, held, targetRoot) {
102
102
  exported.set(key, [...(exported.get(key) ?? []), symbol.id]);
103
103
  }
104
104
  }
105
- const declared = namedBy(held);
106
105
  const found = [];
107
106
  for (const { path, file } of tests) {
108
107
  const from = `${path}#`;
@@ -112,11 +111,6 @@ export function outsideTestReferences(tests, held, targetRoot) {
112
111
  const read = (to, use) => {
113
112
  found.push({ from, to, position, use, resolution: "syntax", test: true, unheld: true });
114
113
  };
115
- for (const declaration of declared(named.text, named.names)) {
116
- for (const to of [declaration.id, ...declaration.members]) {
117
- read(to, "read");
118
- }
119
- }
120
114
  const written = named.text.includes("?") ? queriedModulePath(named.text) : named.text;
121
115
  if (written === undefined) {
122
116
  continue;