Repository navigation
Go: factory calls and GORM-style chains resolve to unrelated symbols #2543
Description
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:
- The Go model-name heuristic in
src/resolution/frameworks/go.tscan nominate an out-of-package struct for a bare call such asQuery(). That heuristic can win against the real same-package function, after which the edge becomesinstantiates. The Service/Store/Repository naming patterns have the same failure mode. - Later links in the Go fluent chain reach name matching without their receiver type.
src/resolution/name-matcher.tsthen picks one of several same-named methods using proximity, producing the unrelatedCardDoedges.
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
inth3shadows commentedon Oct 15, 2026
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
mainat 8fe3156, built from source, Linux (WSL2), Node 22.23.2, Go 1.26, WASM extractor (CODEGRAPH_KERNEL=0). Each case is a single-filepackage mainmodule that passesgo vet. Command:codegraph init -i. Thencallsedges from the SQLite index, assource file:line, source, target kind, target, resolvedBy. The snippets below drop blank lines andpackage main/func main, so the line numbers come from the full files.Composite-literal receiver
Expected:
GitOracle::Worktree. The receiver's type is written at the call.Index-expression receiver
Expected:
RepoCount::Total, or no edge.Standard-library chain
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 frommatchByExactName:findBestMatchfor the first two (name-matcher.ts#L7991-L8003), and the single-candidate branch forFormat(L7970-L7980). From reading the code:calleeName = methodNamefallback inextractCall(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().Formattakes the call-receiver branch. Its inner callee is a selector, not an identifier, soreencodeis false and the same bare name comes out (L5495, L5503). This is the same path as themodel.WithContext(0).Where(1)links in the original report.GO_STD_METHODS(L5246-L5247). So forGitOracle{}.Worktree(), the receiver text names the owner, butFakeOracle::Worktreestays a candidate and wins the proximity tie.Formatis not in that list, and oftime.Time's methods the list carries onlyString,Unix,Before,AfterandEqual.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
Formatcase 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.
colbymchenry commentedon Oct 22, 2026
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'sWhereandFirst, the composite-literal receiver, and the standard-library chain.#2782 is merged to
main. It works out each receiver's type link by link:WhereandFirstreachSessionDo;GitOracle{}.Worktree()reachesGitOracle;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.
fix(go): a call further along a chain links the method of the type th…