Skip to content

Go: factory calls and GORM-style chains resolve to unrelated symbols #2543

Closed

Description

@BX-D

On CodeGraph 1.6.2, a Go factory → field → fluent-method chain can produce incorrect graph edges after a fresh index. A same-package factory call resolves to an unrelated struct, and chained methods resolve to another query type's same-named methods.

The reproduction below is entirely synthetic. It models the shape of GORM-generated query code and requires no GORM dependency, database, credentials, or application code.

Environment: CodeGraph 1.6.2, release commit 6560052a6f856855d3f71eee838fd66ccfa4285d; Linux x86_64; bundled Node.js 24.16.0.

Run this with an unmodified CodeGraph 1.6.2 installation:

repro_dir="$(mktemp -d)"
mkdir -p "$repro_dir/query" "$repro_dir/mysql"

cat > "$repro_dir/go.mod" <<'EOF'
module example.com/app

go 1.22
EOF

cat > "$repro_dir/query/query.go" <<'EOF'
package query

type Query struct { Session Session }
EOF

cat > "$repro_dir/query/card.go" <<'EOF'
package query

type CardDo struct{}
func (c CardDo) Where(value int) CardDo { return c }
func (c CardDo) First() int { return 1 }
EOF

cat > "$repro_dir/query/session.go" <<'EOF'
package query

type Session struct{}
type SessionDo struct{}
func (s Session) WithContext(ctx int) SessionDo { return SessionDo{} }
func (s SessionDo) Where(value int) SessionDo { return s }
func (s SessionDo) First() int { return 2 }
EOF

cat > "$repro_dir/mysql/conn.go" <<'EOF'
package mysql

import "example.com/app/query"

func Query() *query.Query { return &query.Query{} }
EOF

cat > "$repro_dir/mysql/session.go" <<'EOF'
package mysql

func Find() int {
    model := Query().Session
    return model.WithContext(0).
        Where(1).
        First()
}
EOF

codegraph init --yes "$repro_dir"
codegraph callees Find --file mysql/session.go --json --path "$repro_dir"

The relevant outgoing edges for Find, normalized from the resulting graph, are:

calls         CardDo::First       query/card.go
calls         CardDo::Where       query/card.go
instantiates  Query               query/query.go
calls         Session::WithContext query/session.go

Find calls the function mysql.Query() from mysql/conn.go, then operates on Query().Session. It never uses CardDo. The factory call should point to mysql/conn.go; Where and First should point to SessionDo if their receiver can be resolved. When the receiver cannot be established, leaving those method references unresolved is preferable to creating edges to CardDo.

These false edges affect callers, callees, impact analysis, and the relationships shown by codegraph_explore.

I traced the reproduction to two resolution paths in v1.6.2:

  1. The Go model-name heuristic in src/resolution/frameworks/go.ts can nominate an out-of-package struct for a bare call such as Query(). That heuristic can win against the real same-package function, after which the edge becomes instantiates. The Service/Store/Repository naming patterns have the same failure mode.
  2. Later links in the Go fluent chain reach name matching without their receiver type. src/resolution/name-matcher.ts then picks one of several same-named methods using proximity, producing the unrelated CardDo edges.

A conservative local candidate fix addresses these paths:

  • Restrict the type candidates offered by the Go framework heuristic for bare calls to the caller's package, preserving legitimate same-package type conversions.
  • Require receiver evidence from the current call expression before treating a bare extracted name as a collapsed fluent call, and decline ambiguous method-name matches. The check must handle long multiline arguments, comments, and Go raw strings without borrowing a receiver from a later expression.

Validation of the local candidate: 438 tests passed across 11 related suites, including 13 regression cases. The dedicated cases also passed with both the native kernel and WASM fallback. They cover the factory/type collision, single-line and long multiline chains, known typed receivers, same-package conversions (including parentheses, pointers, and comments), later same-name expressions, and raw-string arguments. The candidate removes the false method edges and restores the factory edge; it does not implement complete GORM receiver-type inference.

Related reports: #1276 and #750. This reproduction covers the factory → struct field → fluent chain shape on v1.6.2.

