Summary
In Go a type's methods may (a) use a type-parameter receiver — func (s *Stack[T]) ... — and/or (b) be declared in a different file from the type declaration. Both are idiomatic and extremely common. In v0.9.7 (@colbymchenry/codegraph, source main @ b026e64) the extractor fails to attach such methods to their receiver type: no contains edge is created. The method nodes still exist, but they are orphaned from the struct.
Two independent root causes, one symptom (an incomplete method set on the type):
- Generic receivers — the receiver-type regex can't parse
*Stack[T], so the method gets no receiver type at all.
- Cross-file methods — the struct→method owner lookup is scoped to the current file, so a method declared in another file of the same package never finds its type.
Because almost every downstream feature derives a type's method set from these contains edges — codegraph_node member outlines, callers/callees/impact, the gRPC Unimplemented*Server matcher (goGrpcStubImplEdges), and any future interface-satisfaction work — both bugs silently degrade Go navigation.
Environment
- codegraph v0.9.7 (npm
@colbymchenry/codegraph), source main @ b026e64
- backend:
node:sqlite; macOS arm64
Reproduction (minimal, self-contained)
go.mod
module example.com/repro
go 1.22
generic.go — generic-receiver case
package repro
type Stack[T any] struct{ items []T }
func (s *Stack[T]) Push(v T) { s.items = append(s.items, v) }
func (s *Stack[T]) Pop() (T, bool) { var z T; if len(s.items)==0 { return z,false }; n:=len(s.items)-1; v:=s.items[n]; s.items=s.items[:n]; return v,true }
func (s *Stack[T]) Len() int { return len(s.items) }
widget_a.go / widget_b.go — cross-file case (legal Go: methods on a type may live in any file of the package)
// widget_a.go
package repro
type Widget struct{ name string }
func (w *Widget) Foo() string { return "foo:" + w.name }
// widget_b.go
package repro
func (w *Widget) Bar() string { return "bar:" + w.name } // method in a DIFFERENT file than the struct
Control — same-file methods (e.g. a MemoryStore struct with Save/Load in the same file).
Index, then read the graph DB directly (bypasses all display/formatting layers):
codegraph init -i
sqlite3 .codegraph/codegraph.db \
"SELECT s.name AS type, m.name AS method, m.file_path
FROM edges e
JOIN nodes s ON e.source = s.id
JOIN nodes m ON e.target = m.id
WHERE e.kind='contains' AND s.kind='struct' AND m.kind='method'
ORDER BY s.name;"
Observed (ground truth from the edges table)
| type |
contains→method edges |
missing |
MemoryStore (control, same file) |
Save, Load |
— |
SQLiteStore (control, same file) |
Save, Load |
— |
Widget |
Foo only |
Bar (declared in widget_b.go) |
Stack[T] |
none |
Push, Pop, Len |
The qualified_name of the method nodes isolates the two distinct causes:
Push qualified_name = "Push" -- no receiver type extracted at all
Bar qualified_name = "Widget::Bar" -- receiver extracted fine; only the contains edge is missing
Foo qualified_name = "Widget::Foo" -- contains edge present (same file)
Root cause 1 — generic-receiver regex (src/extraction/languages/go.ts:60)
getReceiverType: (node, source) => {
const receiver = getChildByField(node, 'receiver');
if (!receiver) return undefined;
const text = getNodeText(receiver, source); // e.g. "(s *Stack[T])"
const match = text.match(/\*?\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)/);
return match?.[1];
}
The pattern requires the captured identifier to be immediately followed by optional whitespace and ). For a type-parameter receiver (s *Stack[T]) the type name Stack is followed by [T]), so the regex never matches → getReceiverType returns undefined. The method is then treated as receiver-less: no Type:: qualified name and (via the code path in root cause 2) no contains edge. This affects every method on every generic type (Go 1.18+).
Suggested fix: strip the type-parameter list before matching, or anchor on the first type identifier rather than on ). For example, parsing the receiver as ( name? '*'? IDENT ('[' … ']')? ) and capturing IDENT; a regex such as
/\(\s*(?:[A-Za-z_]\w*\s+)?\*?\s*([A-Za-z_]\w*)/
captures Stack for (s *Stack[T]), (s Stack[K, V]), (*Stack[T]), and the existing non-generic forms (sl *scrapeLoop) / (Type).
Root cause 2 — same-file-only owner lookup (src/extraction/tree-sitter.ts:785-790)
if (receiverType && !this.isInsideClassLikeNode()) {
const ownerNode = this.nodes.find(
(n) =>
n.name === receiverType &&
n.filePath === this.filePath && // <-- restricts the owner to the CURRENT file
(n.kind === 'struct' || n.kind === 'class' || n.kind === 'enum' || n.kind === 'trait')
);
if (ownerNode) {
this.edges.push({ source: ownerNode.id, target: methodNode.id, kind: 'contains' });
}
}
Extraction is per file, and this.nodes holds only the nodes of the file currently being parsed. A method whose receiver type is declared in another file of the same package (e.g. Widget in widget_a.go, Bar in widget_b.go) never finds an ownerNode, so no contains edge is emitted. The Type::method qualified name is still set, so it is purely the structural edge that is lost. This is idiomatic Go (large types routinely split methods across foo.go, foo_apply.go, …). It also affects Rust impl blocks placed in separate files.
Suggested fix: for methodsAreTopLevel / receiver-based languages, bind receiver-type → method as a resolution-phase join over the whole package rather than a same-file extraction-time lookup. After extraction, for each Go/Rust method node that has a receiver type but no contains parent, link it to the type node of matching name within the same package (same directory / module package). The package model added for cross-package call resolution (#388) already supplies what's needed.
Impact
- Type member sets are incomplete in
codegraph_node outlines and in every method-set computation.
callers/callees/impact on these methods lose the structural relationship to their type.
goGrpcStubImplEdges matches hand-written impls by comparing method-name sets built from contains edges; an impl whose methods span files — or any generic service — will not match.
- Blocks correct Go interface-satisfaction (filed as a companion issue), which also needs complete method sets.
Notes
- The generic struct node is indexed but its
type_parameters column is empty — minor, same generics-coverage theme.
- No existing issue covers this (searched open + closed for "generic receiver", "type parameter", "cross-file method").
Summary
In Go a type's methods may (a) use a type-parameter receiver —
func (s *Stack[T]) ...— and/or (b) be declared in a different file from thetypedeclaration. Both are idiomatic and extremely common. In v0.9.7 (@colbymchenry/codegraph, sourcemain @ b026e64) the extractor fails to attach such methods to their receiver type: nocontainsedge is created. The method nodes still exist, but they are orphaned from the struct.Two independent root causes, one symptom (an incomplete method set on the type):
*Stack[T], so the method gets no receiver type at all.Because almost every downstream feature derives a type's method set from these
containsedges —codegraph_nodemember outlines,callers/callees/impact, the gRPCUnimplemented*Servermatcher (goGrpcStubImplEdges), and any future interface-satisfaction work — both bugs silently degrade Go navigation.Environment
@colbymchenry/codegraph), sourcemain @ b026e64node:sqlite; macOS arm64Reproduction (minimal, self-contained)
go.modgeneric.go— generic-receiver casewidget_a.go/widget_b.go— cross-file case (legal Go: methods on a type may live in any file of the package)Control — same-file methods (e.g. a
MemoryStorestruct withSave/Loadin the same file).Index, then read the graph DB directly (bypasses all display/formatting layers):
Observed (ground truth from the
edgestable)contains→method edgesMemoryStore(control, same file)Save,LoadSQLiteStore(control, same file)Save,LoadWidgetFooonlyBar(declared inwidget_b.go)Stack[T]Push,Pop,LenThe
qualified_nameof the method nodes isolates the two distinct causes:Root cause 1 — generic-receiver regex (
src/extraction/languages/go.ts:60)The pattern requires the captured identifier to be immediately followed by optional whitespace and
). For a type-parameter receiver(s *Stack[T])the type nameStackis followed by[T]), so the regex never matches →getReceiverTypereturnsundefined. The method is then treated as receiver-less: noType::qualified name and (via the code path in root cause 2) nocontainsedge. This affects every method on every generic type (Go 1.18+).Suggested fix: strip the type-parameter list before matching, or anchor on the first type identifier rather than on
). For example, parsing the receiver as( name? '*'? IDENT ('[' … ']')? )and capturingIDENT; a regex such as/\(\s*(?:[A-Za-z_]\w*\s+)?\*?\s*([A-Za-z_]\w*)/captures
Stackfor(s *Stack[T]),(s Stack[K, V]),(*Stack[T]), and the existing non-generic forms(sl *scrapeLoop)/(Type).Root cause 2 — same-file-only owner lookup (
src/extraction/tree-sitter.ts:785-790)Extraction is per file, and
this.nodesholds only the nodes of the file currently being parsed. A method whose receiver type is declared in another file of the same package (e.g.Widgetinwidget_a.go,Barinwidget_b.go) never finds anownerNode, so nocontainsedge is emitted. TheType::methodqualified name is still set, so it is purely the structural edge that is lost. This is idiomatic Go (large types routinely split methods acrossfoo.go,foo_apply.go, …). It also affects Rustimplblocks placed in separate files.Suggested fix: for
methodsAreTopLevel/ receiver-based languages, bind receiver-type → method as a resolution-phase join over the whole package rather than a same-file extraction-time lookup. After extraction, for each Go/Rust method node that has a receiver type but nocontainsparent, link it to the type node of matching name within the same package (same directory / module package). The package model added for cross-package call resolution (#388) already supplies what's needed.Impact
codegraph_nodeoutlines and in every method-set computation.callers/callees/impacton these methods lose the structural relationship to their type.goGrpcStubImplEdgesmatches hand-written impls by comparing method-name sets built fromcontainsedges; an impl whose methods span files — or any generic service — will not match.Notes
type_parameterscolumn is empty — minor, same generics-coverage theme.