Repository navigation
Go: a call to a local closure or func parameter links to a same-named package-level function #2580
Description
Summary
Inside a Go function, a call through a local func value, either run := func(…) {…} or a parameter run func(…), gets an exact-match calls edge to a package-level func run with the same name. The local shadows the package-level function at that point, so the call cannot reach it. In a real 284-file Go module, this showed up as a false caller of a package-level helper.
#1857 / #1999 fixed the method half of this: a receiver-less Go call no longer links to a same-named method. A same-named package-level function is still accepted.
Reproduction (verified)
On main at 8fe3156, built from source, Linux (WSL2), Node 22.23.2, Go 1.26, WASM extractor (CODEGRAPH_KERNEL=0). Both files pass go vet. Each is the only .go file in its module (go.mod: module example.com/closure-shadow, go 1.22).
Local closure:
package main
func run(a, b int) int { return a + b }
func use() string {
run := func(s string) string { return s }
return run("x") // the local closure, not the package-level run
}
func main() { use() }Function parameter:
package main
func run(a, b int) int { return a + b }
func use(run func(string) string) string {
return run("x")
}
func main() { use(nil) }Command: codegraph init -i. Then calls edges from the SQLite index, excluding main, as source file:line, source, target kind, target, resolvedBy:
main.go:7 use -> function run (exact-match) # local closure
main.go:6 use -> function run (exact-match) # parameter
Expected: no calls edge from use to run(a, b int). A missing edge is fine here. The callee is a local value that the graph has no node for.
Likely cause (inferred from reading the code)
I traced the edge with an instrumented build of the same source. It is returned by matchByExactName's single-candidate branch (name-matcher.ts#L7970-L7980). The rest of this section comes from reading the code:
- For a bare Go call, the shape filter only excludes methods (
isRustGoCallTarget, L5238), so a package-level function stays a candidate. - The "a name the file binds itself shadows other symbols" veto in the same filter is JS-only (
isLocallyBoundJsName, L7957-L7959). It is also cross-file only, but here the shadowed function is in the same file. - Go already has a block-scope index that answers "is
namea parameter or local at this position":goLocalDecl, L2350-L2359. It reads:=declarations and function parameters. Today it is consulted only for the receiver of dotted refs (for example L2186 and L13381), never for a bare Go call's own name.
A Go counterpart of the JS veto would close this: drop non-method candidates when goLocalDecl(ref.referenceName, ref, context) finds a declaration in scope. It would apply in the same file too, because Go's block scoping makes the local win there as well.
Related
- Go: a call to a local function parameter is bound to a same-named method in a package the file does not import (false caller edge) #1857 (closed, fixed by fix(resolution): a receiver-less Go call never binds to a method (#1857) #1999 building on fix(resolution): a receiver-less Go call never binds to a method (#1857) #1893): the same shape, but against a same-named method.
- fix(resolution): a receiver-less JS/TS call never binds to a method #1735 / fix(resolution): a receiver-less JS/TS call never binds to a method #1759 / fix(resolution): a same-name re-binding is not a local definition #1760: the JS/TS local-binding guards this would mirror.
Not checked
- The native kernel (
CODEGRAPH_KERNELunset). - A local declared with
var run = func…orvar run func(…)rather than:=, and a closure declared in an enclosingif/forheader. - Whether the same false edge appears when the package-level function is in another file of the package. From reading the code it would, because nothing above is file-scoped.
Activity