From 90f1a56fe95df2692cdbc71599b57fe2f307e750 Mon Sep 17 00:00:00 2001 From: msnandhis <45960035+msnandhis@users.noreply.github.com> Date: Sun, 31 May 2026 18:25:56 +0530 Subject: [PATCH 1/3] fix: attach Go receiver methods to owner types --- __tests__/extraction.test.ts | 28 +++++++++++++++ __tests__/resolution.test.ts | 44 ++++++++++++++++++++++++ src/extraction/languages/go.ts | 5 +-- src/resolution/index.ts | 63 ++++++++++++++++++++++++++++++++++ 4 files changed, 138 insertions(+), 2 deletions(-) diff --git a/__tests__/extraction.test.ts b/__tests__/extraction.test.ts index b497af6a9..3e4c6e441 100644 --- a/__tests__/extraction.test.ts +++ b/__tests__/extraction.test.ts @@ -657,6 +657,34 @@ func (s *Service) GetUser(id string) (*User, error) { expect(methodNode).toBeDefined(); expect(methodNode?.name).toBe('GetUser'); }); + + it('should attach generic receiver methods to their Go type', () => { + const code = ` +package main + +type Stack[T any] struct { + items []T +} + +func (s *Stack[T]) Push(v T) { + s.items = append(s.items, v) +} +`; + const result = extractFromSource('stack.go', code); + + const stack = result.nodes.find((n) => n.kind === 'struct' && n.name === 'Stack'); + const push = result.nodes.find((n) => n.kind === 'method' && n.name === 'Push'); + expect(stack).toBeDefined(); + expect(push).toBeDefined(); + expect(push?.qualifiedName).toContain('Stack::Push'); + expect(result.edges).toContainEqual( + expect.objectContaining({ + source: stack!.id, + target: push!.id, + kind: 'contains', + }) + ); + }); }); describe('Rust Extraction', () => { diff --git a/__tests__/resolution.test.ts b/__tests__/resolution.test.ts index 03b8ea6ab..cb6377431 100644 --- a/__tests__/resolution.test.ts +++ b/__tests__/resolution.test.ts @@ -853,6 +853,50 @@ func UseAliased() { expect(target?.filePath.replace(/\\/g, '/')).toBe('pkgb/lib.go'); }); + it('links Go receiver methods to owner types across files in the same package (#583)', async () => { + fs.writeFileSync( + path.join(tempDir, 'go.mod'), + 'module github.com/example/myproject\n\ngo 1.21\n' + ); + fs.mkdirSync(path.join(tempDir, 'repro')); + fs.mkdirSync(path.join(tempDir, 'other')); + fs.writeFileSync( + path.join(tempDir, 'repro', 'widget.go'), + 'package repro\n\ntype Widget struct{ name string }\nfunc (w *Widget) Foo() string { return "foo:" + w.name }\n' + ); + fs.writeFileSync( + path.join(tempDir, 'repro', 'widget_extra.go'), + 'package repro\n\nfunc (w *Widget) Bar() string { return "bar:" + w.name }\n' + ); + fs.writeFileSync( + path.join(tempDir, 'other', 'widget.go'), + 'package other\n\ntype Widget struct{ id int }\n' + ); + + cg = await CodeGraph.init(tempDir, { index: true }); + + const widget = cg.getNodesByKind('struct').find((n) => n.name === 'Widget' && n.filePath === 'repro/widget.go'); + const otherWidget = cg.getNodesByKind('struct').find((n) => n.name === 'Widget' && n.filePath === 'other/widget.go'); + const methods = cg.getNodesByKind('method').filter((n) => n.qualifiedName.startsWith('Widget::')); + const methodNames = methods.map((n) => n.name).sort(); + expect(widget).toBeDefined(); + expect(otherWidget).toBeDefined(); + expect(methodNames).toEqual(['Bar', 'Foo']); + + const containedMethodNames = cg.getOutgoingEdges(widget!.id) + .filter((edge) => edge.kind === 'contains') + .map((edge) => cg.getNode(edge.target)?.name) + .filter(Boolean) + .sort(); + expect(containedMethodNames).toEqual(['Bar', 'Foo']); + + const otherContainedMethodNames = cg.getOutgoingEdges(otherWidget!.id) + .filter((edge) => edge.kind === 'contains') + .map((edge) => cg.getNode(edge.target)?.name) + .filter(Boolean); + expect(otherContainedMethodNames).toEqual([]); + }); + 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 diff --git a/src/extraction/languages/go.ts b/src/extraction/languages/go.ts index 5b4d7975d..90bff24f1 100644 --- a/src/extraction/languages/go.ts +++ b/src/extraction/languages/go.ts @@ -56,8 +56,9 @@ export const goExtractor: LanguageExtractor = { if (!receiver) return undefined; // Find the type identifier inside the receiver const text = getNodeText(receiver, source); - // Extract type name from patterns like "(sl *Type)", "(sl Type)", "(*Type)", "(Type)" - const match = text.match(/\*?\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)/); + // Extract type name from patterns like "(sl *Type)", "(sl Type)", + // "(*Type)", "(Type)", and generic receivers like "(s *Stack[T])". + const match = text.match(/\(\s*(?:[A-Za-z_][A-Za-z0-9_]*\s+)?\*?\s*([A-Za-z_][A-Za-z0-9_]*)/); return match?.[1]; }, }; diff --git a/src/resolution/index.ts b/src/resolution/index.ts index 5158e8301..67d8a99cc 100644 --- a/src/resolution/index.ts +++ b/src/resolution/index.ts @@ -256,10 +256,73 @@ export class ReferenceResolver { }); } } + updated += this.linkGoReceiverMethodOwners(); if (updated > 0) this.clearCaches(); return updated; } + private linkGoReceiverMethodOwners(): number { + const methods = this.queries.getNodesByKind('method') + .filter((node) => node.language === 'go' && node.qualifiedName.includes('::')); + if (methods.length === 0) return 0; + + const ownersByName = new Map(); + for (const kind of ['struct', 'class', 'enum', 'trait'] as const) { + for (const owner of this.queries.getNodesByKind(kind)) { + if (owner.language !== 'go') continue; + const existing = ownersByName.get(owner.name) ?? []; + existing.push(owner); + ownersByName.set(owner.name, existing); + } + } + + const packageNameCache = new Map(); + const packageName = (filePath: string): string | null => { + if (packageNameCache.has(filePath)) return packageNameCache.get(filePath)!; + const source = this.context.readFile(filePath); + const match = source?.match(/^\s*package\s+([A-Za-z_][A-Za-z0-9_]*)/m); + const name = match?.[1] ?? null; + packageNameCache.set(filePath, name); + return name; + }; + + const edges: Edge[] = []; + for (const method of methods) { + const receiverType = method.qualifiedName.split('::', 1)[0]; + if (!receiverType) continue; + + const alreadyOwned = this.queries.getIncomingEdges(method.id, ['contains']) + .some((edge) => { + const source = this.queries.getNodeById(edge.source); + return source && source.language === 'go' && source.name === receiverType; + }); + if (alreadyOwned) continue; + + const methodDir = path.dirname(method.filePath); + const methodPackage = packageName(method.filePath); + if (!methodPackage) continue; + + const owner = (ownersByName.get(receiverType) ?? []).find((candidate) => + path.dirname(candidate.filePath) === methodDir && + packageName(candidate.filePath) === methodPackage + ); + if (!owner) continue; + + edges.push({ + source: owner.id, + target: method.id, + kind: 'contains', + provenance: 'heuristic', + metadata: { synthesizedBy: 'go-receiver-owner' }, + }); + } + + if (edges.length > 0) { + this.queries.insertEdges(edges); + } + return edges.length; + } + /** * Pre-build lightweight caches for resolution. * Node lookups are now handled by indexed SQLite queries instead of From ff577f3fe526498d95083eac67c4fabd714e42a7 Mon Sep 17 00:00:00 2001 From: msnandhis <45960035+msnandhis@users.noreply.github.com> Date: Sun, 31 May 2026 18:43:16 +0530 Subject: [PATCH 2/3] fix: include Go type aliases as method owners --- __tests__/resolution.test.ts | 16 ++++++++++++++++ src/resolution/index.ts | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/__tests__/resolution.test.ts b/__tests__/resolution.test.ts index cb6377431..3f8cc1222 100644 --- a/__tests__/resolution.test.ts +++ b/__tests__/resolution.test.ts @@ -868,6 +868,14 @@ func UseAliased() { path.join(tempDir, 'repro', 'widget_extra.go'), 'package repro\n\nfunc (w *Widget) Bar() string { return "bar:" + w.name }\n' ); + fs.writeFileSync( + path.join(tempDir, 'repro', 'status.go'), + 'package repro\n\ntype Status string\n' + ); + fs.writeFileSync( + path.join(tempDir, 'repro', 'status_extra.go'), + 'package repro\n\nfunc (s Status) String() string { return string(s) }\n' + ); fs.writeFileSync( path.join(tempDir, 'other', 'widget.go'), 'package other\n\ntype Widget struct{ id int }\n' @@ -877,10 +885,12 @@ func UseAliased() { const widget = cg.getNodesByKind('struct').find((n) => n.name === 'Widget' && n.filePath === 'repro/widget.go'); const otherWidget = cg.getNodesByKind('struct').find((n) => n.name === 'Widget' && n.filePath === 'other/widget.go'); + const status = cg.getNodesByKind('type_alias').find((n) => n.name === 'Status' && n.filePath === 'repro/status.go'); const methods = cg.getNodesByKind('method').filter((n) => n.qualifiedName.startsWith('Widget::')); const methodNames = methods.map((n) => n.name).sort(); expect(widget).toBeDefined(); expect(otherWidget).toBeDefined(); + expect(status).toBeDefined(); expect(methodNames).toEqual(['Bar', 'Foo']); const containedMethodNames = cg.getOutgoingEdges(widget!.id) @@ -895,6 +905,12 @@ func UseAliased() { .map((edge) => cg.getNode(edge.target)?.name) .filter(Boolean); expect(otherContainedMethodNames).toEqual([]); + + const statusContainedMethodNames = cg.getOutgoingEdges(status!.id) + .filter((edge) => edge.kind === 'contains') + .map((edge) => cg.getNode(edge.target)?.name) + .filter(Boolean); + expect(statusContainedMethodNames).toEqual(['String']); }); it('TS type_alias object-shape members resolve method calls (#359)', async () => { diff --git a/src/resolution/index.ts b/src/resolution/index.ts index 67d8a99cc..b67773368 100644 --- a/src/resolution/index.ts +++ b/src/resolution/index.ts @@ -267,7 +267,7 @@ export class ReferenceResolver { if (methods.length === 0) return 0; const ownersByName = new Map(); - for (const kind of ['struct', 'class', 'enum', 'trait'] as const) { + for (const kind of ['struct', 'class', 'enum', 'trait', 'type_alias'] as const) { for (const owner of this.queries.getNodesByKind(kind)) { if (owner.language !== 'go') continue; const existing = ownersByName.get(owner.name) ?? []; From 872d04c2f1fd311c090e929e682e3597b8ef72a2 Mon Sep 17 00:00:00 2001 From: msnandhis <45960035+msnandhis@users.noreply.github.com> Date: Sun, 31 May 2026 18:59:34 +0530 Subject: [PATCH 3/3] fix: run Go receiver owner linking for indexFiles --- __tests__/resolution.test.ts | 28 ++++++++++++++++++++++++++++ src/index.ts | 7 ++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/__tests__/resolution.test.ts b/__tests__/resolution.test.ts index 3f8cc1222..e7f8e717a 100644 --- a/__tests__/resolution.test.ts +++ b/__tests__/resolution.test.ts @@ -913,6 +913,34 @@ func UseAliased() { expect(statusContainedMethodNames).toEqual(['String']); }); + it('links Go receiver methods after indexing selected files (#583)', async () => { + fs.writeFileSync( + path.join(tempDir, 'go.mod'), + 'module github.com/example/myproject\n\ngo 1.21\n' + ); + fs.mkdirSync(path.join(tempDir, 'repro')); + fs.writeFileSync( + path.join(tempDir, 'repro', 'widget.go'), + 'package repro\n\ntype Widget struct{ name string }\n' + ); + fs.writeFileSync( + path.join(tempDir, 'repro', 'widget_extra.go'), + 'package repro\n\nfunc (w *Widget) Bar() string { return "bar:" + w.name }\n' + ); + + cg = CodeGraph.initSync(tempDir); + const result = await cg.indexFiles(['repro/widget.go', 'repro/widget_extra.go']); + expect(result.success).toBe(true); + + const widget = cg.getNodesByKind('struct').find((n) => n.name === 'Widget' && n.filePath === 'repro/widget.go'); + expect(widget).toBeDefined(); + const containedMethodNames = cg.getOutgoingEdges(widget!.id) + .filter((edge) => edge.kind === 'contains') + .map((edge) => cg.getNode(edge.target)?.name) + .filter(Boolean); + expect(containedMethodNames).toEqual(['Bar']); + }); + 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 diff --git a/src/index.ts b/src/index.ts index ee3bf51fa..e7ff1a6c9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -397,7 +397,12 @@ export class CodeGraph { return { success: false, filesIndexed: 0, filesSkipped: 0, filesErrored: 0, nodesCreated: 0, edgesCreated: 0, errors: [{ message: 'Could not acquire file lock - another process may be indexing', severity: 'error' as const }], durationMs: 0 }; } try { - return this.orchestrator.indexFiles(filePaths); + const result = await this.orchestrator.indexFiles(filePaths); + if (result.success && result.filesIndexed > 0) { + this.resolver.initialize(); + this.resolver.runPostExtract(); + } + return result; } finally { this.fileLock.release(); }