@cplieger/deadset-ts 6.0.0 → 6.0.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.
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
 
@@ -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) {
@@ -104,12 +105,15 @@ function atPossible(input) {
104
105
  function dialledOver(input, suppressed, found, unmarked) {
105
106
  const would = [...unmarked, ...found];
106
107
  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)));
108
+ const unanswered = heldKeys(input);
109
+ const markup = markupKeys(input);
110
+ const kept = reportable(input, found.filter((finding) => {
111
+ const key = declarationKey(finding.symbol.ref, finding.position.path);
112
+ return (!ledger.withheld(finding) &&
113
+ !unanswered.has(key) &&
114
+ !markup.has(key) &&
115
+ !insideSkipped(input.swept.skipped, finding.position));
116
+ }));
113
117
  const evaluated = evaluateEdges(kept, input.boundary.edges, input.swept.matrix.union.symbols);
114
118
  return { findings: evaluated.findings, evaluations: evaluated.evaluations, ledger };
115
119
  }
@@ -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]) ||
package/dist/src/ref.js CHANGED
@@ -107,6 +107,14 @@ export function renderRef(module, fragment) {
107
107
  }
108
108
  }
109
109
  }
110
+ /**
111
+ * One declaration's key among a run's: its reference and the file that declares it. Two
112
+ * packages of one name spell one reference for two declarations, so the reference alone
113
+ * keys neither.
114
+ */
115
+ export function declarationKey(ref, path) {
116
+ return `${ref}\u0000${path}`;
117
+ }
110
118
  /**
111
119
  * Whether one string is a reference of this language's grammar, in its canonical
112
120
  * spelling. A string that is not, whether misspelled or spelled in a form the
@@ -1,5 +1,6 @@
1
1
  import { jsonChunks } from "../json-chunks.js";
2
2
  import { KINDS } from "../kinds.js";
3
+ import { declarationKey } from "../ref.js";
3
4
  import { lineHashes, symbolFingerprint } from "./fingerprint.js";
4
5
  import { RenderError } from "./reporter.js";
5
6
  import { utf8Bytes } from "./sha256.js";
@@ -122,7 +123,8 @@ function relatedOf(found) {
122
123
  related.push({ label: "write", at, endLine: at.end_line });
123
124
  }
124
125
  for (const one of found.component.members ?? []) {
125
- if (one.ref !== found.symbol.ref) {
126
+ if (declarationKey(one.ref, one.position.path) !==
127
+ declarationKey(found.symbol.ref, found.position.path)) {
126
128
  related.push({ label: "member", at: one.position, endLine: one.position.end_line });
127
129
  }
128
130
  }
@@ -7,6 +7,7 @@
7
7
  import { jsonChunks } from "./json-chunks.js";
8
8
  import { joinPath } from "./paths.js";
9
9
  import { positionAt, walkDocument } from "./json-document.js";
10
+ import { declarationKey } from "./ref.js";
10
11
  import { CODE_FORM, SUPPRESSION_WITHOUT_REASON, SuppressionError, UNSCOPED_ENTRY, blankReason, isEntryPath, isSymbolRef, siteText, } from "./suppress.js";
11
12
  /** The name and the location the grammar fixes for the ignore file. */
12
13
  export const IGNORE_FILE = "deadset-ignore.json";
