Skip to content

fix(go): filter Go predeclared functions from call-target resolution - #2296

Open
PathGao wants to merge 2 commits into
Graphify-Labs:v8from
PathGao:fix-go-builtin-call-targets
Open

fix(go): filter Go predeclared functions from call-target resolution#2296
PathGao wants to merge 2 commits into
Graphify-Labs:v8from
PathGao:fix-go-builtin-call-targets

Conversation

@PathGao

@PathGao PathGao commented Jul 29, 2026

Copy link
Copy Markdown

Summary

_LANGUAGE_BUILTIN_GLOBALS covers JS/TS, Python and Swift (#726, #2147) but never covered Go — while graphify/extractors/go.py:358 already consults that table when resolving a callee. Since the Go resolver looks the callee up by bare name, an unexported method that happens to share a builtin's name absorbs every builtin call in the repository. This adds Go's predeclared functions to the table; no resolver logic changes.

Evidence (real corpus, 8.9k nodes / 26k edges, Go + TypeScript)

The bug was found while graphing a real project. A single method —

// internal/web/service/metric_history.go:133
func (h *metricHistory) append(metric string, t time.Time, v float64) { ... }

— collected 330 phantom inbound calls edges from that project's ordinary append(slice, x) calls, spread across the whole repo. The knock-on effect was worse than the noise: it invented twelve databaseservice edges, i.e. a reported layering violation that does not exist in the source. That is exactly the phantom-bridge shape #726/#2147 fixed for other languages.

before after
edges into builtin-named Go nodes 333 8
of those, cross-file (phantom) 325 0
databaseservice false violations 12 0
user's append method node present present (unchanged)

The 8 remaining edges are all same-file and legitimate: metricHistory --method--> .append(), plus four genuine same-file calls to that method.

Scope decision: functions yes, types no

Only the 14 predeclared functions are added (append, cap, clear, close, complex, copy, delete, imag, make, new, panic, println, real, recoverlen, min, max, print were already in the table via other languages).

Predeclared types (string, int64, byte, error, …) are deliberately left out even though Go conversions are call-shaped: they produced zero phantom edges on the corpus, and listing them would suppress genuine constructor-like calls. Evidence-driven minimum, not a blanket sweep.

Trade-off

A genuine cross-file call to a user function named exactly like a builtin is now skipped. That is the same trade-off #2147 accepted for Data/String, and the asymmetry is stark here: builtin append appears hundreds of times in any Go project, a package-level function named append cannot exist without shadowing the builtin.

Tests

tests/test_go_builtin_call_targets.py, three cases:

  1. builtin append() in one file does not bind to a same-named method in another — fails without the fix, passes with it (verified by reverting only base.py);
  2. the user's append method still exists as a node (the filter must not delete symbols);
  3. a genuine package-level cross-file call still resolves (guard is a no-op for real symbols).

Case 3 uses a package-level call on purpose: the Go resolver intentionally skips receiver method calls for lack of import evidence, so that shape would prove nothing about this filter.

Full suite: no new failures (the pre-existing reds are missing optional deps — terraform grammar, skillgen/ollama network — red on v8 too).

🤖 Generated with Claude Code

graphify/extractors/go.py already consults _LANGUAGE_BUILTIN_GLOBALS
when resolving a callee, but the table covered JS/TS, Python and Swift
(Graphify-Labs#726, Graphify-Labs#2147) and never Go. Because the Go resolver looks the callee up
by bare name, an unexported method sharing a builtin's name absorbs
every builtin call in the repository.

Measured on a real 8.9k-node Go+TS codebase: a
'func (h *metricHistory) append(...)' method collected 330 phantom
inbound calls edges from the project's ordinary append(slice, x) calls,
which in turn invented twelve database-layer -> service-layer edges —
a layering violation absent from the source. After the fix, phantom
cross-file edges to builtin-named nodes drop from 325 to zero; the 8
that remain are all same-file (struct -> method, and real calls to that
method), and the user's method node is untouched.

Builtin types are deliberately not listed. Go conversions are
call-shaped too, but they produced no phantom edges on that corpus and
listing them would suppress genuine constructor-like calls.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

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

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).

Graphify reviewed this change.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).


