Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions src/graph/bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,7 @@ export function goReceiverVarOf(node: SyntaxNode): string | null {
function goReceiverTypeOf(node: SyntaxNode): string | null {
const recv = node.childForFieldName("receiver");
const param = recv?.namedChildren.find((c) => c.type === "parameter_declaration");
let type = param?.childForFieldName("type");
if (type?.type === "pointer_type") type = type.namedChildren.at(-1) ?? null;
return type?.type === "type_identifier" ? type.text : null;
return goTypeName(param?.childForFieldName("type") ?? null);
}

type RecvCtx = Pick<WalkCtx, "scope" | "enclosingClass" | "goReceiverVar" | "lang" | "bindings" | "rSuperClass">;
Expand Down Expand Up @@ -735,8 +733,7 @@ function handleGo(node: SyntaxNode, scope: string[], bindings: FileBindings): vo
}
let typeName: string | null = null;
if (expr.type === "composite_literal") {
const t = expr.childForFieldName("type");
typeName = t?.type === "type_identifier" ? t.text : null;
typeName = goTypeName(expr.childForFieldName("type"));
} else if (expr.type === "call_expression") {
const fn = expr.childForFieldName("function");
// Go convention: NewX(...) binds to X.
Expand All @@ -746,7 +743,10 @@ function handleGo(node: SyntaxNode, scope: string[], bindings: FileBindings): vo
}
}

function goTypeName(type: SyntaxNode | null): string | null {
const base = type?.type === "pointer_type" ? type.namedChildren.at(-1) : type;
/** A Go type's bare name: a pointer and a type-argument list are both erased
* (`*Stack[int]` → `Stack`), since the method set belongs to the generic type. */
function goTypeName(type: SyntaxNode | null | undefined): string | null {
let base = type?.type === "pointer_type" ? type.namedChildren.at(-1) : type;
if (base?.type === "generic_type") base = base.childForFieldName("type");
return base?.type === "type_identifier" ? base.text : null;
}
4 changes: 3 additions & 1 deletion src/graph/extract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2073,12 +2073,14 @@ function swiftSuperClassName(node: SyntaxNode): string | null {
}

/** The receiver's base type name for a Go method, unwrapping a pointer receiver
* (`func (u *User) …` → `User`). Null if it can't be read. */
* (`func (u *User) …` → `User`) and a generic one (`func (s *Stack[T]) …` → `Stack`).
* Null if it can't be read. */
function goReceiverType(node: SyntaxNode): string | null {
const recv = node.childForFieldName("receiver"); // parameter_list
const param = recv?.namedChildren.find((c) => c.type === "parameter_declaration");
let type = param?.childForFieldName("type");
if (type?.type === "pointer_type") type = type.namedChildren.at(-1) ?? null;
if (type?.type === "generic_type") type = type.childForFieldName("type");
return type?.type === "type_identifier" ? type.text : null;
}

Expand Down
55 changes: 55 additions & 0 deletions test/graph-go-generics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* Go methods on generic types in the Tier-1 code graph.
*/
import { test } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { buildGraph } from "../src/graph/build.js";
import { readGraph, wiringPath } from "../src/graph/write.js";
import type { GraphV1, NodeV1 } from "../src/graph/types.js";

function nodeById(graph: GraphV1, id: string): NodeV1 | undefined {
return graph.nodes.find((n) => n.id === id);
}

test("Go extraction: methods on a generic type keep their receiver and resolve through typed locals", async () => {
const dir = mkdtempSync(join(tmpdir(), "graft-go-generic-recv-"));
try {
writeFileSync(join(dir, "go.mod"), "module example.com/app\n\ngo 1.22\n");
writeFileSync(
join(dir, "coll.go"),
"package main\n\n" +
"type Stack[T any] struct{ items []T }\n\n" +
"func (s *Stack[T]) Push(x T) { s.grow() }\nfunc (s *Stack[T]) grow() {}\n\n" +
"type Queue[K comparable, V any] struct{ m map[K]V }\n\n" +
"func (q Queue[K, V]) Push(k K, v V) { q.grow() }\nfunc (q Queue[K, V]) grow() {}\n\n" +
"func NewStack[T any]() *Stack[T] { return &Stack[T]{} }\n\n" +
"func declared() { var s Stack[int]; s.Push(1) }\n" +
"func literal() { q := &Queue[string, int]{}; q.Push(\"a\", 1) }\n" +
"func ctor() { s := NewStack[int](); s.Push(1) }\n",
);

await buildGraph(dir);
const graph = readGraph(wiringPath(join(dir, "graft")))!;

for (const id of ["Stack.Push", "Stack.grow", "Queue.Push", "Queue.grow"]) {
const node = nodeById(graph, `coll.go#${id}`);
assert.equal(node?.kind, "method", id);
assert.equal(node?.owner, id.split(".")[0], id);
}
assert.equal(nodeById(graph, "coll.go#Push"), undefined, "no owner-less Push node");

const calls = (source: string) =>
graph.edges.filter((e) => e.relation === "calls" && e.source === source).map((e) => e.target);
assert.deepEqual(calls("coll.go#Stack.Push"), ["coll.go#Stack.grow"]);
assert.deepEqual(calls("coll.go#Queue.Push"), ["coll.go#Queue.grow"]);
const pushes = (source: string) => calls(source).filter((t) => t.endsWith(".Push"));
assert.deepEqual(pushes("coll.go#declared"), ["coll.go#Stack.Push"], "var s Stack[int]");
assert.deepEqual(pushes("coll.go#literal"), ["coll.go#Queue.Push"], "&Queue[string, int]{}");
assert.deepEqual(pushes("coll.go#ctor"), ["coll.go#Stack.Push"], "NewStack[int]()");
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
Loading