Skip to content

fix(go): a call further along a chain links the method of the type the chain reaches - #2782

Merged
colbymchenry merged 1 commit into
mainfrom
claude/issue-2543-go-factory-chains
Oct 22, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/issue-2543-go-factory-chains

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Summary

In Go, a method called on a receiver the extractor keeps no name for now links to the method of the type that receiver has, or to nothing. That covers a fluent chain's later links (model.WithContext(0).Where(1).First()), a call on what a function or method returns (time.Now().UTC().Format(…), query.Use(db).User.Where(…)), a composite literal (GitOracle{}.Worktree()), a conversion ((*store.Response)(nil).Header()) and a field reached further down a chain. Before, these calls arrived by their bare method name and name matching picked a project method of that name: the issue's GORM-style Where and First went to another query type, a standard-library chain's Format to the one project Format, and a literal's method to a namesake type.

Fixes #2543.

Reproduction (issue script, main c326b6be vs this branch)

call in Find main this branch
Query() mysql/conn.go function Query (already fixed on main) same
.Where(1) CardDo::Where (query/card.go) SessionDo::Where (query/session.go)
.First() CardDo::First (query/card.go) SessionDo::First (query/session.go)

The three shapes in @inth3shadows's comment:

shape main this branch
GitOracle{}.Worktree() FakeOracle::Worktree GitOracle::Worktree
out[0].Total() RepoCount::Total (fixed by #2755) same
time.Now().UTC().Format(…) Report::Format no edge

Root cause

The extractor keeps a Go call's receiver only for x.M(), a.b.M() and F().M() with an identifier F. Every other link of a chain reaches the resolver as the bare method name, at the column where its chain starts, and the name matcher then chose among all same-named methods by proximity, or took the only one outright.

Fix

A new Go step in resolveOneInner, after the type-assertion and indexed-element steps, reads the chain at the ref's column in the file's blanked source and types the receiver link by link (matchGoChainedCall in name-matcher.ts):

  • a call gives its function's or method's result: the first, or the one a multi-value assignment takes (_, txn := store.Begin()), with a package's declarations required to agree;
  • a conversion T(x) / (*pkg.T)(x) and a composite literal T{…} / pkg.T{…} / List[int]{…} give their type, and a generic function called with its type arguments (validate.PtrMapNoNils[string, string](…), sets.New[string](…)) gives its result;
  • a result written with the function's type parameters keeps them as parameters, read from the declaration since the indexed signature leaves [T any] out: T is no package's type (a namesake type T is not linked), V Validator has its constraint's methods, Set[T] is a Set. Reading a one-line generic function's type parameters also needed the helper to include its only line;
  • an embedded field is selectable by its type's name (client.KV.Get), and a package-level variable is typed from the call it is set to (var localScheme = scheme.New());
  • the method is then the one Go selects on that type, own or promoted.

A chain whose type comes from outside the project links nothing, and so does one whose type can't be read (a func value's call, a result written with type parameters, an anonymous struct, a function-local type): guessing by the name is what produced the wrong edges. When a chain calls the same name twice, the bare ref is the later call (the first, if the extractor keeps it, arrives under its own name).

The change is resolver-only and shared by both extractors; the new tests pass with CODEGRAPH_KERNEL=0 and with the native kernel.

Validation

New __tests__/go-chained-receivers.test.ts (8 cases: the issue's chain, composite literals, standard-library chains, call/multi-value/conversion/embedded-field typing, a package-level variable, generic calls and literals, a repeated name in one chain, receivers that can't be read). 7 of the 8 fail on main; the repeated-name case passes there by name matching and fails under a first-link rule. Mutating the last-link rule, the type-argument handling, the result's type parameters or the one-line head each fails its own case. The 43 Go test files pass (463 tests).

A/B, main c326b6be vs this change (no Go, JS or Python indexing change between c326b6be and the branch base 88ebefa1, whose new commits are upgrade and Pascal fixes):

repo edges removed / added same edge, new metadata retargeted dropped added
gin 0f09c3a9 80 / 72 14 50 16 8
etcd bf9cadf0 1,726 / 1,327 479 728 519 120
prometheus 017fcf2e 2,249 / 1,265 568 671 1,010 26
sonic 53db5562 (gorm/gen dal/) 910 / 877 116 734 60 27
kubernetes 006cdf87 88,288 / 84,331 22,575 53,098 12,615 8,658
express 0dee2c52 (control) 0 / 0, dump byte-identical
flask d086db85 (control) 0 / 0, dump byte-identical

