fix(scope-resolution): link type consumers to the type they name

An exported contract type owned its members after round 1 and still answered
`incoming: {}`, so "what breaks if I remove this field?" — the question a
contract type exists to answer — had no edge to walk. Measured on the
reporting repo: all 324 TypeAlias nodes AND every Interface node had DEFINES
as their only incoming edge.

Two independent causes, and the second is why the first was not enough.

TypeScript captured no type references at all — only cpp and csharp did — so
an annotation naming a declared type minted no reference site. Added for
annotations, generic arguments and `as` assertions, anchored to those contexts
rather than a bare `(type_identifier)`, which would also match the name in
`type X = …` and make every declaration a consumer of itself.

That alone fixed interfaces and left aliases still empty. `TypeAlias` was
missing from `LINKABLE_LABELS`, so alias graph nodes were never indexed in
`nodeLookup` and `resolveDefGraphId` could not bridge a def to its node — the
edge was dropped AFTER a successful lookup. `CLASS_KINDS` has always listed
TypeAlias and the ClassRegistry returned the def correctly, which is what made
this read as a resolution failure; instrumenting the lookup showed it
returning the right def all along and moved the search one table over. Exactly
the bug already documented two entries above it for Trait.

Fixes every language that spells an alias this way — TypeScript, Kotlin, Dart
and Rust all emit `@declaration.type_alias`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ReidenXerx 2026-08-06 20:32:38 +03:00
parent 2e29a03467
commit 19974b49d8
4 changed files with 68 additions and 0 deletions

View file

@ -1218,6 +1218,37 @@ export const TYPESCRIPT_SCOPE_QUERY = `
(object
(shorthand_property_identifier) @reference.name @reference.property-key @reference.value-ref)
;; References — TYPE POSITION (R2-2). An annotation naming a declared type is
;; the only thing that makes that type's declaration reachable from the code
;; that depends on it, and TypeScript captured none: only cpp and csharp emitted
;; type references at all. So an exported API-contract type owned its members
;; (round 1) but had \`incoming: {}\`, and "what breaks if I remove this field?"
;; — the question a contract type exists to answer — had no edge to walk.
;;
;; The resolution path was already complete on the other side:
;; \`type-reference\` routes to the ClassRegistry, whose CLASS_KINDS already
;; lists TypeAlias, Interface and Enum, and \`edges.ts\` already maps the kind to
;; USES. Only the capture was missing.
;;
;; Anchored to the CONTEXTS a type is used in — annotations, type arguments,
;; and heritage \`implements\` — never a bare \`(type_identifier)\`. A blanket rule
;; would also match the identifier in \`type X = …\` and \`interface X\`, making
;; every declaration a consumer of itself.
(type_annotation
(type_identifier) @reference.name @reference.type_reference)
(type_annotation
(generic_type
name: (type_identifier) @reference.name @reference.type_reference))
(type_arguments
(type_identifier) @reference.name @reference.type_reference)
;; \`x as SomeType\` / \`satisfies SomeType\` — an assertion is a claim ABOUT a
;; declared type, so the code making it depends on that declaration.
(as_expression
(type_identifier) @reference.name @reference.type_reference)
`;
/**

View file

@ -273,6 +273,21 @@ export const LINKABLE_LABELS: ReadonlySet<NodeLabel> = new Set<NodeLabel>([
// IMPLEMENTS edges from classes to traits are otherwise invisible to
// the scope-resolution MRO pass.
'Trait',
// TypeAlias is linkable for the same reason Trait is (R2-2). The alias
// resolves fine — `CLASS_KINDS` has always listed it, and the ClassRegistry
// returns the def — but without an entry here `resolveDefGraphId` cannot
// bridge that def to its graph node, so the edge is dropped after a
// SUCCESSFUL lookup. That is why an exported contract type owned its members
// and still reported `incoming: {}`: the failure was one table away from
// everything that appeared to be responsible.
//
// Covers every language that spells an alias this way — TypeScript, Kotlin,
// Dart and Rust all emit `@declaration.type_alias`. The remaining
// `CLASS_KINDS` entries (Typedef, Record, Union, Delegate, Annotation,
// Template) plausibly have the same gap, but nothing exercises them today
// and adding labels no test covers is how this list drifts out of sync with
// what it claims.
'TypeAlias',
// Variable / Property are linkable too — receiver-bound write/read
// ACCESSES edges target field nodes (e.g. `user.name = "x"` →
// ACCESSES edge to User's `name` Variable/Property node).

View file

@ -17,3 +17,4 @@ export function renderAlias(cfg: LiveModeConfig): number {
export function renderIface(cfg: LiveModeIface): number {
return cfg.ifaceSlots;
}

View file

@ -80,4 +80,25 @@ describe('TypeScript type-alias and interface members (A4)', () => {
it('links an alias field to its consumer', () => {
expect(readersOf('bookNotionalUsdt')).toContain('renderAlias');
});
// R2-2. Owning the members was only half of it: with no edge INTO the type,
// `context()` on an exported contract answered `incoming: {}`, so "what
// breaks if I remove this field?" — the question a contract type exists to
// answer — had nothing to walk. Measured on the reporting repo, all 324
// TypeAlias nodes and every Interface node had DEFINES as their ONLY
// incoming edge, because TypeScript captured no type references at all.
describe('type consumers (R2-2)', () => {
const usersOf = (typeName: string): string[] =>
getRelationships(result, 'USES')
.filter((e) => e.target === typeName)
.map((e) => e.source);
it('links a parameter annotation to the alias it names', () => {
expect(usersOf('LiveModeConfig')).toContain('renderAlias');
});
it('links a parameter annotation to the interface it names', () => {
expect(usersOf('LiveModeIface')).toContain('renderIface');
});
});
});