@@ -38,9 +39,6 @@ function documentSite(shape) {
38
39
  function isObject(value) {
39
40
  return typeof value === "object" && value !== null && !Array.isArray(value);
40
41
  }
41
- function referenceKey(ref, path) {
42
- return `${ref}\u0000${path}`;
43
- }
44
42
  /**
45
43
  * Reads one entry or row: the records it binds, the refusals it carries, or the
46
44
  * malformed value that ends the run.
@@ -100,7 +98,7 @@ function readRecord(value, site, byReference, shape) {
100
98
  if (refusals.length > 0) {
101
99
  return { records: [], refusals };
102
100
  }
103
- const named = byReference.get(referenceKey(symbol, written.path)) ?? [];
101
+ const named = byReference.get(declarationKey(symbol, written.path)) ?? [];
104
102
  if (named.length === 0) {
105
103
  return { records: [{ ...written, bound: "" }], refusals: [] };
106
104
  }
@@ -109,7 +107,7 @@ function readRecord(value, site, byReference, shape) {
109
107
  function byReferenceOf(symbols) {
110
108
  const byReference = new Map();
111
109
  for (const symbol of symbols) {
112
- const key = referenceKey(symbol.ref, symbol.position.path);
110
+ const key = declarationKey(symbol.ref, symbol.position.path);
113
111
  byReference.set(key, [...(byReference.get(key) ?? []), symbol]);
114
112
  }
115
113
  return byReference;
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { ModuleKind, ModuleResolutionKind } from "@typescript/native/unstable/sync";
9
9
  import { isStringLiteralLikeNode } from "@typescript/native/unstable/ast";
10
- import { dirnamePath, joinPath, resolvePath } from "./paths.js";
10
+ import { dirnamePath, resolvePath } from "./paths.js";
11
11
  /** The most questions one round trip carries. */
12
12
  const BATCH = 2048;
13
13
  /**
@@ -20,31 +20,6 @@ function analysisOptions(parsed) {
20
20
  const { composite, rootDir: _rootDir, outDir: _outDir, declarationDir: _declarationDir, tsBuildInfoFile: _tsBuildInfoFile, ...kept } = parsed.options;
21
21
  return composite === true ? { ...kept, declaration: true } : kept;
22
22
  }
23
- /**
24
- * The references a built program can carry: only where every referenced
25
- * configuration reads and is composite with output, because the compiler reports a
26
- * broken reference at syntax a built program does not have.
27
- */
28
- function carriedReferences(engine, parsed) {
29
- const references = parsed.projectReferences ?? [];
30
- for (const reference of references) {
31
- // A reference that names a directory names the `tsconfig.json` in it.
32
- const configFile = reference.path.endsWith(".json")
33
- ? reference.path
34
- : joinPath(reference.path, "tsconfig.json");
35
- let options;
36
- try {
37
- options = engine.parseConfigFile(configFile).options;
38
- }
39
- catch {
40
- return [];
41
- }
42
- if (options.composite !== true || options.noEmit === true) {
43
- return [];
44
- }
45
- }
46
- return references;
47
- }
48
23
  /** The conditions an `exports` lookup under one configuration and one resolution mode reads. */
49
24
  function conditionsOf(parsed, mode) {
50
25
  const resolution = parsed.options.moduleResolution;
@@ -94,7 +69,7 @@ function configurationOf(engine, host, reading) {
94
69
  if (reason !== undefined) {
95
70
  return { kind: "unread", reason };
96
71
  }
97
- const { options } = reading.parsed;
72
+ const { options, projectReferences: references = [] } = reading.parsed;
98
73
  const raw = options["pathsBasePath"];
99
74
  const base = typeof raw === "string" ? raw : dirnamePath(reading.configFile);
100
75
  const paths = {};
@@ -117,8 +92,8 @@ function configurationOf(engine, host, reading) {
117
92
  ...(options.composite === true ? { composite: false, declaration: true } : {}),
118
93
  paths,
119
94
  },
120
- ...(reading.references.length > 0
121
- ? { references: reading.references.map((reference) => ({ path: reference.path })) }
95
+ ...(references.length > 0
96
+ ? { references: references.map((reference) => ({ path: reference.path })) }
122
97
  : {}),
123
98
  ...(exclude === undefined ? {} : { exclude }),
124
99
  });
@@ -150,6 +125,7 @@ function configurationOf(engine, host, reading) {
150
125
  /** The program one configuration is built as, under the answers it holds so far. */
151
126
  function programOf(engine, reading) {
152
127
  const compilerOptions = analysisOptions(reading.parsed);
128
+ const references = reading.parsed.projectReferences ?? [];
153
129
  const resolver = engine.createModuleResolver(compilerOptions, reading.entries);
154
130
  return {
155
131
  resolver,
@@ -159,7 +135,7 @@ function programOf(engine, reading) {
159
135
  options: {
160
136
  moduleResolver: resolver,
161
137
  configFileParsingDiagnostics: reading.parsed.errors,
162
- ...(reading.references.length > 0 ? { projectReferences: reading.references } : {}),
138
+ ...(references.length > 0 ? { projectReferences: references } : {}),
163
139
  },
164
140
  },
165
141
  };
@@ -292,7 +268,6 @@ export function workspacePrograms(engine, host, configFiles, resolver) {
292
268
  {
293
269
  configFile,
294
270
  parsed,
295
- references: carriedReferences(engine, parsed),
296
271
  entries: [],
297
272
  seen: new Set(),
298
273
  bound: new Map(),
package/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "6.0.0",
3
+ "version": "6.0.1",
4
4
  "license": "GPL-3.0-or-later",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "6.0.0",
3
+ "version": "6.0.1",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Deterministic dead-code analysis for TypeScript, class and type members included",
@@ -29,7 +29,7 @@
29
29
  "lint:fix": "eslint . --fix && prettier --write ."
30
30
  },
31
31
  "dependencies": {
32
- "@typescript/native": "npm:typescript@7.1.0-dev.20261006.1"
32
+ "@typescript/native": "npm:typescript@7.1.0-dev.20261007.1"
33
33
  },
34
34
  "overrides": {
35
35
  "qs": "^6.15.2"
@@ -10,7 +10,8 @@ import type { Cascade, Confidence, Severity } from "../config.ts";
10
10
  import type { CompletedFinding, Finding, FindingComponent, PositionedSymbol } from "../finding.ts";
11
11
  import { OUTSIDE, type Graph } from "../graph.ts";
12
12
  import { KINDS, type KindRow } from "../kinds.ts";
13
- import { byPosition } from "../position.ts";
13
+ import { byPosition, positionKey } from "../position.ts";
14
+ import { declarationKey } from "../ref.ts";
14
15
  import { FAMILY_KEY_LENGTH } from "../resolve.ts";
15
16
  import { isPartSubject, isRowSubject, type Dials } from "../suppress.ts";
16
17
  import type { Boundary } from "./boundary.ts";
@@ -149,7 +150,9 @@ export function completed(
149
150
  ): readonly CompletedFinding[] {
150
151
  const { swept } = input;
151
152
  const union = swept.matrix.union;
152
- const positions = new Map(union.symbols.map((symbol, at) => [symbol.ref, at]));
153
+ const positions = new Map(
154
+ union.symbols.map((symbol, at) => [declarationKey(symbol.ref, symbol.position.path), at]),
155
+ );
153
156
  const candidates = new Map(swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
154
157
  const classOf = reachabilityClasses(input);
155
158
  const componentOf = findingComponents(swept, input.config.reporters.cascade);
@@ -168,10 +171,14 @@ export function completed(
168
171
  throw new Error(`${finding.code} names no live row of the issue-kind vocabulary`);
169
172
  }
170
173
  // A row's reference may spell a declaration's, as an ignore entry naming one does, and
171
- // a row is still never the declaration.
174
+ // a row is still never the declaration. Two declarations of one file can share a
175
+ // reference, so the one the finding is positioned at is the subject where there is one.
176
+ const declared = union.at(positionKey(finding.position));
172
177
  const at = isRowSubject(finding.symbol.kind)
173
178
  ? OUTSIDE
174
- : (positions.get(finding.symbol.ref) ?? OUTSIDE);
179
+ : union.symbols[declared]?.ref === finding.symbol.ref
180
+ ? declared
181
+ : (positions.get(declarationKey(finding.symbol.ref, finding.position.path)) ?? OUTSIDE);
175
182
  // A part names the declaration that holds it, and is decided inside it rather than by
176
183
  // the declaration's liveness, so it carries none of the declaration's sweep facts.
177
184
  const part = isPartSubject(finding.symbol.kind);
@@ -2,6 +2,7 @@ import type { Confidence } from "../config.ts";
2
2
  import { evaluateEdges, type EdgeEvaluation } from "../edges.ts";
3
3
  import type { CompletedFinding } from "../finding.ts";
4
4
  import { liveFilesAt } from "../matrix.ts";
5
+ import { declarationKey } from "../ref.ts";
5
6
  import { ledgerOf, type Dials, type Ledger, type SuppressionRecord } from "../suppress.ts";
6
7
  import { insideSkipped } from "../type-errors.ts";
7
8
  import { completed, dialsOf, reportable } from "./completion.ts";
@@ -28,10 +29,10 @@ export const EMITTERS: Emitters = new Map<string, Emitter>([
28
29
  ]);
29
30
 
30
31
  /**
31
- * The references of every declaration an unanswered question could have kept live, which
32
- * the run reports nothing about: a finding about one would be computed from the gap.
32
+ * The keys of every declaration an unanswered question could have kept live, which the run
33
+ * reports nothing about: a finding about one would be computed from the gap.
33
34
  */
34
- function heldRefs(input: EmitterInput): ReadonlySet<string> {
35
+ function heldKeys(input: EmitterInput): ReadonlySet<string> {
35
36
  const held = new Set(input.swept.heldByUnanswered);
36
37
  if (held.size === 0) {
37
38
  return held;
@@ -39,16 +40,16 @@ function heldRefs(input: EmitterInput): ReadonlySet<string> {
39
40
  return new Set(
40
41
  input.swept.matrix.union.symbols
41
42
  .filter((symbol) => held.has(symbol.id))
42
- .map((symbol) => symbol.ref),
43
+ .map((symbol) => declarationKey(symbol.ref, symbol.position.path)),
43
44
  );
44
45
  }
45
46
 
46
47
  /**
47
- * The references of every top-level declaration of a live component file. The file's
48
+ * The keys of every top-level declaration of a live component file. The file's
48
49
  * markup may use each of them, which no analysis of the markup can rule out, so the run
49
50
  * reports nothing about one while the file is live.
50
51
  */
51
- function markupRefs(input: EmitterInput): ReadonlySet<string> {
52
+ function markupKeys(input: EmitterInput): ReadonlySet<string> {
52
53
  const { swept } = input;
53
54
  if (swept.componentFiles.length === 0) {
54
55
  return new Set();
@@ -61,7 +62,7 @@ function markupRefs(input: EmitterInput): ReadonlySet<string> {
61
62
  return new Set(
62
63
  swept.matrix.union.symbols
63
64
  .filter((symbol) => live.has(symbol.parent))
64
- .map((symbol) => symbol.ref),
65
+ .map((symbol) => declarationKey(symbol.ref, symbol.position.path)),
65
66
  );
66
67
  }
67
68
 
@@ -175,17 +176,19 @@ function dialledOver(
175
176
  ): Omit<DecidedFindings, "would" | "withheld"> {
176
177
  const would = [...unmarked, ...found];
177
178
  const ledger = ledgerOf(suppressed.records, would, dialsOver(input, found, suppressed, unmarked));
178
- const unanswered = heldRefs(input);
179
- const markup = markupRefs(input);
179
+ const unanswered = heldKeys(input);
180
+ const markup = markupKeys(input);
180
181
  const kept = reportable(
181
182
  input,
182
- found.filter(
183
- (finding) =>
183
+ found.filter((finding) => {
184
+ const key = declarationKey(finding.symbol.ref, finding.position.path);
185
+ return (
184
186
  !ledger.withheld(finding) &&
185
- !unanswered.has(finding.symbol.ref) &&
186
- !markup.has(finding.symbol.ref) &&
187
- !insideSkipped(input.swept.skipped, finding.position),
188
- ),
187
+ !unanswered.has(key) &&
188
+ !markup.has(key) &&
189
+ !insideSkipped(input.swept.skipped, finding.position)
190
+ );
191
+ }),
189
192
  );
190
193
  const evaluated = evaluateEdges(kept, input.boundary.edges, input.swept.matrix.union.symbols);
191
194
  return { findings: evaluated.findings, evaluations: evaluated.evaluations, ledger };
@@ -2,6 +2,7 @@ import type { Finding, FindingDetails, FindingPosition } from "../finding.ts";
2
2
  import type { InventorySymbol } from "../inventory.ts";
3
3
  import type { MatrixCandidate } from "../matrix.ts";
4
4
  import { positionKey } from "../position.ts";
5
+ import { declarationKey } from "../ref.ts";
5
6
  import type { Emitter, EmitterInput } from "./emitter.ts";
6
7
 
7
8
  /** The codes of this family. */
@@ -242,12 +243,16 @@ function unusedTypeParameters(run: Run): readonly Finding[] {
242
243
  }
243
244
 
244
245
  /**
245
- * The references of the declarations the write-only kind reports, whatever the
246
+ * The keys of the declarations the write-only kind reports, whatever the
246
247
  * severity: a narrowing kind yields each to it, since deleting the declaration
247
248
  * supersedes narrowing it.
248
249
  */
249
250
  export function writeOnlyDeclarations(input: EmitterInput): ReadonlySet<string> {
250
- return new Set(writeOnlySymbols(indexed(input)).map((finding) => finding.symbol.ref));
251
+ return new Set(
252
+ writeOnlySymbols(indexed(input)).map((finding) =>
253
+ declarationKey(finding.symbol.ref, finding.position.path),
254
+ ),
255
+ );
251
256
  }
252
257
 
253
258
  /**
@@ -2,6 +2,7 @@ import type { Finding } from "../finding.ts";
2
2
  import { OUTSIDE, type Graph } from "../graph.ts";
3
3
  import type { InventorySymbol, SymbolKind } from "../inventory.ts";
4
4
  import type { MatrixCandidate } from "../matrix.ts";
5
+ import { declarationKey } from "../ref.ts";
5
6
  import type { RootKind } from "../roots.ts";
6
7
  import type { Emitter, EmitterInput } from "./emitter.ts";
7
8
  import { writeOnlyDeclarations } from "./reads-and-writes.ts";
@@ -159,7 +160,7 @@ function narrowing(
159
160
  const file = facts.fileOf[at] ?? OUTSIDE;
160
161
  if (
161
162
  !topLevelExport(facts, at, symbol) ||
162
- world.writeOnly.has(symbol.ref) ||
163
+ world.writeOnly.has(declarationKey(symbol.ref, symbol.position.path)) ||
163
164
  symbol.kind === "export-alias" ||
164
165
  entersByName(facts.rootedBy[at]) ||
165
166
  entersByName(facts.rootedBy[file]) ||
package/src/ref.ts CHANGED
@@ -169,6 +169,15 @@ export function renderRef(module: Module, fragment: Fragment): string {
169
169
  }
170
170
  }
171
171
 
172
+ /**
173
+ * One declaration's key among a run's: its reference and the file that declares it. Two
174
+ * packages of one name spell one reference for two declarations, so the reference alone
175
+ * keys neither.
176
+ */
177
+ export function declarationKey(ref: string, path: string): string {
178
+ return `${ref}\u0000${path}`;
179
+ }
180
+
172
181
  /**
173
182
  * Whether one string is a reference of this language's grammar, in its canonical
174
183
  * spelling. A string that is not, whether misspelled or spelled in a form the
@@ -1,6 +1,7 @@
1
1
  import type { Confidence, Severity } from "../config.ts";
2
2
  import { jsonChunks } from "../json-chunks.ts";
3
3
  import { KINDS, type KindRow } from "../kinds.ts";
4
+ import { declarationKey } from "../ref.ts";
4
5
  import type { Report, WireFinding, WireStaleSuppression } from "../report.ts";
5
6
  import { lineHashes, symbolFingerprint } from "./fingerprint.ts";
6
7
  import { RenderError, type RenderOptions } from "./reporter.ts";
@@ -160,7 +161,10 @@ function relatedOf(found: WireFinding): readonly Related[] {
160
161
  related.push({ label: "write", at, endLine: at.end_line });
161
162
  }
162
163
  for (const one of found.component.members ?? []) {
163
- if (one.ref !== found.symbol.ref) {
164
+ if (
165
+ declarationKey(one.ref, one.position.path) !==
166
+ declarationKey(found.symbol.ref, found.position.path)
167
+ ) {
164
168
  related.push({ label: "member", at: one.position, endLine: one.position.end_line });
165
169
  }
166
170
  }
@@ -11,6 +11,7 @@ import { jsonChunks } from "./json-chunks.ts";
11
11
  import { joinPath } from "./paths.ts";
12
12
  import { positionAt, walkDocument } from "./json-document.ts";
13
13
  import type { Position } from "./position.ts";
14
+ import { declarationKey } from "./ref.ts";
14
15
  import {
15
16
  CODE_FORM,
16
17
  SUPPRESSION_WITHOUT_REASON,
@@ -84,10 +85,6 @@ interface Read {
84
85
  /** The declarations of the run by reference and file, which is what an entry names. */
85
86
  type ByReference = ReadonlyMap<string, readonly InventorySymbol[]>;
86
87
 
87
- function referenceKey(ref: string, path: string): string {
88
- return `${ref}\u0000${path}`;
89
- }
90
-
91
88
  /**
92
89
  * Reads one entry or row: the records it binds, the refusals it carries, or the
93
90
  * malformed value that ends the run.
@@ -148,7 +145,7 @@ function readRecord(value: unknown, site: Position, byReference: ByReference, sh
148
145
  if (refusals.length > 0) {
149
146
  return { records: [], refusals };
150
147
  }
151
- const named = byReference.get(referenceKey(symbol, written.path)) ?? [];
148
+ const named = byReference.get(declarationKey(symbol, written.path)) ?? [];
152
149
  if (named.length === 0) {
153
150
  return { records: [{ ...written, bound: "" }], refusals: [] };
154
151
  }
@@ -158,7 +155,7 @@ function readRecord(value: unknown, site: Position, byReference: ByReference, sh
158
155
  function byReferenceOf(symbols: readonly InventorySymbol[]): ByReference {
159
156
  const byReference = new Map<string, InventorySymbol[]>();
160
157
  for (const symbol of symbols) {
161
- const key = referenceKey(symbol.ref, symbol.position.path);
158
+ const key = declarationKey(symbol.ref, symbol.position.path);
162
159
  byReference.set(key, [...(byReference.get(key) ?? []), symbol]);
163
160
  }
164
161
  return byReference;
@@ -17,7 +17,7 @@ import type {
17
17
  import { isStringLiteralLikeNode } from "@typescript/native/unstable/ast";
18
18
  import type { StringLiteralLikeNode } from "@typescript/native/unstable/ast";
19
19
  import type { Host } from "./host.ts";
20
- import { dirnamePath, joinPath, resolvePath } from "./paths.ts";
20
+ import { dirnamePath, resolvePath } from "./paths.ts";
21
21
  import type { Engine } from "./session.ts";
22
22
  import type { WorkspaceResolver } from "./workspace-resolution.ts";
23
23
 
@@ -82,34 +82,6 @@ function analysisOptions(parsed: Parsed): CreateSnapshotProgramParams["compilerO
82
82
  return composite === true ? { ...kept, declaration: true } : kept;
83
83
  }
84
84
 
85
- /**
86
- * The references a built program can carry: only where every referenced
87
- * configuration reads and is composite with output, because the compiler reports a
88
- * broken reference at syntax a built program does not have.
89
- */
90
- function carriedReferences(
91
- engine: Engine,
92
- parsed: Parsed,
93
- ): NonNullable<Parsed["projectReferences"]> {
94
- const references = parsed.projectReferences ?? [];
95
- for (const reference of references) {
96
- // A reference that names a directory names the `tsconfig.json` in it.
97
- const configFile = reference.path.endsWith(".json")
98
- ? reference.path
99
- : joinPath(reference.path, "tsconfig.json");
100
- let options: Parsed["options"];
101
- try {
102
- options = engine.parseConfigFile(configFile).options;
103
- } catch {
104
- return [];
105
- }
106
- if (options.composite !== true || options.noEmit === true) {
107
- return [];
108
- }
109
- }
110
- return references;
111
- }
112
-
113
85
  /** The conditions an `exports` lookup under one configuration and one resolution mode reads. */
114
86
  function conditionsOf(parsed: Parsed, mode: ResolutionMode | undefined): ReadonlySet<string> {
115
87
  const resolution = parsed.options.moduleResolution;
@@ -132,7 +104,6 @@ function resolutionMode(kind: ModuleKind): ResolutionMode | undefined {
132
104
  interface Reading {
133
105
  readonly configFile: string;
134
106
  readonly parsed: Parsed;
135
- readonly references: NonNullable<Parsed["projectReferences"]>;
136
107
  readonly entries: ModuleResolutionEntry[];
137
108
  readonly seen: Set<string>;
138
109
  /** Per member specifier, every file an import of it is read as. */
@@ -181,7 +152,7 @@ function configurationOf(
181
152
  if (reason !== undefined) {
182
153
  return { kind: "unread", reason };
183
154
  }
184
- const { options } = reading.parsed;
155
+ const { options, projectReferences: references = [] } = reading.parsed;
185
156
  const raw: unknown = (options as Record<string, unknown>)["pathsBasePath"];
186
157
  const base = typeof raw === "string" ? raw : dirnamePath(reading.configFile);
187
158
  const paths: Record<string, string[]> = {};
@@ -204,8 +175,8 @@ function configurationOf(
204
175
  ...(options.composite === true ? { composite: false, declaration: true } : {}),
205
176
  paths,
206
177
  },
207
- ...(reading.references.length > 0
208
- ? { references: reading.references.map((reference) => ({ path: reference.path })) }
178
+ ...(references.length > 0
179
+ ? { references: references.map((reference) => ({ path: reference.path })) }
209
180
  : {}),
210
181
  ...(exclude === undefined ? {} : { exclude }),
211
182
  });
@@ -240,6 +211,7 @@ function programOf(
240
211
  reading: Reading,
241
212
  ): { readonly params: CreateSnapshotProgramParams; readonly resolver: ModuleResolver } {
242
213
  const compilerOptions = analysisOptions(reading.parsed);
214
+ const references = reading.parsed.projectReferences ?? [];
243
215
  const resolver = engine.createModuleResolver(compilerOptions, reading.entries);
244
216
  return {
245
217
  resolver,
@@ -249,7 +221,7 @@ function programOf(
249
221
  options: {
250
222
  moduleResolver: resolver,
251
223
  configFileParsingDiagnostics: reading.parsed.errors,
252
- ...(reading.references.length > 0 ? { projectReferences: reading.references } : {}),
224
+ ...(references.length > 0 ? { projectReferences: references } : {}),
253
225
  },
254
226
  },
255
227
  };
@@ -436,7 +408,6 @@ export function workspacePrograms(
436
408
  {
437
409
  configFile,
438
410
  parsed,
439
- references: carriedReferences(engine, parsed),
440
411
  entries: [],
441
412
  seen: new Set<string>(),
442
413
  bound: new Map<string, Set<string>>(),