Nodes are identical in every repo. Every removed edge was a name-match guess (exact-match) except 173 in kubernetes: 171 are the same edge now owned by another ref of its chain (a repeated fldPath.Child(…).Child(…)), and 2 import-resolved links of metav1.Now().Time.Unix() to the package function metav1.Unix; every added edge is a typed instance-method link.

Triage (seeded samples, source read for each):

  • Retargets: all sampled correct. sonic's gorm/gen chains move from attachmentDo::Where to each table's own xxxDo::Where / First / Delete, and xerr.WithStatus(…).WithMsg(…) to util/xerr instead of util/haloerr; kubernetes' scheduler test wrappers (st.MakePod().Name(…).Obj()), client-go typed clients (client.CoreV1().ConfigMaps(ns).Delete), field.NewPath(…).Index(0), ktesting.Init(t).SyncTest in the package the file imports.
  • Drops, by why the chain links nothing: kubernetes 10,802 outside the project, 1,126 no such method on a project type (it comes from an embedded outside or built-in type), 460 can't be read, 171 kept under another ref of the chain, 56 decided by the indexed-element step; prometheus 426 / 431 / 71 / 82 (indexed); etcd 370 / 131 / 11 / 7 (indexed); sonic 17 outside / 43 no method; gin 16 outside. External and no-method drops (gomega, klog, time.Time, gorm/gen's embedded gen.DO.Count, a method from an embedded error) were all wrong before. Of the "can't be read" drops, a sample of 20 in kubernetes had 6 correct-by-luck edges (anonymous-struct test tables, function-local types, func-valued package variables) and 14 wrong ones.
  • Additions: all sampled correct (rh.lister.ControllerRevisions(ns).Get, v1.ResourceMetricStatus{}.OpenAPIModelName(), fields.Set{…}.AsSelector().String(), gorm/gen Delete/Create/Order).

Cost, median of 3 interleaved codegraph init runs per arm (wall / CPU seconds; the machine was shared, so kubernetes runs spread over 99–136 s on both arms):

repo main this change
etcd 9.5 / 28.6 8.6 / 25.4
prometheus 10.5 / 42.1 10.5 / 42.6
kubernetes 121.9 / 413.7 121.3 / 416.8

Full suite on the pushed commit a24619cf (CODEGRAPH_KERNEL_EXPECT=1 npx vitest run --maxWorkers=4, Windows): 708 files, 8,445 passed, 0 failed, 82 skipped.

Not covered

  • An explicitly instantiated generic call, F[int](x), links no edge to F itself on main or here (the extractor emits no ref for it); the method called on its result now links.
  • A receiver typed through a func-valued variable (var GetX = gauge.get, initTest := testutils.InitTest…), an anonymous struct (test tables), or a function-local type still links nothing.

No contributor PR for this issue; @inth3shadows's comment supplied the composite-literal and standard-library cases, both covered by the tests.

🤖 Generated with Claude Code

…e chain reaches

A Go method called on a receiver the extractor keeps no name for, such as
a fluent chain's later link (model.WithContext(0).Where(1).First()), a
call on what a function returns (time.Now().UTC().Format(...)) or a
composite literal (GitOracle{}.Worktree()), reached the resolver as its
bare name at the column where the chain starts. Name matching then took
a project method of that name by proximity, or the only one: the issue's
GORM-style Where and First went to another query type, the standard
library's Format to the one project Format, and a literal's method to a
namesake type.

A new Go resolver step reads the chain at that column and types the
receiver link by link: a call's result, or the one a multi-value
assignment takes; a conversion's or a composite literal's type; a
generic call given its type arguments; an embedded field by its type's
name; a package-level variable from the call it is set to. A result
written with type parameters keeps them as parameters, read from the
declaration, since the indexed signature leaves them out, and the helper
that reads them now includes a one-line function's only line. The method
is the one Go selects on that type. A type from outside the project, or
one that can't be read, links nothing instead of a guess. When a chain
calls one name twice, the bare ref is the later call.

