feat(go): resolve receiver-typed member calls, and park them across repos - #3395
feat(go): resolve receiver-typed member calls, and park them across repos#3395xiongjianxu wants to merge 6 commits into
Conversation
`g.Greet()` where `Greeter` is declared in another file produced no edge. The
shared cross-file pass skips member calls, and the Go extractor read the
receiver's identifier only to discard it for anything that was not an imported
package — so the one shape Go dependency injection actually takes (a struct
field used from a method) contributed nothing to the call graph.
`extract_go` now exports a per-file `go_type_table` built from the four places a
Go receiver's type is written down: a struct field, a parameter (which covers the
method receiver `func (s *Server)`), `var x T`, and `x := T{}` / `x := &T{}`.
`_resolve_go_member_calls` looks the receiver up there, takes the single
declaration of that type and emits the `calls` edge to its method.
Also stamps `_callable` / `_callable_class` on Go declarations (Graphify-Labs#2438). Go is the
one extractor outside the tree-sitter engine, so its nodes carried neither, which
left them invisible to every consumer that reads the markers off the node.
Design notes:
- The receiver travels as `member_receiver`, not `receiver`: in this extractor
`receiver` means "imported package", and the Swift/Python/Ruby member-call
resolvers select on `is_member_call` plus `receiver` without checking the
language, so a Go name there binds to their types in a mixed corpus.
- `s.logger.Log()` is read as a call on the `logger` field only when `s` is the
enclosing method's own receiver. The table is flat and file-scoped, so any
other head could be a package or a variable whose type it cannot confirm.
- Names are matched case-sensitively, unlike the sibling resolvers. Go exports by
capitalisation, so `Run` and `run` on one type are two different methods.
- Go does not defer: a member call reaches `raw_calls` only once the bare callee
name misses in the caller's file, so in-file behaviour is byte-identical.
- Two packages declaring the same name stay two type nodes and the
single-definition guard bails — without import evidence neither is the answer.
…raphify-Labs#3152) The Go resolver had the receiver's type in hand and dropped the call when nothing in the corpus declared it, so `graph.json` — the only artifact `merge-graphs` and `global add` read — recorded nothing and no merge-time pass could recover it. A merged graph of a Go service and a Go library was missing exactly the edges that make it a call graph. Parks the pair on the caller node like the Java/C++/C#/Swift resolvers already do, and adds `"go": {".go"}` to the merge pass's language guard. `.go` alone: Go and Java both turn up in one backend merge, and a shared type name across them is a coincidence, not a namespace. The `_key` case preservation the merge pass already documents is what makes this correct for Go — `greet` is a different method from `Greet` and is not reachable from another package at all.
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 2 advisory finding(s) below merit a look before merge.
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
Adds Go cross-file member-call resolution so that g.Greet() binds to the method on the receiver's declared type instead of dropping. A new per-file _go_receiver_type_table records name -> TypeName for struct fields, parameters (including method receivers), var x T, and x := T{} bindings, and _resolve_go_member_calls looks the receiver up there, requiring a single type definition and a single matching method (matched case-sensitively to keep exported/unexported names distinct) before emitting an INFERRED calls edge at 0.85; ambiguous types bail. Receivers typed to a type declared nowhere in the corpus are parked on the caller for a later merged-graph pass (#3152), and .go files now participate in the cross-repo call suffix map.
Worth a look
- Go receiver type table conflates same variable name across functions —
graphify/extractors/go.py:116· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- raw_calls gains member_receiver key that language-agnostic resolvers may bind incorrectly —
graphify/extractors/go.py:594· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 1935 functions depend on the 325 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 546 callers, 43 callees - new:
_rebuild_code()— 115 callers, 51 callees - new:
extract_js()— 85 callers, 4 callees - new:
extract_xaml()— 19 callers, 17 callees - new:
dispatch_command()— 2 callers, 124 callees - new:
_get_extractor()— 26 callers, 6 callees - new:
extract_go()— 19 callers, 8 callees - new:
link_cross_repo_member_calls()— 20 callers, 7 callees - …and 36 more — each is listed as a finding
Verification — 1935 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 1770 function(s) in the blast radius were not formally verified this run
Formal verification
Could not verify: Could not verify extract\_go.
The verifier did not have enough to check extract\_go, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set
· 1 grounded finding(s) anchored inline below; 43 more finding(s) on lines outside this diff (see the check run).
| return table | ||
|
|
||
|
|
||
| def extract_go(path: Path) -> dict: |
There was a problem hiding this comment.
extract_go()
fans out to 8 callees (efferent coupling); 19 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
…n receiver A corpus-wide `_callable_class` lookup let a same-named class in another language answer a Go receiver, and it hid from the parking branch that no Go file declares the type. The flat per-file table also mistyped a method's own receiver when an earlier binding of the same name won, so `s.Save()` inside a `Store` method could reach `Server.Save`.
…ared resolver does
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
Adds Go receiver-typed member-call resolution so a method call like g.Greet() links to the method declared on the receiver's type even when that declaration lives in another file. During extraction, _go_receiver_type_table and _go_single_type_name build a file-flat name -> TypeName table from struct fields, parameters (including the method receiver), var and := bindings, and each function body now carries its receiver name and type; nodes get stamped _callable/_callable_class for the resolver to read. The new tail-registry _resolve_go_member_calls pass looks the receiver's type up (case-sensitively, preferring the enclosing method's own receiver type over the flat table), emits an INFERRED calls edge at 0.85 only when the type resolves to a single .go declaration with a single matching method, and parks unresolved receivers on the caller for a later merge; ambiguous type or method matches bail rather than guess.
Worth a look
- Go member resolver ignores package scope for bare receiver types —
graphify/extract.py:4584· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Go pointer-typed receivers never match their declared type —
graphify/extract.py:4596· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Existing non-call edges suppress Go call emission —
graphify/extract.py:4607· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- parameter_declaration binding can retype a struct field's calls via first-binding-wins across the whole file —
graphify/extractors/go.py:105· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Go receiver type table conflates same variable name across scopes —
graphify/extractors/go.py:113· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 1937 functions depend on the 327 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 546 callers, 43 callees - new:
_rebuild_code()— 115 callers, 51 callees - new:
extract_js()— 85 callers, 4 callees - new:
extract_xaml()— 19 callers, 17 callees - new:
dispatch_command()— 2 callers, 124 callees - new:
_get_extractor()— 26 callers, 6 callees - new:
extract_go()— 19 callers, 8 callees - new:
link_cross_repo_member_calls()— 20 callers, 7 callees - …and 36 more — each is listed as a finding
Verification — 1937 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 1772 function(s) in the blast radius were not formally verified this run
Formal verification
Could not verify: Could not verify extract\_go.
The verifier did not have enough to check extract\_go, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set
· 1 grounded finding(s) anchored inline below; 43 more finding(s) on lines outside this diff (see the check run).
| return table | ||
|
|
||
|
|
||
| def extract_go(path: Path) -> dict: |
There was a problem hiding this comment.
extract_go()
fans out to 8 callees (efferent coupling); 19 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
|
Self-audit pass over the bot review, plus one thing the review did not catch. Two fixes pushed: 1. The declaration index was corpus-wide (the review's high-severity finding). 2. The flat per-file table could mistype a method's own receiver. Go names receivers with one letter, so two types in one file both writing On the One gap this audit surfaced that is not in scope here and pre-exists on Verification: |
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
Adds cross-file Go member-call resolution: g.Greet() now resolves through the receiver's declared type via a new per-file go_type_table (struct fields, parameters/receivers, var, and := composite bindings), emitting an INFERRED calls edge only when the type has a single local declaration and the method a single target. Matches Go names case-sensitively so Run/run stay distinct, prefers the enclosing method's own receiver type over the flat table, and registers go as a cross-repo language so a receiver typed to a type declared nowhere in the corpus is parked for a later merged graph instead of dropped.
Worth a look
- Go pointer receiver types are looked up without dereferencing —
graphify/extract.py:4599· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Go receiver type table conflates same variable names across function scopes —
graphify/extractors/go.py:100· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- member_receiver_type never falls back to file-flat type_table —
graphify/extractors/go.py:549· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- member_receiver_type only set for own-receiver, never resolved from flat type_table —
graphify/extractors/go.py:549· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- _calls collapses duplicate call edges by label pair —
tests/test_go_receiver_member_calls.py:31· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 1937 functions depend on the 327 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 546 callers, 43 callees - new:
_rebuild_code()— 115 callers, 51 callees - new:
extract_js()— 85 callers, 4 callees - new:
extract_xaml()— 19 callers, 17 callees - new:
dispatch_command()— 2 callers, 124 callees - new:
_get_extractor()— 26 callers, 6 callees - new:
extract_go()— 19 callers, 8 callees - new:
link_cross_repo_member_calls()— 20 callers, 7 callees - …and 36 more — each is listed as a finding
Verification — 1937 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 1772 function(s) in the blast radius were not formally verified this run
Formal verification
Could not verify: Could not verify extract\_go.
The verifier did not have enough to check extract\_go, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set
· 1 grounded finding(s) anchored inline below; 43 more finding(s) on lines outside this diff (see the check run).
| return table | ||
|
|
||
|
|
||
| def extract_go(path: Path) -> dict: |
There was a problem hiding this comment.
extract_go()
fans out to 8 callees (efferent coupling); 19 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
|
Filed the type-node gap I mentioned above separately: #3399, with PR #3400 based on It matters here: this PR's resolver needs exactly one declaration of the receiver's type, and today a package that declares |
…t table Parameter names repeat across a Go file, so the file-flat table answered one function's receiver with a sibling function's parameter type: `Second(g *Shouter)` calling `g.Greet()` bound to Greeter.Greet. Each function body now carries its own parameter/local table, consulted before the flat one, which keeps serving fields.
|
One of the five findings was real and is fixed in Receiver type table conflates variable names across function scopes (go.py:100) — fixed, func First(g *Greeter) {}
func Second(g *Shouter) { g.Greet() } // before: bound to Greeter.GreetEach function body now carries its own parameter/local table ( Pointer receivers are not looked up without dereferencing (extract.py:4599) — no defect.
One thing this PR still needs to fire on idiomatic multi-file packages: #3400, which folds the Full suite green: 5326 passed, 93 skipped; ruff clean. |
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 3 advisory finding(s) below merit a look before merge.
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
Adds Go receiver-typed member-call resolution so a method call like g.Greet() whose type is declared in another file gets a calls edge: _resolve_go_member_calls looks the receiver up in a per-file go_type_table (built by _go_receiver_type_table from struct fields, parameters/receivers, var x T, and x := T{} bindings), takes the single Go declaration of that type, and emits an INFERRED edge only when both the type and the target method are unambiguous, matching names case-sensitively. Registers Go with the cross-repo call machinery and parks receivers typed to a type this build declares nowhere for a later merged graph to finish (#3152); anything ambiguous or type-table-missed is left unresolved rather than guessed.
Worth a look
- method_index keyed on (edge.source, method_name) but 'method' edge direction may be target=type not source=type —
graphify/extract.py· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Go pointer receiver types are not normalized before type lookup —
graphify/extract.py:4595· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- file-level type_table computed but unused, and per-function scope shadows it —
graphify/extractors/go.py:519· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 1938 functions depend on the 328 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 546 callers, 43 callees - new:
_rebuild_code()— 115 callers, 51 callees - new:
extract_js()— 85 callers, 4 callees - new:
extract_xaml()— 19 callers, 17 callees - new:
dispatch_command()— 2 callers, 124 callees - new:
_get_extractor()— 26 callers, 6 callees - new:
extract_go()— 19 callers, 8 callees - new:
link_cross_repo_member_calls()— 20 callers, 7 callees - …and 36 more — each is listed as a finding
Verification — 1938 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 1773 function(s) in the blast radius were not formally verified this run
Formal verification
Could not verify: Could not verify extract\_go.
The verifier did not have enough to check extract\_go, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set
· 1 grounded finding(s) anchored inline below; 43 more finding(s) on lines outside this diff (see the check run).
| return table | ||
|
|
||
|
|
||
| def extract_go(path: Path) -> dict: |
There was a problem hiding this comment.
extract_go()
fans out to 8 callees (efferent coupling); 19 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
test_the_first_binding_of_a_name_wins asserted the flat table's cross-function shadowing as intended behaviour, which the scoping fix removed; its fixture now pins that each function's parameter answers only its own calls.
|
Checked all three against the branch. One was a real staleness my own fix introduced and is File-level What was stale:
Go pointer receiver types not normalized (extract.py:4595) — no defect. Nothing with a func Boot(srv *Server) { srv.Close() } // -> Server.Close, INFERRED
func Boot2() { var srv2 *Server; srv2.Close() } // -> Server.Close, INFERREDFull suite green: 5325 passed, 93 skipped; ruff clean. |
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 1 advisory finding(s) below merit a look before merge.
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
Adds cross-file resolution of Go receiver-typed member calls (g.Greet()) where the method is declared in another file, which the shared cross-file pass skips. _resolve_go_member_calls looks the receiver's declared type up in a per-file go_type_table (built by _go_receiver_type_table from struct fields, parameters/receivers, var x T, and x := T{} bindings), matches case-sensitively so exported and unexported methods stay distinct, and emits an INFERRED calls edge (score 0.85) only when the type resolves to a single local Go declaration with a single matching method; ambiguous or same-named non-Go declarations bail. Calls to a type declared nowhere in the corpus are parked on the caller for a later merge to finish (#3152), and go is now registered as a cross-repo call language.
Worth a look
- Go member call records no longer populate the existing receiver field —
graphify/extractors/go.py:602· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 1937 functions depend on the 327 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 546 callers, 43 callees - new:
_rebuild_code()— 115 callers, 51 callees - new:
extract_js()— 85 callers, 4 callees - new:
extract_xaml()— 19 callers, 17 callees - new:
dispatch_command()— 2 callers, 124 callees - new:
_get_extractor()— 26 callers, 6 callees - new:
extract_go()— 19 callers, 8 callees - new:
link_cross_repo_member_calls()— 20 callers, 7 callees - …and 36 more — each is listed as a finding
Verification — 1937 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 1772 function(s) in the blast radius were not formally verified this run
Formal verification
Could not verify: Could not verify extract\_go.
The verifier did not have enough to check extract\_go, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set
· 1 grounded finding(s) anchored inline below; 43 more finding(s) on lines outside this diff (see the check run).
| return table | ||
|
|
||
|
|
||
| def extract_go(path: Path) -> dict: |
There was a problem hiding this comment.
extract_go()
fans out to 8 callees (efferent coupling); 19 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
|
Checked; the finding does not hold — the
This PR only adds The consumer that does read Full suite green: 5325 passed, 93 skipped; ruff clean. CI green on |
Fixes #3393. Follows the same two-commit shape as #3385 (ObjC), #3387 (TS/JS),
#3389 (Kotlin) and #3391 (PHP): the single-repo half first, then the park.
Commit 1 — resolve Go member calls through the receiver's declared type
extract_gonow exports a per-filego_type_tablebuilt from the four places a Goreceiver's type is written down: a struct field, a parameter (which covers the method
receiver
func (s *Server)),var x T, andx := T{}/x := &T{}._resolve_go_member_callslooks the receiver up there, takes the single declaration ofthat type, and emits the
callsedge to its method.It also stamps
_callable/_callable_classon Go declarations. Go is the oneextractor outside the tree-sitter engine, so its nodes carried neither marker and were
invisible to every consumer that reads them off the node. Scoped check: the
indirect-call guard only consults them for
rc["indirect"]entries, which Go neverproduces, and
link_shared_type_declarationsadditionally requiresmetadata.namespace, which Go never sets — so nothing existing changes shape.Commit 2 — park the call when the type is in another repo
Parks the pair on the caller node and adds
"go": {".go"}to the merge pass'slanguage guard.
Design calls worth a look
member_receiver, notreceiver. In this extractorreceiveralready means "imported package name", and — more importantly — theSwift/Python/Ruby member-call resolvers select on
is_member_callplusreceiverwithout checking the language. A Go name placed there would bind to their types in
a mixed corpus. A key of its own is the precise fix; widening those three resolvers'
gates is a separate change.
s.logger.Log()is read as a call on theloggerfield only whensis theenclosing method's own receiver. This is the Go analogue of PHP's
$this->prop->method(). The table is flat and file-scoped, so any other head couldbe a package or a variable whose type it cannot confirm;
p.greeter.Greet()on aparameter stays unresolved on purpose.
capitalisation, so
Runandrunon one type are two different methods with twodifferent visibilities.
cross_repo_calls._keyalready preserves case and documentswhy, so both halves agree.
raw_callsonly once the bare calleename misses in the caller's file, which is already the existing behaviour, so in-file
resolution is byte-identical.
test_a_same_file_method_call_keeps_its_extracted_edgepins that the pass neither downgrades nor doubles an in-file edge.
confidence_scoreis 0.85, not 0.8. The rubric inreferences/extraction-spec.mdis the discrete set {0.55, 0.65, 0.75, 0.85, 0.95}; same tier, a value the documented
scale contains.
.goalone in the language guard. Kotlin's set includes.javabecause the JVMclasspath is one namespace; Go and Java share nothing, and a matching type name across
a Go service and a Java service in one merge is a coincidence.
Greeterand*Greeterqualify; a slice, map orchannel element is not the receiver of
x.M(), andpkg.Greeternames a type no barelocal name can stand for.
Known costs
g := NewGreeter()stays unresolved: typing it means reading the constructor's returntype, which is a separate change.
parameter named like a field in another function does not retype the field's calls.
Pinned by
test_the_first_binding_of_a_name_wins.unresolved, as before.
Tests
tests/test_go_receiver_member_calls.py, 11 new cases — 5 positive shapes, 5 negatives(constructor return, chain not rooted at the method's receiver, package-qualified type,
two packages declaring one name, first-binding-wins) and one that pins in-file
behaviour. 6 of them fail on
v8. On the merge side, ago-struct-fieldarm intest_each_language_parks_the_call_and_the_merge_finishes_it, plus a Go/Java negativeand a case-sensitivity negative.
Full suite: 5323 passed, 93 skipped, no regressions (
test_ollama_retry_cap.pydeselected — it fails on
v8too).ruffclean, no added line over 100 columns.