Graphify review — findings

This PR adds Go's predeclared function names (e.g. append, make, new, panic, copy, delete) to the _LANGUAGE_BUILTIN_GLOBALS frozenset in graphify/extractors/base.py, so the Go resolver treats them as builtins rather than binding calls to same-named user symbols. The stated intent is to prevent an unexported method sharing a builtin's name from absorbing all builtin calls as phantom inbound edges. It also adds a new test file (tests/test_go_builtin_call_targets.py) covering three cases: builtin append not binding to a user method, the user method node surviving the filter, and a genuine cross-file call still resolving. The change surface is limited to one edited set of string entries plus one new test file; note that Go builtin types were intentionally excluded from the list.

Worth a look

  • Builtin name filter is language-agnostic and shadows user symbols in other languagesgraphify/extractors/base.py:58 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Adding common Go method names to builtin globals may suppress genuine method callsgraphify/extractors/base.py:60 · 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 builtin 'len' missing from builtin globals setgraphify/extractors/base.py:60 · 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 — 1817 functions depend on the 17 node(s) this change touches.

Health — this change adds coupling hotspots:

  • worse: extract() — 346 callers, 27 callees

Verification — 1817 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: 1095 function(s) in the blast radius were not formally verified this run

· 1 more finding(s) on lines outside this diff (see the check run).

@safishamsi safishamsi left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks @PathGao — the diagnosis and test scaffolding are solid (bare append(x) phantom-binding cross-file is a real bug). But the set you added to is _LANGUAGE_BUILTIN_GLOBALS, which is shared across ~11 languages through the engine and the cross-file pass — adding append/make/copy/new/close/delete changes resolution for all of them, and it wraps the in-file EXTRACTED match too. Concretely it kills every in-file Rust Type::new() edge (new normalizes to the same token; Rust deliberately keeps its own name blocklist in the cross-file branch only), and drops genuine same-file Go h.append(v) selector calls. Please move this to a Go-local _GO_PREDECLARED_FUNCS set applied only when the callee node type is a bare identifier (mirroring Rust's language-local pattern), and gate Go raw_calls in the shared pass (raw_calls already carry language, like the bash gate). Keep the tests — happy to re-review once it's Go-scoped.

Review feedback on Graphify-Labs#2296: the previous revision added Go's predeclared
names to _LANGUAGE_BUILTIN_GLOBALS, which ~11 languages consult through
engine.py and the cross-file pass, and whose check wraps the in-file
EXTRACTED branch as well as raw_calls. Two confirmed regressions:

  * Rust normalizes 'Widget::new(3)' to the bare token 'new', so every
    in-file 'Type::new()' edge disappeared. Rust keeps its own
    _RUST_TRAIT_METHOD_BLOCKLIST, deliberately on the cross-file branch
    only — this change follows that language-local pattern.
  * Go 'h.append(v)' is a selector_expression call to a real method and
    was dropped with it. On the 3x-ui corpus this cost a genuine
    'systemMetrics.append(...)' -> '(*metricHistory).append' edge.

The filter now lives in extractors/go.py as _GO_PREDECLARED_FUNCS and
fires only when the callee node is a bare identifier, so selector calls
('h.append(v)', 'pkg.Delete(x)') and every other language are untouched.
Go raw_calls now carry language="go" and the shared pass gates on it,
mirroring the bash gate, as a backstop for Go raw_calls minted
elsewhere. The set is the Go spec's predeclared list in full: being
Go-local and bare-identifier-only makes 'len'/'max'/'min'/'print' safe
to include, and a spec boundary beats a hand-picked subset.

Remeasured on the same 466-file Go corpus:

  upstream v8      16904 edges, 334 inbound to 'append'
  previous rev     16571 edges,   1 inbound  (332 phantom gone, but the
                                              genuine selector call too)
  this rev         16572 edges,   2 inbound  (332 phantom gone, genuine
                                              call restored)

Tests kept, plus three regressions: builtin 'append' must not bind
in-file (the branch a cross-file-only gate would miss), the Go selector
call must survive, and the in-file Rust 'Type::new()' edge must survive.
The last two fail on the previous revision.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@PathGao

PathGao commented Jul 29, 2026

Copy link
Copy Markdown
Author

Thanks @safishamsi — you were right on both counts, and I reproduced them before changing anything. Pushed 60f0500.

Confirmed regressions. Against upstream/v8 both edges exist; on the previous revision both are gone:

case v8 previous rev
in-file Rust build()Widget::new(3) 1 edge 0
in-file Go record()h.append(v) 1 edge 0

The mechanism is exactly as you described: the if callee_name and callee_name not in _LANGUAGE_BUILTIN_GLOBALS guard wraps both the in-file EXTRACTED branch and the elif that feeds raw_calls, so a name in the shared set is unreachable in either. Rust's scoped_identifier handler takes the name field, which is the bare token new.

What changed.

  1. _GO_PREDECLARED_FUNCS now lives in extractors/go.py next to _GO_PREDECLARED_TYPES, mirroring _RUST_TRAIT_METHOD_BLOCKLIST. The Go block is out of _LANGUAGE_BUILTIN_GLOBALSbase.py is back to its v8 content.
  2. It fires only when func_node.type == "identifier". selector_expression callees (h.append(v), pkg.Delete(x)) never reach it. The check sits before both branches, because the in-file branch needs it too: a sibling function in the same file as the shadowing method binds through label_to_nid without ever producing a raw_call, so a cross-file-only gate would leave that edge behind.
  3. Go raw_calls now carry "language": "go", and the shared pass gates on it right after the bash gate — a backstop for Go raw_calls minted on any other path.

One judgement call to flag: the set is now the Go spec's predeclared function list in full, so it picks up len, max, min, print that the previous revision omitted. Being Go-local and bare-identifier-only is what makes that safe — those four carry the same shadowing hazard as append, and a spec boundary seemed easier to reason about than a hand-picked subset. Happy to trim it back to the original ten if you'd rather keep the delta minimal.

Remeasured on the same 466-file Go corpus (3x-ui), counting inbound edges to nodes whose label is a predeclared name:

upstream v8    16904 edges    334 inbound to `append`
previous rev   16571 edges      1 inbound   ← 332 phantom gone, genuine call gone too
this rev       16572 edges      2 inbound   ← 332 phantom gone, genuine call kept

That restored edge is the one your review predicted: systemMetrics.append(metric, ...) at internal/web/service/metric_history.go:366(*metricHistory).append. The other surviving edge is the struct→method declaration edge.

Tests. The original three are unchanged. Three more added:

  • test_builtin_append_does_not_bind_in_file — covers the branch a cross-file-only gate would miss.
  • test_go_selector_call_to_shadowing_method_survivesh.append(v) must resolve.
  • test_rust_in_file_type_new_edge_survives — the cross-language guard.

The last two fail on the previous revision and pass on this one. Full suite is green locally apart from 13 pre-existing failures from optional deps missing in my venv (tree-sitter-hcl, openai); they fail identically on the unmodified branch. ruff check 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.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).

Graphify reviewed this change.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).


Graphify review — findings

This PR adds Go's predeclared builtin functions (e.g. append, len, make, new, close, delete) as a new _GO_PREDECLARED_FUNCS set and filters them out as call targets so a bare builtin call cannot bind to a user-defined symbol that shares the name. The filtering is applied in two places: the Go extractor (extractors/go.py), which now skips bare-identifier builtin callees before recording edges or raw_calls and tags Go raw_calls with "language": "go", and the cross-file resolution pass in extract.py, which drops Go raw_calls whose callee is a predeclared func. It's deliberately Go-local and restricted to bare identifiers (not selector calls like h.append(v)), rather than being added to the shared builtin-globals set. A new test file covers cross-file and same-file shadowing scenarios plus the intentional no-op cases.

No blocking issues surfaced. 1 lower-confidence candidate did not survive cross-model review.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 1217 functions depend on the 217 functions this change touches.

Health — this change adds coupling hotspots:

  • worse: extract() — 347 callers, 27 callees

Verification — 1217 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: 1097 function(s) in the blast radius were not formally verified this run

· 1 more finding(s) on lines outside this diff (see the check run).

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.

2 participants