Summary
The Golang extractor produces no calls edges for method calls on typed receivers.
This affects typed function parameters, struct fields of any type, and chained
selector calls. All three patterns are standard in Golang service code.
Root Cause
graphify/extractors/go.py, walk_calls marks every non-package-alias receiver as is_member_call=True:
is_member_call = receiver_name not in go_imported_pkgs
graphify/symbol_resolution.py, resolve_cross_file_raw_calls drops every raw call with that flag:
if raw_call.get("is_member_call"):
continue # dropped unconditionally
No Golang receiver-type resolver exists. The extractor records is_member_call=True but no receiver_type. The resolver has nothing to match. No edge is emitted.
Reproduction
Use mindbenders (https://github.com/vkstack/mindbenders) to reproduce.
- Clone the repository.
- Run
graphify update . in the project root.
- Run
graphify query "calls from UploadProfile".
- Run
graphify query "calls from zapWrite".
- Run
graphify query "calls from Eval".
The graph shows no calls edges for any of the call sites in Affected Patterns.
Affected Patterns
Pattern A — Typed function parameter
limiter is a *redis_rate.Limiter parameter. It is not a package alias. The extractor marks limiter.Allow(...) as is_member_call=True. The edge is dropped.
// mindbenders/ratelimiter/leakybucket.go:23
func Eval(ctx context.Context, limiter *redis_rate.Limiter, ...) bool {
res, err := limiter.Allow(ctx, key, limit) // no calls edge
...
}
Pattern B — Struct field
The field type does not matter. Interface, pointer-to-struct, concrete-value, and func-typed fields all produce the same result.
// mindbenders/profiler/profiler.go:63,218 — interface field
type profiler struct{ uploader uploader.IProfileUploader }
p.uploader.UploadProfile(target, bytes.NewReader(prof.Data)) // no calls edge
// mindbenders/logging/zap.go:71,73 — pointer-to-struct field
type dlogger struct{ zap *zap.Logger }
entry := dLogger.zap.Check(zlevel, MessageKey) // no calls edge
// mindbenders/profiler/profiler.go:59,108,110 — concrete-value field
type profiler struct{ wg sync.WaitGroup }
p.wg.Add(1) // no calls edge
p.wg.Done() // no calls edge
// mindbenders/logging/logger.go:23,94 — func-typed field
type dlogger struct{ writer func(Fields, Level, string) }
dLogger.writer(fields, cb, MessageKey) // no calls edge
Pattern C — Selector chain
tree-sitter parses a.b.c() as nested selector_expression nodes. The extractor reads the outer operand as a multi-segment string ("b.Start"). That string is not in go_imported_pkgs. The call is dropped.
// mindbenders/profiler/profiler.go:210,214,216
b.Start = b.Start.Local() // receiver_name = "b.Start" → dropped
target = b.Start.Format("2006-01-02/15:04:05") + ... // receiver_name = "b.Start" → dropped
target = p.cfg.OutputDirFn(b.Start) + ... // receiver_name = "p.cfg" → dropped
Pattern D — Interface dispatch
The member-call fix (Patterns A–C) produces a calls edge to the interface method node. No implements edge connects the interface to its concrete type. The graph stops at the interface boundary.
In C#, csharp_dispatch.py walks implements edges (emitted from the explicit class Foo : IBar declaration) and adds dispatches_to edges from interface methods to concrete methods. No equivalent exists for Go.
Go has no implements keyword. The extractor must compare method sets to find which struct types implement which interfaces. If T's method set is a superset of I's method set, emit T --implements--> I. Then a go_dispatch.py resolver follows csharp_dispatch.py's pattern to emit dispatches_to edges.
// After Pattern B fix: profiler.UploadProfile --calls--> IProfileUploader.UploadProfile
// Still missing: ConcreteUploader --implements--> IProfileUploader
// Still missing: IProfileUploader.UploadProfile --dispatches_to--> ConcreteUploader.UploadProfile
Out of scope: Field accesses as values (x := a.b, val := a.b.c) are not call_expression nodes. They need a separate reads edge. They are not part of this issue.
Why It Matters
Golang service code uses interfaces and struct composition for all major component boundaries. Patterns A, B, and C cover the majority of cross-component calls in a Golang service.
In mindbenders, these edges are absent from the graph:
profiler.UploadProfile → IProfileUploader.UploadProfile
profiler.run → metrics.reset
profiler.stop → sync.WaitGroup.Wait, sync.Once.Do
dlogger.zapWrite → zap.Logger.Check
dlogger.write → dlogger.writer
Eval → redis_rate.Limiter.Allow
The call graph for a Golang service is near empty without this fix. Dependency analysis, dead-code detection, and security audits all depend on complete call edges.
Even after Pattern A–C edges land, every call through an interface field stops at the interface node. Golang service code uses interface injection at every component boundary. Navigation from a caller to a concrete implementation is not possible without implements and dispatches_to edges.
Suggested Fix
The fix follows the approach in PR #3240 (C++ parameter receiver fix).
Step 1 — Build a receiver-type table during extraction.
- From each
function_declaration: map parameter names to their declared types.
- From each
method_declaration: map the receiver to its struct type. Read the struct definition. Map each field name to its declared type.
- Map
*T and T to the same name T. Mark func-typed fields with a func marker distinct from method entries.
Result: go_receiver_type_table = { name → type_string }
Step 2 — Resolve the receiver chain during call extraction.
If is_member_call=True, split receiver_name on .:
- One segment: look it up in the table. Record
receiver_type.
- Two segments: look up segment 0 for the struct type. Look up segment 1 as a field name. Record the field type.
- Three or more: resolve each segment in order. Record the final type.
Strip * at each step.
Step 3 — Add _resolve_go_member_calls in symbol_resolution.py.
Match (receiver_type, callee_name) against the Golang method index. Emit relation="calls", confidence="INFERRED" edges. This follows the same contract as the Kotlin resolver in PR #1965.
Step 4 — Build implements edges by method-set comparison.
For each interface_type declaration, collect the method set: names and parameter counts. For each named struct_type, collect its method set. For each pair where T's method set is a superset of I's method set, emit T --implements--> I with confidence="INFERRED". This step runs after all files in the project are extracted.
Step 5 — Add go_dispatch.py, following csharp_dispatch.py.
Walk implements edges from Step 4. For each interface with exactly one implementing type, match method names. Emit IFace.Method --dispatches_to--> Concrete.Method with confidence="INFERRED". Do not emit dispatches_to edges when multiple types implement the same interface.
Implementation order: Pattern A (single-segment) → Pattern B (two-segment, one field lookup) → Pattern C (deep chains, iterative lookup) → Step 4 (implements edges, post-extraction) → Step 5 (go_dispatch.py, requires Step 4).
Related
| Reference |
Description |
| #2041 |
Canonical cross-language tracking issue for member-call gap |
| #1313 |
Golang cross-package qualified calls (pkg.Func()) — a distinct problem |
| PR #3240 |
C++ parameter receiver fix — reference implementation |
| PR #1965 |
Kotlin receiver-typed resolver — reference implementation |
| #2234 |
Same gap in Rust |
Runtime
Summary
The Golang extractor produces no
callsedges for method calls on typed receivers.This affects typed function parameters, struct fields of any type, and chained
selector calls. All three patterns are standard in Golang service code.
Root Cause
graphify/extractors/go.py,walk_callsmarks every non-package-alias receiver asis_member_call=True:graphify/symbol_resolution.py,resolve_cross_file_raw_callsdrops every raw call with that flag:No Golang receiver-type resolver exists. The extractor records
is_member_call=Truebut noreceiver_type. The resolver has nothing to match. No edge is emitted.Reproduction
Use mindbenders (https://github.com/vkstack/mindbenders) to reproduce.
graphify update .in the project root.graphify query "calls from UploadProfile".graphify query "calls from zapWrite".graphify query "calls from Eval".The graph shows no
callsedges for any of the call sites in Affected Patterns.Affected Patterns
Pattern A — Typed function parameter
limiteris a*redis_rate.Limiterparameter. It is not a package alias. The extractor markslimiter.Allow(...)asis_member_call=True. The edge is dropped.Pattern B — Struct field
The field type does not matter. Interface, pointer-to-struct, concrete-value, and func-typed fields all produce the same result.
Pattern C — Selector chain
tree-sitter parses
a.b.c()as nestedselector_expressionnodes. The extractor reads the outer operand as a multi-segment string ("b.Start"). That string is not ingo_imported_pkgs. The call is dropped.Pattern D — Interface dispatch
The member-call fix (Patterns A–C) produces a
callsedge to the interface method node. Noimplementsedge connects the interface to its concrete type. The graph stops at the interface boundary.In C#,
csharp_dispatch.pywalksimplementsedges (emitted from the explicitclass Foo : IBardeclaration) and addsdispatches_toedges from interface methods to concrete methods. No equivalent exists for Go.Go has no
implementskeyword. The extractor must compare method sets to find which struct types implement which interfaces. IfT's method set is a superset ofI's method set, emitT --implements--> I. Then ago_dispatch.pyresolver followscsharp_dispatch.py's pattern to emitdispatches_toedges.Out of scope: Field accesses as values (
x := a.b,val := a.b.c) are notcall_expressionnodes. They need a separatereadsedge. They are not part of this issue.Why It Matters
Golang service code uses interfaces and struct composition for all major component boundaries. Patterns A, B, and C cover the majority of cross-component calls in a Golang service.
In mindbenders, these edges are absent from the graph:
profiler.UploadProfile→IProfileUploader.UploadProfileprofiler.run→metrics.resetprofiler.stop→sync.WaitGroup.Wait,sync.Once.Dodlogger.zapWrite→zap.Logger.Checkdlogger.write→dlogger.writerEval→redis_rate.Limiter.AllowThe call graph for a Golang service is near empty without this fix. Dependency analysis, dead-code detection, and security audits all depend on complete call edges.
Even after Pattern A–C edges land, every call through an interface field stops at the interface node. Golang service code uses interface injection at every component boundary. Navigation from a caller to a concrete implementation is not possible without
implementsanddispatches_toedges.Suggested Fix
The fix follows the approach in PR #3240 (C++ parameter receiver fix).
Step 1 — Build a receiver-type table during extraction.
function_declaration: map parameter names to their declared types.method_declaration: map the receiver to its struct type. Read the struct definition. Map each field name to its declared type.*TandTto the same nameT. Mark func-typed fields with afuncmarker distinct from method entries.Result:
go_receiver_type_table = { name → type_string }Step 2 — Resolve the receiver chain during call extraction.
If
is_member_call=True, splitreceiver_nameon.:receiver_type.Strip
*at each step.Step 3 — Add
_resolve_go_member_callsinsymbol_resolution.py.Match
(receiver_type, callee_name)against the Golang method index. Emitrelation="calls", confidence="INFERRED"edges. This follows the same contract as the Kotlin resolver in PR #1965.Step 4 — Build
implementsedges by method-set comparison.For each
interface_typedeclaration, collect the method set: names and parameter counts. For each namedstruct_type, collect its method set. For each pair whereT's method set is a superset ofI's method set, emitT --implements--> Iwithconfidence="INFERRED". This step runs after all files in the project are extracted.Step 5 — Add
go_dispatch.py, followingcsharp_dispatch.py.Walk
implementsedges from Step 4. For each interface with exactly one implementing type, match method names. EmitIFace.Method --dispatches_to--> Concrete.Methodwithconfidence="INFERRED". Do not emitdispatches_toedges when multiple types implement the same interface.Implementation order: Pattern A (single-segment) → Pattern B (two-segment, one field lookup) → Pattern C (deep chains, iterative lookup) → Step 4 (
implementsedges, post-extraction) → Step 5 (go_dispatch.py, requires Step 4).Related
pkg.Func()) — a distinct problemRuntime