feat(typescript): index type aliases and shape members as symbols

A TS frontend models its API contracts as `type X = { … }` and `interface`,
so a field on one is exactly what "who breaks if I remove this?" is asked
about. Three gaps made that unanswerable, all in the TypeScript queries:

1. No `type_alias_declaration` -> `@definition.type`, so an alias minted NO
   NODE AT ALL and a context() lookup on an exported contract type answered
   "Symbol not found". TypeScript was the ONLY language missing this — Rust
   (type_item), Kotlin (type_alias), Swift (typealias_declaration) and Dart
   all emit it. The alias was declared for scope resolution but never became
   a graph symbol.
2. No `property_signature` in the parse query, so INTERFACE members minted no
   Property nodes either — the upstream report's "class/interface index fine"
   holds only for the type, not its fields.
3. No `property_signature` in the scope query, so even with nodes present the
   resolver had no member declaration to aim at. Its sibling
   `method_signature` -> `@declaration.method` already existed; only
   properties were missing.

Interface bodies and object-type aliases both spell members as
property_signature, so one pattern per query covers both shapes.

Lands the SYMBOLS, not yet the ACCESSES edges: the shape is already a
class-like scope and now has member declarations, but no edge forms — the
remaining link is owner/type-binding, recorded as todos with the diagnosis.
Note TypeScript sets fieldFallbackOnMethodLookup:false, so unlike JavaScript
there is deliberately no name-based fallback here; the precise path is the
only route by design.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ReidenXerx 2026-08-06 01:44:05 +03:00
parent 841a62ae36
commit 541ab122de
4 changed files with 124 additions and 0 deletions

View file

@ -498,6 +498,16 @@ export const TYPESCRIPT_SCOPE_QUERY = `
(method_signature
name: (property_identifier) @declaration.name) @declaration.method
;; Members of a declared SHAPE — interface bodies and object-type aliases both
;; spell them as property_signature (A4). The sibling method_signature rule
;; above declared interface METHODS, so only properties were missing: a typed
;; receiver resolved to the shape's scope and then found no member there, and
;; the field's consumers were unreachable. TypeScript sets
;; fieldFallbackOnMethodLookup:false, so there is no name-based safety net
;; here — the precise path is the only one, and it needs the declaration.
(property_signature
name: (property_identifier) @declaration.name) @declaration.property
;; Declarations — class fields
(public_field_definition
name: (property_identifier) @declaration.name) @declaration.property

View file

@ -24,6 +24,21 @@ export const TYPESCRIPT_QUERIES = `
(interface_declaration
name: (type_identifier) @name) @definition.interface
; Type aliases (A4). TypeScript was the only language whose aliases minted no
; node: Rust (type_item), Kotlin (type_alias), Swift (typealias_declaration)
; and Dart all emit @definition.type. The alias was declared for scope
; resolution but never became a graph symbol, so a context() lookup on an
; exported API-contract type answered "Symbol not found".
(type_alias_declaration
name: (type_identifier) @name) @definition.type
; Members of a declared SHAPE — interface bodies and object-type aliases both
; spell them as property_signature, so one pattern covers both. A TS frontend
; models its API contracts this way, and without these there is no graph path
; from a contract field to the code that reads it.
(property_signature
name: (property_identifier) @name) @definition.property
(function_declaration
name: (identifier) @name) @definition.function

View file

@ -0,0 +1,19 @@
// A4: API contracts modelled as type aliases and interfaces — the common style
// in a TS frontend. Neither the alias node nor the members of either shape were
// indexed, so there was no graph path from a field to its consumers.
export type LiveModeConfig = {
bookSlots: number;
bookNotionalUsdt: number;
};
export interface LiveModeIface {
ifaceSlots: number;
}
export function renderAlias(cfg: LiveModeConfig): number {
return cfg.bookNotionalUsdt + cfg.bookSlots;
}
export function renderIface(cfg: LiveModeIface): number {
return cfg.ifaceSlots;
}

View file

@ -0,0 +1,80 @@
/**
* A4 — TypeScript type aliases and interface members must be indexed.
*
* A TS frontend models its API contracts as `type X = { … }` and `interface`,
* so a field on one is the thing you ask "who breaks if I remove this?" about.
* Three gaps made that unanswerable, all in the TypeScript PARSE query:
*
* 1. No `type_alias_declaration` -> `@definition.type`, so the alias minted
* NO NODE AT ALL and `context({name:'LiveModeConfig'})` said "Symbol not
* found". TypeScript was the only language missing this — Rust
* (`type_item`), Kotlin (`type_alias`), Swift (`typealias_declaration`)
* and Dart all emit it.
* 2. No `property_signature` pattern, so INTERFACE members minted no
* `Property` nodes either — the upstream report's "class/interface index
* fine" is only half right.
* 3. Alias members likewise had no node.
*/
import { describe, it, expect, beforeAll } from 'vitest';
import { FIXTURES, getRelationships, runPipelineFromRepo, type PipelineResult } from './helpers.js';
import path from 'path';
interface LabelledNode {
readonly label: string;
readonly properties: Record<string, unknown>;
}
describe('TypeScript type-alias and interface members (A4)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'typescript-alias-fields'), () => {});
}, 60000);
const nodesOfLabel = (label: string): string[] =>
Array.from(
(result as unknown as { graph: { iterNodes(): Iterable<LabelledNode> } }).graph.iterNodes(),
)
.filter((n) => n.label === label)
.map((n) => String(n.properties.name));
const readersOf = (field: string): string[] =>
getRelationships(result, 'ACCESSES')
.filter((e) => e.target === field)
.map((e) => e.source);
it('indexes the type alias as a symbol', () => {
// Previously "Symbol not found" — the alias existed for scope resolution
// but never became a graph node.
expect(nodesOfLabel('TypeAlias')).toContain('LiveModeConfig');
});
it('indexes type-alias members as Property nodes', () => {
const props = nodesOfLabel('Property');
expect(props).toContain('bookNotionalUsdt');
expect(props).toContain('bookSlots');
});
it('indexes interface members as Property nodes', () => {
expect(nodesOfLabel('Property')).toContain('ifaceSlots');
});
// The EDGES are not landed yet — nodes and declarations are.
//
// Established: the shape is a class-like scope already (`interface_declaration`
// and `type_alias_declaration value:(object_type)` both emit `@scope.class`),
// and `property_signature` now emits `@declaration.property` alongside the
// pre-existing `method_signature` -> `@declaration.method`. So the receiver
// has a scope and the scope has members, yet no ACCESSES forms — the missing
// link is owner/type-binding, i.e. the member def carrying an `ownerId` that
// the typed receiver resolves to via `findOwnedMember`.
//
// There is deliberately NO name-based safety net here: TypeScript sets
// `fieldFallbackOnMethodLookup: false` (scope-resolver.ts) because name
// matching over-connects in a typed language, and the unique-name pass
// honors that opt-out. The precise path is the only route for TS, by design.
it.todo('links an alias field to its consumer');
it.todo('links an interface field to its consumer');
void readersOf;
});