fix(go): attribute calls inside top-level closures to the var, not the file (#693) - #744
Merged
Merged
Conversation
…e file (#693) A function called only from an anonymous func_literal at package level — a cobra `RunE: func(){…}` handler, a goroutine literal, a callback closure stored in a `var` — had its call leak to the FILE node, because the Go var-initializer walk ran with an empty scope. So `callers`/`impact` showed the function with a file (or no meaningful) caller, unlike JS/TS where an arrow-in-const becomes a named node whose calls attribute correctly. Scope the Go top-level var/const initializer walk to the declared symbol, so a call nested in any func_literal initializer (struct field, slice/map, nested closure) attributes to the enclosing var. EXTRACTION_VERSION 3->4 (re-index to pick up the corrected attribution). Validated on cli/cli (858 Go files): node/edge counts identical, file-level dependents byte-identical (no regression), and 62 top-level-closure calls correctly moved from file-attributed to var-attributed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
bompus
pushed a commit
to bompus/codegraph
that referenced
this pull request
Sep 5, 2026
…lbymchenry#693 for Kotlin) The Kotlin property hook consumes the whole `property_declaration` subtree, so the dispatcher only scanned it for function-as-value candidates and never walked it for calls. Every call inside a property initializer was therefore dropped from the graph entirely — not misattributed, gone: private val fieldLambda: () -> Unit = { target() } // no caller private val samField = Runnable { target() } // no caller private val plain = compute() // no caller private val delegated by lazy { compute() } // no caller That is exactly how Android/MSDK callbacks are declared, so anything reached only through such a callback looked like it had no callers and its blast radius came back far too small. Fix: after minting the property node, walk its RHS — the named child after the `=` token plus a `property_delegate` — through visitFunctionBody with the property pushed on the scope stack. This is Go's colbymchenry#693/colbymchenry#744 fix ported; tree-sitter-kotlin exposes no fields at all, hence the `=` anchor instead of Go's `child_by_field_name("value")`. Not touched, by design: the `scope == "local"` early return, the hook-declined destructuring branch, and the declaration's own children (modifiers, `val`/`var`, name+type, extension receiver, `getter`/`setter`). Both arms move together — the Rust kernel and the TS extractor — so kernel-kotlin-parity stays byte-identical; the torture fixture gains the lambda/SAM/anonymous-object initializer shapes. Measured on a 113-file Android/Kotlin app: strictly additive, 0 lost nodes / edges / refs, +7 nodes (anonymous-object overrides that were invisible), +109 edges (+47 calls, +35 instantiates, +20 references), and the real case that motivated this — a `CameraFrameListener` field — now shows up as a caller of the method it invokes.
bompus
pushed a commit
to bompus/codegraph
that referenced
this pull request
Sep 5, 2026
…ry#693 for Java) `extractField` minted the field node and stopped; the dispatcher then only scanned the `field_declaration` subtree for function-as-value candidates. So a field initializer's code was never walked: private final Runnable fieldLambda = () -> target(); // no caller private final Runnable l = new LocationListener() { … }; // invisible private final int eager = compute(); // no caller The anonymous-class form lost more than edges — the class and its overrides were never extracted at all, which is how `Parcelable.Creator` and every Android listener field is written. Fix: walk the declarator's `value` through visitFunctionBody with the field pushed on the scope stack — Go's colbymchenry#693/colbymchenry#744 fix, and the same shape as the TS/JS class-field walk already sitting in the methodTypes branch (colbymchenry#808). `extractField` is shared, but the walk is keyed on the `value` FIELD, which only Java's `variable_declarator` carries: C# (bare child), VB.NET (`initializer`) and PHP (`default_value`, separate branch) are untouched and stay for their own turn. Kernel and TS extractor move together, so kernel parity stays byte-identical. Measured on the 409-file DJI MSDK v5 UX SDK (Java): strictly additive, 0 lost nodes / edges / refs, +21 nodes (anonymous listener and Creator classes with their overrides), +21 edges, +651 refs, 409/409 files still byte-parity between the two arms.
bompus
pushed a commit
to bompus/codegraph
that referenced
this pull request
Sep 5, 2026
…ymchenry#693 for TS/JS) Two defects in one place — extractVariable's JS-family branch. 1. The initializer walk ran with only the FILE on the node stack, so `const cfg = loadConfig()` recorded the FILE as loadConfig's caller. That is literally the leak colbymchenry#693 described and colbymchenry#744 fixed for Go; the closing note there said "JS/TS didn't have this gap", which holds only for the `const f = () => …` shape (an arrow value delegates to extractFunction and gets its own node). 2. Object literals were excluded from the walk outright, on the grounds that their function-valued members are extracted individually below — but that only happens for EXPORTED consts. `const obj = { handler: () => target() }` therefore contributed nothing at all: no member node, and no call edge to anything. Fix: walk the initializer with the declared symbol pushed on the stack, and skip only the shapes whose members really are extracted one-by-one (exported object-of-functions, RTK endpoints, Pinia setup, Vue store collections) — walking those too would double-count each member arrow's calls. Two follow-on repairs the change surfaced: - CFML `<cfscript>` bodies are delegated to a separate extractor as if they were a whole module, so a `var x = helper()` local inside a `<cffunction>` minted a top-level variable node and the walk attributed `helper` to it. Those snippet-top-level non-callables are locals of the enclosing function; their refs now redirect to it, like the snippet's file-attributed refs already did. - codegraph_explore stopped listing a synthesized dynamic-dispatch link once the same pair also had a static call edge: it asked getCallers/getCallees, which return one row per NEIGHBOUR (the colbymchenry#1086 de-dup), so the static edge hid the heuristic one. It now asks the edges directly. This is exactly the RTK thunk case — `dispatch(innerThunk(n))` inside a thunk initializer is now a real static edge, which is the point, but the synthesized hop must still show up in the summary. Measured on three independent TypeScript trees (codegraph's own src/, evcc-ng, gv-grx — 499 files): 0 lost nodes/edges/refs, 780 refs re-attributed from the file node to the declaring constant, 228 genuinely new refs from object literals, node and edge counts unchanged. Kernel parity holds (324/324 non-deferred files byte-identical).
bompus
pushed a commit
to bompus/codegraph
that referenced
this pull request
Sep 5, 2026
…olbymchenry#693 for Scala) The val/var hook minted the node and returned true, so the dispatcher only scanned the subtree for function-as-value candidates and the initializer's code was never walked: val fieldLambda: () => Unit = () => target() // no caller val direct = target() // no caller lazy val lazily = compute() // no caller val anon = new Runnable { def run() = target() } // no caller Scala puts almost everything in a val, so this is not an edge case: on a 32-file SpinalHDL project the graph was missing 812 references. Fix: walk the `value` field through visitFunctionBody with the declared symbol pushed on the scope stack — the same shape as Go's colbymchenry#693/colbymchenry#744 fix, which the grammar supports directly here (`val_definition` exposes `value`). Kernel and TS extractor move together; kernel parity holds. Measured on that SpinalHDL project: 0 lost refs, 0 re-attributed, +812 new, node and edge counts unchanged (824/792), 32/32 files byte-parity between the two arms.
bompus
pushed a commit
to bompus/codegraph
that referenced
this pull request
Sep 5, 2026
…he name (colbymchenry#693 for Python) The assignment branch minted the node and stopped, so every call on the right-hand side was missing from the graph: APP = compute() # no caller handler = lambda: target() # no caller MAPPING = {"a": compute()} # no caller first, second = compute(), f() # no caller, and no symbol either That is everything a module wires up at import time — `app = FastAPI()`, `ENGINE = create_engine(url)`, `router = build_router()`, handler registries — so whatever those build looked unreferenced. Fix: walk the `right` field through visitFunctionBody with the assigned name pushed on the scope stack. A tuple target mints no symbol, so its RHS is walked at the enclosing scope rather than lost. Gated to Python — Ruby shares this branch and gets its own turn. Class attributes are untouched: they never reach this branch (the dispatcher's class-scope gate excludes them), so their calls keep riding the class node. Giving them symbols of their own is a separate question — there are no nodes for them today at all. A function-as-value in an initializer (`REGISTRY = {"org": Serializer}`) now produces a reference from BOTH the assigned name and the file node, because the dispatcher's own scan runs either way. That is the shape Go's colbymchenry#744 already ships — verified against the Go arm on the same input — so the two languages stay consistent; the function-ref test's expectation is updated to match. Measured on three Python trees (libresdr, ADRC-betaflight, bot_predlogka_big_project — 799 files): 0 lost refs, 0 re-attributed, +3057 new; node and edge counts unchanged. Kernel parity holds — 6221/6221 files byte-identical across the sweep.
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #693.
Problem
A function called only from inside an anonymous closure at package level — a cobra
RunE: func(...) {...}handler, a goroutine literal, or a callback closure stored in avar— showed up with no meaningful caller. The call leaked to the file node, socodegraph_callers/codegraph_impactcouldn't answer "what callsWire?".Root cause: Go's top-level var-initializer walk ran with an empty scope, so a
Wire()call insidevar rootCmd = &cobra.Command{ RunE: func(){ Wire() } }attributed to the file. This is a Go-specific gap — JS/TS already turn an arrow-in-const (const handler = () => {…}) and object-arrow methods into named nodes whose calls attribute correctly.Fix
Scope the Go top-level
var/constinitializer walk to the declared symbol, so a call nested in anyfunc_literalinitializer — struct field (RunE:), slice/map element, or nested closure — attributes to the enclosing var instead of the file.callers(Wire)now returnsrootCmd(variable).EXTRACTION_VERSION3→4 so existing Go indexes get a re-index hint.Validation
callers(Wire)→rootCmdafter the fix. Covered every closure shape (struct-field, plain var, function-arg, nested, in-function control) — top-level ones now attribute to a var, in-function ones still to the function.extractVariablechange).🤖 Generated with Claude Code