Skip to content

Go: a call to a local closure or func parameter links to a same-named package-level function #2580

Closed

Description

@inth3shadows

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 name a 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

Not checked

  • The native kernel (CODEGRAPH_KERNEL unset).
  • A local declared with var run = func… or var run func(…) rather than :=, and a closure declared in an enclosing if/for header.
  • 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions