A call site `recorder.stop()` where `recorder: RecorderHandle` and
`type RecorderHandle = { stop: () => Promise<void> }` used to attach
its edge to an unrelated `class Foo { stop() {} }` in a sibling
directory — there was no `RecorderHandle::stop` node, so the existing
camelCase/path-proximity scoring picked the only `stop` method in the
graph (which happened to be wrong). False-positive `calls` edges
silently widened `codegraph_impact` blast radius.
`extractTypeAlias` now surfaces object-shape (and intersection-type)
members as first-class graph nodes:
type X = { foo: T; bar(): T };
-> X (type_alias)
X::foo (property)
X::bar (method)
Function-typed properties (`stop: () => Promise<void>`) emit as `method`
kind so `obj.stop()` resolves to them at the call site — same node
kind the existing receiver-name/word-overlap heuristic in
`matchMethodCall` already prefers. No new resolver logic needed.
Walk only immediate `object_type` / `intersection_type` operands of the
alias value. Anonymous nested object types inside generic arguments
(`Promise<{ ok: true }>`) intentionally don't produce phantom members.
Validation on excalidraw/excalidraw (314 .ts files):
+776 new property nodes (alias non-function members)
+1,008 new method nodes (alias function-typed properties + method_signatures)
+226 calls edges newly accurate against alias members
User's exact 3-file repro:
before: finaliseRecording -> StdioMcpClient::stop (wrong, sibling dir)
after: finaliseRecording -> RecorderHandle::stop (correct)
StdioMcpClient::stop callers: voice/ false-positives gone
Closes #359.
This commit is contained in:
@@ -742,6 +742,66 @@ func UseAliased() {
|
||||
expect(target?.filePath.replace(/\\/g, '/')).toBe('pkgb/lib.go');
|
||||
});
|
||||
|
||||
it('TS type_alias object-shape members resolve method calls (#359)', async () => {
|
||||
// Pre-#359, `recorder.stop()` (recorder: RecorderHandle) attached
|
||||
// to `StdioMcpClient.stop` in a sibling directory via path-proximity
|
||||
// because the type_alias had no `stop` node — only the unrelated
|
||||
// class did. Now type_alias produces member nodes (property/method),
|
||||
// so the camelCase receiver↔type word overlap pulls the call to
|
||||
// `RecorderHandle::stop` instead of the look-alike class.
|
||||
fs.mkdirSync(path.join(tempDir, 'voice'));
|
||||
fs.mkdirSync(path.join(tempDir, 'codegraph'));
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, 'voice', 'recorder.ts'),
|
||||
`export type RecorderHandle = {
|
||||
wavPath: string;
|
||||
stop: () => Promise<{ ok: true }>;
|
||||
};
|
||||
`
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, 'voice', 'controller.ts'),
|
||||
`import type { RecorderHandle } from "./recorder";
|
||||
export async function finaliseRecording(recorder: RecorderHandle) {
|
||||
return await recorder.stop();
|
||||
}
|
||||
`
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, 'codegraph', 'stdio-client.ts'),
|
||||
`export class StdioMcpClient {
|
||||
private stopped = false;
|
||||
async stop(): Promise<void> { this.stopped = true; }
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
cg = await CodeGraph.init(tempDir, { index: true });
|
||||
|
||||
const handleStop = cg
|
||||
.getNodesByKind('method')
|
||||
.find((n) => n.qualifiedName === 'RecorderHandle::stop');
|
||||
expect(handleStop).toBeDefined();
|
||||
|
||||
const clientStop = cg
|
||||
.getNodesByKind('method')
|
||||
.find((n) => n.qualifiedName === 'StdioMcpClient::stop');
|
||||
expect(clientStop).toBeDefined();
|
||||
|
||||
const handleCallers = cg.getIncomingEdges(handleStop!.id).filter((e) => e.kind === 'calls');
|
||||
const clientCallers = cg.getIncomingEdges(clientStop!.id).filter((e) => e.kind === 'calls');
|
||||
expect(handleCallers.length).toBeGreaterThanOrEqual(1);
|
||||
// The class method must have NO callers — voice/'s call must NOT
|
||||
// mis-attribute. A non-empty list would mean the false-positive
|
||||
// path is still firing.
|
||||
expect(clientCallers).toHaveLength(0);
|
||||
|
||||
// Function-typed property surfaces as a `method` node, not `property`,
|
||||
// because `stop()` semantics at the call site are method semantics.
|
||||
expect(handleStop!.kind).toBe('method');
|
||||
});
|
||||
|
||||
it('C# extracts references from method/property/field types (#381)', async () => {
|
||||
// Pre-#381, every C# project produced ZERO `references` edges:
|
||||
// csharp.ts was missing returnField, and the type-leaf walker
|
||||
|
||||
Reference in New Issue
Block a user