Activity

  1. inth3shadows commented on Oct 15, 2026

    @inth3shadows
    Contributor

    Three more receiver shapes that end the same way as the fluent chain here: the method reaches name matching as a bare name, and it binds to an unrelated same-named method. I found them while checking edges in a real 284-file Go module.

    On main at 8fe3156, built from source, Linux (WSL2), Node 22.23.2, Go 1.26, WASM extractor (CODEGRAPH_KERNEL=0). Each case is a single-file package main module that passes go vet. Command: codegraph init -i. Then calls edges from the SQLite index, as source file:line, source, target kind, target, resolvedBy. The snippets below drop blank lines and package main / func main, so the line numbers come from the full files.

    Composite-literal receiver

    type Oracle interface{ Worktree() string }
    type GitOracle struct{}
    func (GitOracle) Worktree() string { return "" }
    type FakeOracle struct{}
    func (FakeOracle) Worktree() string { return "" }
    
    func use() string { return GitOracle{}.Worktree() }
    main.go:13 use -> method FakeOracle::Worktree (exact-match)
    

    Expected: GitOracle::Worktree. The receiver's type is written at the call.

    Index-expression receiver

    type RepoCount struct{ n int }
    type Bucket struct{ n int }
    func (r RepoCount) Total() int { return r.n }
    func (b Bucket) Total() int    { return b.n }
    
    func use(out []RepoCount) int { return out[0].Total() }
    main.go:10 use -> method Bucket::Total (exact-match)
    

    Expected: RepoCount::Total, or no edge.

    Standard-library chain

    import "time"
    
    type Report struct{}
    func (r Report) Format() string { return "" }
    
    func stamp() string { return time.Now().UTC().Format(time.RFC3339) }
    main.go:10 stamp -> method Report::Format (exact-match)
    

    Expected: no edge. This is time.Time.Format, and the chain never touches a project type. With one same-named method in the project it is adopted outright.

    Where the bare name comes from. I traced the ref names and resolution sites with an instrumented build of the same source. The extractor emits bare Worktree / Total / Format. The edge comes from matchByExactName: findBestMatch for the first two (name-matcher.ts#L7991-L8003), and the single-candidate branch for Format (L7970-L7980). From reading the code:

    • Composite-literal and index-expression receivers fall through to the generic calleeName = methodName fallback in extractCall (tree-sitter.ts#L5625-L5627). Go's only receiver-keeping branches are the two-segment selector (a.b.M(), L5585-L5599) and a call receiver whose inner callee is a bare identifier (New().M()).
    • time.Now().UTC().Format takes the call-receiver branch. Its inner callee is a selector, not an identifier, so reencode is false and the same bare name comes out (L5495, L5503). This is the same path as the model.WithContext(0).Where(1) links in the original report.
    • On the resolver side, the chained-call filter only consults the receiver for names in GO_STD_METHODS (L5246-L5247). So for GitOracle{}.Worktree(), the receiver text names the owner, but FakeOracle::Worktree stays a candidate and wins the proximity tie. Format is not in that list, and of time.Time's methods the list carries only String, Unix, Before, After and Equal.

    TS/JS already handles this shape by emitting nothing for a receiver with no static type (the branch just above the fallback, L5612-L5624, from #1683 / #1707). The "decline ambiguous method-name matches" half of the candidate fix described above would cover the first two cases. The single-candidate Format case also needs the extractor or the resolver to stop treating the bare name as a free match.

    Not checked: the native kernel, and the open #1640 / #1954 branches against these files.

  2. colbymchenry commented on Oct 22, 2026

    @colbymchenry
    Owner

    Thanks for the clear reproduction, and thanks @inth3shadows for the extra receiver shapes.

    On current main, the factory call and the index-expression case were already fixed. Three things still linked to unrelated methods: the fluent chain's Where and First, the composite-literal receiver, and the standard-library chain.

    #2782 is merged to main. It works out each receiver's type link by link:

    • Where and First reach SessionDo;
    • GitOracle{}.Worktree() reaches GitOracle;
    • time.Now().UTC().Format() links to nothing.

    When a chain's type comes from outside your project or can't be worked out, it now links nothing instead of guessing. Re-index your Go projects once the next release is out.

  3. added a commit that references this issue on Oct 22, 2026
    0c3e12d
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