Skip to content

feat(go): resolve receiver-typed member calls, and park them across repos - #3395

Open
xiongjianxu wants to merge 6 commits into
Graphify-Labs:v8from
xiongjianxu:feat/go-cross-repo-member-calls
Open

feat(go): resolve receiver-typed member calls, and park them across repos#3395
xiongjianxu wants to merge 6 commits into
Graphify-Labs:v8from
xiongjianxu:feat/go-cross-repo-member-calls

Conversation

@xiongjianxu

Copy link
Copy Markdown
Contributor

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_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.

It also stamps _callable / _callable_class on Go declarations. Go is the one
extractor 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 never
produces, and link_shared_type_declarations additionally requires
metadata.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's
language guard.

Design calls worth a look

  • The receiver travels as member_receiver, not receiver. In this extractor
    receiver already means "imported package name", and — more importantly — the
    Swift/Python/Ruby member-call resolvers select on is_member_call plus receiver
    without 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 the logger field only when s is the
    enclosing 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 could
    be a package or a variable whose type it cannot confirm; p.greeter.Greet() on a
    parameter stays unresolved on purpose.
  • Names are matched case-sensitively, unlike the sibling resolvers. Go exports by
    capitalisation, so Run and run on one type are two different methods with two
    different visibilities. cross_repo_calls._key already preserves case and documents
    why, so both halves agree.
  • Go does not defer. A member call reaches raw_calls only once the bare callee
    name 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_edge
    pins that the pass neither downgrades nor doubles an in-file edge.
  • confidence_score is 0.85, not 0.8. The rubric in references/extraction-spec.md
    is the discrete set {0.55, 0.65, 0.75, 0.85, 0.95}; same tier, a value the documented
    scale contains.
  • .go alone in the language guard. Kotlin's set includes .java because the JVM
    classpath 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.
  • A single type name or nothing. Greeter and *Greeter qualify; a slice, map or
    channel element is not the receiver of x.M(), and pkg.Greeter names a type no bare
    local name can stand for.

Known costs

  • g := NewGreeter() stays unresolved: typing it means reading the constructor's return
    type, which is a separate change.
  • The type table is flat per file, so first binding wins across all four sources — a
    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.
  • Framework receivers on package-level values that no local table names remain
    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, a go-struct-field arm in
test_each_language_parks_the_call_and_the_merge_finishes_it, plus a Go/Java negative
and a case-sensitivity negative.

Full suite: 5323 passed, 93 skipped, no regressions (test_ollama_retry_cap.py
deselected — it fails on v8 too). ruff clean, no added line over 100 columns.

xuxiongjian added 2 commits September 7, 2026 19:06
`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.

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 functionsgraphify/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 incorrectlygraphify/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).

Comment thread graphify/extractors/go.py
return table


def extract_go(path: Path) -> dict:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressionextract_go()

fans out to 8 callees (efferent coupling); 19 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

xuxiongjian added 2 commits September 7, 2026 20:04
…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`.

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 typesgraphify/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 typegraphify/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 emissiongraphify/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 filegraphify/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 scopesgraphify/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).

Comment thread graphify/extractors/go.py
return table


def extract_go(path: Path) -> dict:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressionextract_go()

fans out to 8 callees (efferent coupling); 19 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

@xiongjianxu

Copy link
Copy Markdown
Contributor Author

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). _callable_class is stamped by every extractor, so a Java class Greeter could answer a Go receiver typed Greeter, and — worse for this PR's purpose — its mere presence made type_defs non-empty, so the parking branch never ran and the call was silently dropped instead of handed to the merge. The index is now gated with _lang_family(...) == "go", the same interop-family map the shared cross-file call resolver uses at the candidate_family check. Pinned by test_a_type_from_another_language_never_answers_a_go_receiver, which asserts both halves: no edge, and a parked entry.

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 func (s *T) is ordinary; first-binding-wins then gave the second one the first one's type. The extractor now stamps receiver_type from the enclosing method's own declaration and the resolver prefers it over the table. Pinned by test_the_methods_own_receiver_outranks_an_earlier_binding_of_the_name, which fails on the previous commit with a Store.Get -> Server.Save edge.

On the member_receiver finding — that a language-agnostic resolver might consume it: the Swift/Python/Ruby member-call resolvers select on is_member_call plus receiver, and Go leaves receiver for the imported-package case only. That separation is why the new key exists rather than reusing receiver; putting a Go variable name in receiver is exactly what would let those resolvers bind it in a mixed corpus.

One gap this audit surfaced that is not in scope here and pre-exists on v8: extract_go mints a type node in every file that declares a method on the type, and _disambiguate_colliding_node_ids then splits the colliding ids by path. A package whose type and methods live in different files therefore has ≥2 type nodes, and every single-definition guard — this resolver's included — bails. C/C++/ObjC avoid it through _merge_decl_def_classes; Go has no equivalent. Filing that separately.

Verification: ruff check graphify tests clean; full suite 5325 passed / 93 skipped, with only the four pre-existing tests/test_ollama_retry_cap.py env failures that v8 also shows here.

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 dereferencinggraphify/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 scopesgraphify/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_tablegraphify/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_tablegraphify/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 pairtests/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).

Comment thread graphify/extractors/go.py
return table


def extract_go(path: Path) -> dict:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressionextract_go()

fans out to 8 callees (efferent coupling); 19 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

@xiongjianxu

Copy link
Copy Markdown
Contributor Author

Filed the type-node gap I mentioned above separately: #3399, with PR #3400 based on v8 (independent of this one, no ordering between them).

It matters here: this PR's resolver needs exactly one declaration of the receiver's type, and today a package that declares type Server in one file and its methods in others produces one type node per file — so the resolver bails on any package larger than a single file. #3400 folds that id-collision before disambiguation, the way _merge_decl_def_classes already does for C/C++/ObjC.

…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.
@xiongjianxu

Copy link
Copy Markdown
Contributor Author

One of the five findings was real and is fixed in d375863; the other four do not hold.

Receiver type table conflates variable names across function scopes (go.py:100) — fixed,
and it produced a wrong edge, not just a miss.
Go files reuse s, c, w, r as
parameter names in every function, so a table flat over the file answered one function's
receiver with a sibling's parameter type:

func First(g *Greeter)  {}
func Second(g *Shouter) { g.Greet() }   // before: bound to Greeter.Greet

Each function body now carries its own parameter/local table (_go_receiver_type_table run on
the function node rather than the file root), consulted before the flat one; the flat table
stays as the fallback that types a struct field. Pinned by
test_a_parameter_binds_only_inside_its_own_function.

Pointer receivers are not looked up without dereferencing (extract.py:4599) — no defect.
_go_single_type_name unwraps pointer_type before reading the identifier, and the
method_declaration path applies .lstrip("*"). func Boot(srv *Server) { srv.Close() }
resolves to Server.Close on the current branch; verified.

member_receiver_type "never falls back to the file-flat table" (go.py:549) — no defect,
twice.
The fallback lives in the resolver, not the extractor:
type_name = rc.get("receiver_type") or type_table_by_file[...].get(receiver). The field is
deliberately narrow so the stated type wins over a flat-table guess; after d375863 it also
carries the enclosing function's own scope.

_calls collapses duplicate call edges by label pair (test helper) — intentional. The
helper is a readability shim shared by the five sibling PRs; the assertions are about whether a
pair exists and which node id it names, and the cases are single-call bodies. Counting duplicates
would test the helper, not the resolver.

One thing this PR still needs to fire on idiomatic multi-file packages: #3400, which folds the
type-node copies each file declaring a method mints. Without it a package whose type and methods
are split across files produces no member-call edge at all — reproduced on this branch.

Full suite green: 5326 passed, 93 skipped; ruff clean.

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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=typegraphify/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 lookupgraphify/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 itgraphify/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).

Comment thread graphify/extractors/go.py
return table


def extract_go(path: Path) -> dict:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressionextract_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.
@xiongjianxu

Copy link
Copy Markdown
Contributor Author

Checked all three against the branch. One was a real staleness my own fix introduced and is
cleaned up in 37497b5; the other two do not hold.

File-level type_table "computed but unused" (go.py:519) — it is load-bearing, but the finding
did point at real staleness next to it.
The flat table is the fallback that types a struct
field
, which no per-function scope can see because the struct is declared outside every function
body. Proof: neutralise just the fallback expression in the resolver
(type_name = rc.get("receiver_type"), dropping the or type_table_by_file...) and exactly one
test fails —
test_a_struct_field_types_the_receiver_through_the_methods_own_receiver, i.e. a.greeter.Greet().
The other twelve pass. So the shadowing is the intended precedence, not a dead branch.

What was stale: test_the_first_binding_of_a_name_wins still described the flat table's
cross-function shadowing as the intended behaviour ("a parameter named like an earlier one has to
lose"), which is precisely what d375863 removed, and it only asserted one of the two directions.
Its fixture now pins both — Run(g *Greeter) reaches greeter.go and Also(g *Other) reaches
other.go — under the name test_a_parameter_binds_only_inside_its_own_function.

method_index keyed on the wrong edge end — no defect. The Go extractor emits
add_edge(parent_nid, method_nid, "method", ...), so source is the type and target is the
method. Every passing member-call test in the file goes through that index; had the direction been
reversed, none would resolve.

Go pointer receiver types not normalized (extract.py:4595) — no defect. Nothing with a *
ever reaches the lookup: _go_single_type_name unwraps pointer_type before reading the
identifier, and the method_declaration path applies .lstrip("*"). Verified on this branch for
both a pointer parameter and a pointer var:

func Boot(srv *Server)  { srv.Close() }        // -> Server.Close, INFERRED
func Boot2() { var srv2 *Server; srv2.Close() } // -> Server.Close, INFERRED

Full suite green: 5325 passed, 93 skipped; ruff clean.

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 fieldgraphify/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).

Comment thread graphify/extractors/go.py
return table


def extract_go(path: Path) -> dict:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressionextract_go()

fans out to 8 callees (efferent coupling); 19 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

@xiongjianxu

Copy link
Copy Markdown
Contributor Author

Checked; the finding does not hold — the receiver field is byte-identical to v8.

receiver for Go member calls (go.py:602) — unchanged, not "no longer populated". On v8 the
record already read "receiver": package_receiver, and package_receiver was already assigned
only on the not-a-member-call branch (if not is_member_call: package_receiver = receiver_name).
Diffing this branch against v8 filtered to those lines returns nothing:

$ git diff v8 -- graphify/extractors/go.py | grep -E '^[-+].*(package_receiver|"receiver")'
$          # (empty)

This PR only adds member_receiver alongside it. Keeping them apart is the point: the
Swift/Python/Ruby member-call resolvers select on is_member_call plus receiver without checking
language, so a Go variable name placed in receiver would let them bind it to their own types in
a mixed corpus.

The consumer that does read receiver/import_path for Go is the shared cross-file pass at
extract.py:7313, which filters candidates by exact _go_import_path_for_file match — package
evidence, which a variable name is not. Behaviour there is identical on both refs: the same
package-qualified fixture (import "util" + util.Helper()) produces the same edge set on
37497b5 as on v8.

Full suite green: 5325 passed, 93 skipped; ruff clean. CI green on 37497b5 (4/4).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Go member calls resolve to nothing across files, and cannot cross a repo boundary at all

1 participant