@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/src/edges.ts CHANGED
@@ -200,9 +200,10 @@ function compare(a: string, b: string): number {
200
200
 
201
201
  /**
202
202
  * Evaluates every declared side of this language over the run's findings. A side naming a
203
- * symbol the run enumerates nothing under is `absent`; one naming a symbol the run holds a
204
- * finding about is `dead` and carries that finding, pending until a merge reads the other
205
- * side and reported nowhere else, a narrowing finding included; any other is `live`.
203
+ * symbol the run enumerates nothing under is `absent`; otherwise it is evaluated once per
204
+ * declaration its reference spells. A declaration the run holds a finding about is `dead`
205
+ * and carries that finding, pending until a merge reads the other side and reported
206
+ * nowhere else, a narrowing finding included; any other is `live`.
206
207
  */
207
208
  export function evaluateEdges<F extends Finding>(
208
209
  findings: readonly F[],
@@ -212,10 +213,10 @@ export function evaluateEdges<F extends Finding>(
212
213
  if (sides.length === 0) {
213
214
  return { findings, evaluations: [] };
214
215
  }
215
- const byRef = new Map<string, string>();
216
+ const byRef = new Map<string, string[]>();
216
217
  for (const symbol of symbols) {
217
- if (symbol.kind !== "file" && !byRef.has(symbol.ref)) {
218
- byRef.set(symbol.ref, symbol.id);
218
+ if (symbol.kind !== "file") {
219
+ byRef.set(symbol.ref, [...(byRef.get(symbol.ref) ?? []), symbol.id]);
219
220
  }
220
221
  }
221
222
  const held = new Map<string, number>();
@@ -226,18 +227,20 @@ export function evaluateEdges<F extends Finding>(
226
227
  });
227
228
 
228
229
  const pending = new Set<number>();
229
- const evaluations = sides.map((side): EdgeEvaluation<F> => {
230
- const id = byRef.get(side.symbol);
231
- if (id === undefined) {
232
- return { ...side, state: "absent" };
230
+ const evaluations = sides.flatMap((side): EdgeEvaluation<F>[] => {
231
+ const ids = byRef.get(side.symbol);
232
+ if (ids === undefined) {
233
+ return [{ ...side, state: "absent" }];
233
234
  }
234
- const at = held.get(id);
235
- const finding = at === undefined ? undefined : findings[at];
236
- if (at === undefined || finding === undefined) {
237
- return { ...side, state: "live" };
238
- }
239
- pending.add(at);
240
- return { ...side, state: "dead", finding };
235
+ return ids.map((id) => {
236
+ const at = held.get(id);
237
+ const finding = at === undefined ? undefined : findings[at];
238
+ if (at === undefined || finding === undefined) {
239
+ return { ...side, state: "live" };
240
+ }
241
+ pending.add(at);
242
+ return { ...side, state: "dead", finding };
243
+ });
241
244
  });
242
245
  evaluations.sort((a, b) => compare(a.edge, b.edge) || compare(a.side, b.side));
243
246
  return { findings: findings.filter((_finding, at) => !pending.has(at)), evaluations };
@@ -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
 
@@ -126,8 +127,7 @@ function dialsOver(
126
127
  * family's findings completed by one step, so every component number is minted once, the
127
128
  * finding each record claims withheld, the rest withheld by the dials, so a root one family
128
129
  * reports withholds what another reports in its component, and every declared edge
129
- * evaluated over what remains, so a pending finding is in its evaluation alone. What the
130
- * minimum confidence withheld is what the same decision at the lowest minimum adds.
130
+ * evaluated over what remains, so a pending finding is in its evaluation alone.
131
131
  */
132
132
  export function decidedFindings(
133
133
  input: EmitterInput,
@@ -137,20 +137,7 @@ export function decidedFindings(
137
137
  const unmarked =
138
138
  suppressed.records.length === 0 ? [] : completedOver(suppressed.unmarked, fallenMembers);
139
139
  const would = [...unmarked, ...found];
140
- const decided = dialledOver(input, suppressed, found, unmarked);
141
- const shown =
142
- input.config.analysis.minConfidence === "possible"
143
- ? decided
144
- : dialledOver(
145
- atPossible(input),
146
- {
147
- unmarked: atPossible(suppressed.unmarked),
148
- records: suppressed.records,
149
- },
150
- found,
151
- unmarked,
152
- );
153
- return { ...decided, would, withheld: withheldOf(decided.findings, shown.findings) };
140
+ return { ...dialledOver(input, suppressed, found, unmarked), would };
154
141
  }
155
142
 
156
143
  /** The run's input with the minimum confidence at its lowest, which withholds nothing. */
@@ -172,46 +159,46 @@ function dialledOver(
172
159
  suppressed: Suppressed,
173
160
  found: readonly CompletedFinding[],
174
161
  unmarked: readonly CompletedFinding[],
175
- ): Omit<DecidedFindings, "would" | "withheld"> {
162
+ ): Omit<DecidedFindings, "would"> {
176
163
  const would = [...unmarked, ...found];
177
164
  const ledger = ledgerOf(suppressed.records, would, dialsOver(input, found, suppressed, unmarked));
178
- const unanswered = heldRefs(input);
179
- const markup = markupRefs(input);
180
- const kept = reportable(
181
- input,
182
- found.filter(
183
- (finding) =>
184
- !ledger.withheld(finding) &&
185
- !unanswered.has(finding.symbol.ref) &&
186
- !markup.has(finding.symbol.ref) &&
187
- !insideSkipped(input.swept.skipped, finding.position),
188
- ),
189
- );
165
+ const unanswered = heldKeys(input);
166
+ const markup = markupKeys(input);
167
+ const candidates = found.filter((finding) => {
168
+ const key = declarationKey(finding.symbol.ref, finding.position.path);
169
+ return (
170
+ !ledger.withheld(finding) &&
171
+ !unanswered.has(key) &&
172
+ !markup.has(key) &&
173
+ !insideSkipped(input.swept.skipped, finding.position)
174
+ );
175
+ });
176
+ const kept = reportable(input, candidates);
190
177
  const evaluated = evaluateEdges(kept, input.boundary.edges, input.swept.matrix.union.symbols);
191
- return { findings: evaluated.findings, evaluations: evaluated.evaluations, ledger };
178
+ return {
179
+ findings: evaluated.findings,
180
+ evaluations: evaluated.evaluations,
181
+ ledger,
182
+ withheld: withheldOf(input, candidates),
183
+ };
192
184
  }
193
185
 
194
- /** The findings the lowest minimum would report that the run does not, counted by confidence. */
195
- function withheldOf(
196
- reported: readonly CompletedFinding[],
197
- shown: readonly CompletedFinding[],
198
- ): Withheld {
199
- const kept = new Set(reported.map(findingKey));
186
+ /**
187
+ * The findings the dials withhold only because their confidence is below the minimum, each
188
+ * counted at the confidence this run gives it: the dials at the lowest minimum keep them.
189
+ */
190
+ function withheldOf(input: EmitterInput, candidates: readonly CompletedFinding[]): Withheld {
191
+ const dials = dialsOf(input, candidates);
192
+ const lowest = dialsOf(atPossible(input), candidates);
200
193
  const counts = { certain: 0, probable: 0, possible: 0 };
201
- for (const finding of shown) {
202
- if (!kept.has(findingKey(finding))) {
194
+ for (const finding of candidates) {
195
+ if (dials.withholds(finding) && !lowest.withholds(finding)) {
203
196
  counts[finding.confidence] += 1;
204
197
  }
205
198
  }
206
199
  return counts;
207
200
  }
208
201
 
209
- /** A finding's identity across two decisions of one run: its position, code and subject. */
210
- function findingKey(finding: CompletedFinding): string {
211
- const { path, line, column } = finding.position;
212
- return JSON.stringify([path, line, column, finding.code, finding.symbol.ref]);
213
- }
214
-
215
202
  /** Every finding the run reports, each family's in table order. */
216
203
  export function findingsOf(input: EmitterInput): readonly CompletedFinding[] {
217
204
  return decidedFindings(input).findings;
@@ -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/inventory.ts CHANGED
@@ -1110,7 +1110,12 @@ function refOf(record: Building, chain: readonly Component[]): string {
1110
1110
  return renderRef(record.module, { of: "module" });
1111
1111
  }
1112
1112
  if (record.kind === "export-alias") {
1113
- return renderRef(record.module, { of: "alias", name: record.component.text });
1113
+ const block = record.parentChain.at(-1);
1114
+ return renderRef(record.module, {
1115
+ of: "alias",
1116
+ name: record.component.text,
1117
+ ...(block === undefined ? {} : { block: block.text }),
1118
+ });
1114
1119
  }
1115
1120
  return renderRef(record.module, {
1116
1121
  of: "declaration",
@@ -26,7 +26,7 @@ import {
26
26
  import { nodeKey, type Inventory, type InventorySymbol } from "./inventory.ts";
27
27
  import { renderPosition } from "./position.ts";
28
28
  import { UNANSWERED } from "./query.ts";
29
- import { nameComponent } from "./ref.ts";
29
+ import { aliasFragment, nameComponent } from "./ref.ts";
30
30
  import type { Reference } from "./references.ts";
31
31
  import type { ProjectView } from "./session.ts";
32
32
 
@@ -110,7 +110,7 @@ interface Named {
110
110
  * The module declarations of an inventory a specifier names by name or pattern, each with
111
111
  * the members that hold the export names given.
112
112
  */
113
- export function namedBy(
113
+ function namedBy(
114
114
  held: Inventory,
115
115
  ): (specifier: string, names: readonly string[]) => readonly Named[] {
116
116
  if (held.modules.size === 0) {
@@ -139,7 +139,7 @@ export function namedBy(
139
139
  const wanted = new Set(
140
140
  names.flatMap((one) => [
141
141
  `${ref}.${nameComponent(one)}`,
142
- `${scope}#${nameComponent(one)}:alias`,
142
+ `${scope}#${aliasFragment(one, name)}`,
143
143
  ]),
144
144
  );
145
145
  const members = (children.get(id) ?? []).filter((child) => wanted.has(child.ref));
@@ -1,10 +1,10 @@
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
 
10
10
  import {
@@ -23,7 +23,7 @@ import { RESOLVED_EXTENSIONS, type ParseFile } from "./configuration-files.ts";
23
23
  import { emittedFrom } from "./emit-map.ts";
24
24
  import type { Host } from "./host.ts";
25
25
  import type { Inventory } from "./inventory.ts";
26
- import { namedBy, namesImported } from "./module-declarations.ts";
26
+ import { namesImported } from "./module-declarations.ts";
27
27
  import { dirnamePath, joinPath, normalizePath, queriedModulePath } from "./paths.ts";
28
28
  import { renderPosition } from "./position.ts";
29
29
  import { nameComponent } from "./ref.ts";
@@ -154,7 +154,6 @@ export function outsideTestReferences(
154
154
  exported.set(key, [...(exported.get(key) ?? []), symbol.id]);
155
155
  }
156
156
  }
157
- const declared = namedBy(held);
158
157
  const found: Reference[] = [];
159
158
  for (const { path, file } of tests) {
160
159
  const from = `${path}#`;
@@ -164,11 +163,6 @@ export function outsideTestReferences(
164
163
  const read = (to: string, use: Reference["use"]): void => {
165
164
  found.push({ from, to, position, use, resolution: "syntax", test: true, unheld: true });
166
165
  };
167
- for (const declaration of declared(named.text, named.names)) {
168
- for (const to of [declaration.id, ...declaration.members]) {
169
- read(to, "read");
170
- }
171
- }
172
166
  const written = named.text.includes("?") ? queriedModulePath(named.text) : named.text;
173
167
  if (written === undefined) {
174
168
  continue;
package/src/overrides.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * The overrides of the target's class members. A call through the base class names the
3
3
  * base's member and runs the override of whichever subclass the value is, so the base
4
- * member's declaration references every override. A method overriding a member of a class
5
- * declared outside the target, at any depth of its `extends` chain, is called by outside
6
- * code holding the instance through that class, so its own class references it.
4
+ * member's declaration references every override. A method, an accessor or a property,
5
+ * static or not, overriding a member of a class declared outside the target, at any depth
6
+ * of its `extends` chain, is used by outside code holding the instance or the class
7
+ * through that class, so its own class references it.
7
8
  */
8
9
 
9
10
  import {
@@ -19,8 +20,9 @@ import {
19
20
  import type { AliasChains } from "./alias-chain.ts";
20
21
  import { memberComponent } from "./class-members.ts";
21
22
  import { nodeKey, type Inventory, type InventorySymbol } from "./inventory.ts";
22
- import { SymbolFlags, type Symbol as TSSymbol } from "@typescript/native/unstable/sync";
23
+ import { SymbolFlags, type Symbol as TSSymbol, type Type } from "@typescript/native/unstable/sync";
23
24
  import { UNANSWERED } from "./query.ts";
25
+ import { nameComponent } from "./ref.ts";
24
26
  import type { Reference } from "./references.ts";
25
27
  import type { ProjectView } from "./session.ts";
26
28
 
@@ -60,7 +62,7 @@ function extending<Brand>(
60
62
 
61
63
  /**
62
64
  * The references each base class member makes to its overrides, for the classes of one
63
- * project that extend another class of the target, and each class makes to its methods
65
+ * project that extend another class of the target, and each class makes to its members
64
66
  * that override a member of a class declared outside the target. A member a nearer base
65
67
  * already overrides is reached through that override. A private member is not
66
68
  * overridden. Each reference is placed at the override and made by its file.
@@ -134,7 +136,7 @@ export function overrideReferences<Brand>(
134
136
  const outsideNames = outsideMemberNames(project, bases, outsideBases);
135
137
  for (const [classId, names] of outsideNames) {
136
138
  for (const [component, member] of members.get(classId) ?? []) {
137
- if (member.kind === "method" && names.has(component)) {
139
+ if (names.has(component)) {
138
140
  found.push({
139
141
  from: classId,
140
142
  to: member.id,
@@ -166,7 +168,8 @@ export function overrideReferences<Brand>(
166
168
 
167
169
  /**
168
170
  * Per class of the target with a class declared outside the target in its `extends`
169
- * chain, the names of the members that outside class's instances carry.
171
+ * chain, the member components that outside class's instances and constructor carry, a
172
+ * static one spelled as a static member's component is.
170
173
  */
171
174
  function outsideMemberNames<Brand>(
172
175
  project: ProjectView<Brand>,
@@ -175,10 +178,16 @@ function outsideMemberNames<Brand>(
175
178
  ): ReadonlyMap<string, ReadonlySet<string>> {
176
179
  const direct = new Map<string, ReadonlySet<string>>();
177
180
  for (const [classId, base] of outsideBases) {
178
- const type = project.queries.declaredTypeOf(base);
179
- const properties = type === UNANSWERED ? UNANSWERED : project.queries.propertiesOf(type);
180
- if (properties !== UNANSWERED) {
181
- direct.set(classId, new Set(properties.map((property) => property.escapedName)));
181
+ const instance = project.queries.declaredTypeOf(base);
182
+ const statics = project.queries.typeOfSymbol(base);
183
+ const components = [
184
+ ...namesOf(project, instance === UNANSWERED ? undefined : instance).map(nameComponent),
185
+ ...namesOf(project, statics === UNANSWERED ? undefined : statics).map(
186
+ (name) => `${nameComponent(name)}:static`,
187
+ ),
188
+ ];
189
+ if (components.length > 0) {
190
+ direct.set(classId, new Set(components));
182
191
  }
183
192
  }
184
193
  const names = new Map<string, ReadonlySet<string>>();
@@ -200,3 +209,9 @@ function outsideMemberNames<Brand>(
200
209
  }
201
210
  return names;
202
211
  }
212
+
213
+ /** The names of a type's properties; none where the type or its properties are unanswered. */
214
+ function namesOf<Brand>(project: ProjectView<Brand>, type: Type | undefined): readonly string[] {
215
+ const properties = type === undefined ? UNANSWERED : project.queries.propertiesOf(type);
216
+ return properties === UNANSWERED ? [] : properties.map((property) => property.name);
217
+ }
package/src/ref.ts CHANGED
@@ -47,7 +47,7 @@ const MANIFEST_SCOPE = `${PREFIX}${TS_PACKAGE}/package\\.json`;
47
47
  export const REF_EXPRESSIONS: readonly string[] = [
48
48
  `^${SCOPE}#$`,
49
49
  `^${SCOPE}#${TS_HEAD}$`,
50
- `^${SCOPE}#${TS_HEAD}:alias$`,
50
+ `^${SCOPE}#(?:${TS_QUOTED}\\.)?${TS_HEAD}:alias$`,
51
51
  `^${SCOPE}#${TS_HEAD}(?:\\.${TS_MEMBER})+$`,
52
52
  `^${SCOPE}#${TS_HEAD}(?:\\.${TS_MEMBER})+:static$`,
53
53
  `^${SCOPE}#${TS_HEAD}(?:\\.${TS_MEMBER})*(?::static)?<${TS_IDENT}>$`,
@@ -107,8 +107,12 @@ export type Fragment =
107
107
  readonly static: boolean;
108
108
  readonly typeParameter?: string;
109
109
  }
110
- /** One export specifier, a symbol of its own beside the declaration it names. */
111
- | { readonly of: "alias"; readonly name: string }
110
+ /**
111
+ * One export specifier, a symbol of its own beside the declaration it names. One
112
+ * written inside a `declare module` block carries the block's name, which is always
113
+ * quoted, so the aliases two blocks of one file declare under one name stay apart.
114
+ */
115
+ | { readonly of: "alias"; readonly name: string; readonly block?: string }
112
116
  /** One dependency of a manifest, scoped by that manifest. */
113
117
  | { readonly of: "dependency"; readonly name: string; readonly section: DependencySection };
114
118
 
@@ -124,6 +128,10 @@ export function nameComponent(name: string): string {
124
128
  if (IDENTIFIER.test(name) || PRIVATE_NAME.test(name)) {
125
129
  return name;
126
130
  }
131
+ return quoted(name);
132
+ }
133
+
134
+ function quoted(name: string): string {
127
135
  const escaped = name
128
136
  .replaceAll("\\", "\\\\")
129
137
  .replaceAll("'", "\\'")
@@ -142,6 +150,12 @@ export function computedComponent(text: string): string {
142
150
  return `[${text.replaceAll(ASCII_WHITESPACE, "")}]`;
143
151
  }
144
152
 
153
+ /** The fragment of an export alias, inside the `declare module` block named, if one is. */
154
+ export function aliasFragment(name: string, block?: string): string {
155
+ const head = block === undefined ? "" : `${quoted(block)}.`;
156
+ return `${head}${nameComponent(name)}:alias`;
157
+ }
158
+
145
159
  function spell(component: Component): string {
146
160
  return component.computed ? computedComponent(component.text) : nameComponent(component.text);
147
161
  }
@@ -157,7 +171,7 @@ export function renderRef(module: Module, fragment: Fragment): string {
157
171
  case "module":
158
172
  return `${scope}#`;
159
173
  case "alias":
160
- return `${scope}#${nameComponent(fragment.name)}:alias`;
174
+ return `${scope}#${aliasFragment(fragment.name, fragment.block)}`;
161
175
  case "dependency":
162
176
  return `${scope}#${fragment.name}:${fragment.section}`;
163
177
  case "declaration": {
@@ -169,6 +183,15 @@ export function renderRef(module: Module, fragment: Fragment): string {
169
183
  }
170
184
  }
171
185
 
186
+ /**
187
+ * One declaration's key among a run's: its reference and the file that declares it. Two
188
+ * packages of one name spell one reference for two declarations, so the reference alone
189
+ * keys neither.
190
+ */
191
+ export function declarationKey(ref: string, path: string): string {
192
+ return `${ref}\u0000${path}`;
193
+ }
194
+
172
195
  /**
173
196
  * Whether one string is a reference of this language's grammar, in its canonical
174
197
  * 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;
package/src/version.ts CHANGED
@@ -4,4 +4,4 @@
4
4
  * independently of the analyzer's own version, which is the one the host answers
5
5
  * from the package the analyzer was installed from.
6
6
  */
7
- export const CONTRACT_VERSION = "6.0.0";
7
+ export const CONTRACT_VERSION = "6.1.0";