Validation: 8 new tests; 7 fail on main, and the repeated-name case
fails under a first-link rule; the 43 Go test files pass. A/B against
main c326b6b, edges removed/added: gin 80/72, etcd 1726/1327,
prometheus 2249/1265, sonic (gorm/gen) 910/877, kubernetes 88288/84331.
Every removed edge was a name-match guess except 173 in kubernetes: 171
are the same edge now owned by another ref of its chain, and 2 wrong
links of metav1.Now().Time.Unix(). Sampled retargets, drops and
additions read correct; express and flask dumps are byte-identical.
Indexing cost is within noise, median of 3 interleaved runs, wall/CPU
seconds main -> fix: etcd 9.5/28.6 -> 8.6/25.4, prometheus 10.5/42.1 ->
10.5/42.6, kubernetes 121.9/413.7 -> 121.3/416.8.

Fixes #2543

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@colbymchenry
colbymchenry merged commit 0c3e12d into main Oct 22, 2026
@colbymchenry
colbymchenry deleted the claude/issue-2543-go-factory-chains branch October 22, 2026 04:54
colbymchenry added a commit that referenced this pull request Oct 28, 2026
…its type's method (#2961)

A Go method call on a local bound from a call or a conversion, like
`r := newRing(); r.Reset()`, `v1 := router.Group(...); v1.GET(...)` or
`c, r := CreateTestContext(w); r.Use(...)`, was matched by the method's
name alone: inferLocalReceiverType reads a declaration's written type and a
few line patterns, none of which reads a call's result. The by-name guess
took a namesake (etcd's `lg := s.Logger()` linked `lg.Info(...)` to a
project logger's Info, prometheus's `NewWriteHandler(...)` results to the
read handler's ServeHTTP), or found nothing.

Main already types a value from its declaration for indexed and chained
calls (goDeclaredValueType, #2782). Plain receivers now use it too: a local
whose in-scope declaration has a call or a conversion as its value takes
that value's type (the k-th result of several, a method's result, a
conversion's type), read in the file that writes it (GoReceiverDecl.file),
for matchMethodCall, matchMemberFunctionRef and matchGoFieldChainCall. A
value of a type from outside the project links nothing, including one a
project function returns as an outside interface. Assertions stay with
goAssertedLocalType, and a type switch's guard is not typed from its value.

The typer itself, for every path that uses it:
- a trailing `.(T)` is a T whatever its prefix (`pool.Get().(*Conv)`);
- a call through a func-typed field gives the field's result type;
- a bare call a dot import brings in is typed (`package watch_test`
  calling `NewStreamWatcher(...)`);
- a method of a struct or interface literal, of an outside generic type
  (grpc's `ServerStreamingClient[Resp].Recv()`) or one the index doesn't
  show on a project type gives an unknown type, not "outside", so name
  matching runs as before.

Validation:
- go-receiver-type.test.ts (the PR's 23 cases, 2 expectations updated where
  the call is now typed, plus 4 ours), go-std-method-receivers.test.ts and
  go-chained-receivers.test.ts: on main 19 of 51 fail; all pass here. All
  45 go-*.test.ts files pass (491 tests).
- A/B vs main 2577601 (same kernel): gin +194/-52/65 moved, etcd
  +4,441/-359/743, prometheus +7,041/-239/1,409, a kubernetes staging
  subtree +1,154/-86/354, harbor without its portal +423/-138/366.
  express and flask byte-identical. Sampled additions right; removals are
  outside values (zap, os, grpc, otel, beego, pflag) and the outside-
  interface results, about 80% of which were wrong by sample. Known loss:
  a type defined over an interface (`type Codec Serializer`) has no methods
  in the index, so a coincidental guess on such a value goes, as in #2706.
- Compared with PR #1954 on the same repos: 4,962 more links and 360 of
  its edges gone, those mostly wrong guesses (166 of them the outside-
  interface results); its declaration-line restriction also lost right
  links on range variables and field values, which this leaves to main's
  scan.
- CPU (profile self time of receiver typing): prometheus 1.82 -> 1.72 s,
  k8s subtree 0.88 -> 0.92 s, harbor 1.02 -> 0.86 s.

Reimplements PR #1954 by @danusha2345 on main's value typer; its tests are
included.

Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant