TypeScript service/RPC contracts written as a tuple of generic types — `type List = [Service<'query_apply_record', Req, Resp>, …]` — carry their names only as string-literal type arguments, so static extraction never indexed them and `codegraph query query_apply_record` returned nothing. Add a narrow TS/TSX type-alias pass that emits each tuple entry's string-literal name as a `method` node under the alias (qualifiedName `List::query_apply_record`), making it searchable. Scope is limited to a direct literal arg of a generic that is a direct tuple element, with a valid-identifier filter — so utility types (Pick/Omit/Record), deeper nested generics, and route paths produce no noise. Bumps EXTRACTION_VERSION so existing indexes get a re-index hint. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
1983590533
commit
636d9fcb7d
@@ -451,6 +451,59 @@ type Internal = string;
|
||||
expect(exported).toHaveLength(2);
|
||||
expect(exported.map((n) => n.name).sort()).toEqual(['DateFormat', 'UnitSystem']);
|
||||
});
|
||||
|
||||
// A service/contract registry written as a tuple of generic instantiations —
|
||||
// the names are string-literal type arguments, not declarations, so static
|
||||
// extraction otherwise never indexes them (issue #634).
|
||||
it('extracts string-literal contract names from a generic tuple type alias (#634)', () => {
|
||||
const code = `
|
||||
interface Service<Name extends string, Req, Resp> { name: Name; }
|
||||
export type MyServiceList = [
|
||||
Service<'query_apply_record', { pageNo: number }, { ok: boolean }>,
|
||||
Service<'apply_confirm', { code: string }, { ok: boolean }>
|
||||
];
|
||||
`;
|
||||
const result = extractFromSource('services/api.ts', code);
|
||||
|
||||
const names = result.nodes.filter(
|
||||
(n) => n.kind === 'method' && n.qualifiedName.startsWith('MyServiceList::')
|
||||
);
|
||||
expect(names.map((n) => n.name).sort()).toEqual(['apply_confirm', 'query_apply_record']);
|
||||
|
||||
const queryNode = names.find((n) => n.name === 'query_apply_record');
|
||||
expect(queryNode?.qualifiedName).toBe('MyServiceList::query_apply_record');
|
||||
// Signature carries the full contract entry so search results show context.
|
||||
expect(queryNode?.signature).toContain("Service<'query_apply_record'");
|
||||
|
||||
// The string-literal name is contained by the type alias.
|
||||
const alias = result.nodes.find((n) => n.kind === 'type_alias' && n.name === 'MyServiceList');
|
||||
const containsEdge = result.edges.find(
|
||||
(e) => e.kind === 'contains' && e.source === alias?.id && e.target === queryNode?.id
|
||||
);
|
||||
expect(containsEdge).toBeDefined();
|
||||
});
|
||||
|
||||
it('does not extract string literals from utility types or nested generics (#634)', () => {
|
||||
const code = `
|
||||
interface User { id: string; name: string; }
|
||||
interface Service<Name extends string, Req, Resp> { name: Name; }
|
||||
export type Picked = Pick<User, 'id' | 'name'>;
|
||||
export type Rec = Record<'foo' | 'bar', number>;
|
||||
// Tuple entry, but the name is a non-identifier route path; the nested Pick's
|
||||
// 'id' must also stay out (only DIRECT literal args of a tuple's generic count).
|
||||
export type Routes = [Service<'/api/users', Pick<User, 'id'>, {}>];
|
||||
// Bare string-literal tuple — not generic type arguments.
|
||||
export type Names = ['alpha', 'beta'];
|
||||
`;
|
||||
const result = extractFromSource('noise.ts', code);
|
||||
|
||||
const leaked = result.nodes.filter(
|
||||
(n) =>
|
||||
(n.kind === 'method' || n.kind === 'property') &&
|
||||
['id', 'name', 'foo', 'bar', 'alpha', 'beta'].includes(n.name)
|
||||
);
|
||||
expect(leaked).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Exported Variable Extraction', () => {
|
||||
|
||||
Reference in New Issue
Block a user