Skip to content

fix(go): a variable bound from a call is typed by what the call returns - #2961

Merged
colbymchenry merged 1 commit into
mainfrom
claude/go-call-bound-receivers
Oct 28, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/go-call-bound-receivers

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Summary

In Go, a method called on a variable that holds what a call returns, like r.Reset() after r := newRing(), v1.GET(…) after v1 := router.Group("/v1"), or r.Use(…) after c, r := CreateTestContext(w), now links to the method of the type that call returns. Before, these were matched by the method's name alone. A value of a type from outside the project, such as c, _ := net.Dial(…), an *os.File or an http.Handler a project function returns, now links to nothing instead of a project namesake.

This reimplements @danusha2345's PR #1954 on the value typer main already has (#2782), with their tests.

Root cause

inferLocalReceiverType types a Go receiver from the type its in-scope declaration writes (s *store.Store, x := &T{…}) and from a few line patterns. None of them reads a call's result, so a local bound from a call fell through to name matching. That guess took a namesake: etcd's lg := s.Logger() linked lg.Info(…) to a project logger's Info, and prometheus's NewWriteHandler(…) results to the read handler's ServeHTTP. Or it found nothing.

Fix

main has typed a value from its declaration since #2782 (goDeclaredValueType → goCalledType / goSignatureResultType / goMethodResultType), but only for indexed and chained calls. PR #1954 added a second, regex-based derivation of the same thing (goBoundCallResult with a paren scan of up to 40 lines per scanned line, goFirstResult, goMethodOfWrittenType). This change uses main's typer for plain receivers instead:

  • 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, or the conversion's type. The type is 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. That includes a project function's result written as an outside interface: about 80% of those by-name links were wrong in the A/B below.
  • Assertions stay with goAssertedLocalType, and a type switch's guard is not typed from its value.

Changes to the typer, for every path that uses it:

  • a trailing .(T) is a T whatever its prefix (prometheus's rw.converterPool.Get().(*PrometheusConverter));
  • a call through a func-typed field gives the field's result type (harbor's h.ArtifactControllerFunc());
  • a bare call that a dot import brings in is typed (kubernetes' 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 rather than "outside", so name matching runs as before.

Validation

  • Red→green: go-receiver-type.test.ts (the PR's 23 cases plus 4 of 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).

  • Expectations changed, where a call is now typed:

    • the PR's selfArg: the inner e of if e := wrap(e.Run()) is wrap's *ringLog;
    • the PR's chainedCtor: r := newRing().Reader() is a reader;
    • versionText and urls (std-method file): typed at 0.9;
    • Go: factory calls and GORM-style chains resolve to unrelated symbols #2543's h.get().Run() through a get func() A field: A::Run;
    • the PR's interfaceResult now links nothing (policy above).
  • A/B (same kernel; main 2577601 for this branch; the PR on its own cherry-pick; no Go drift between the bases):

    repo PR vs main (added / removed / moved) this branch vs main this branch vs PR
    gin +153 / -48 / 65 +194 / -52 / 65 +41 / -4 / 0
    etcd +3,468 / -225 / 395 +4,441 / -359 / 743 +976 / -137 / 348
    prometheus +3,442 / -166 / 1,027 +7,041 / -239 / 1,409 +3,658 / -132 / 400
    kubernetes staging subtree (1,247 Go files) +992 / -68 / 341 +1,154 / -86 / 354 +194 / -50 / 13
    harbor without its portal (1,611 Go files) +333 / -104 / 355 +423 / -138 / 366 +93 / -37 / 11

    express and flask are byte-identical.

    Triage, by sample:

    • Links this branch adds over the PR are right (e.g. clus := testRunner.NewCluster(…), app := head.Appender(ctx), tx := b.BatchTx(), v1 := router.Group(…)).
    • etcd's retargets are this branch right, 14 of 14 (typed vs the PR's by-name).
    • The PR's edges this branch drops are mostly wrong guesses (gin 5/5, etcd 13/14, prometheus 10/12).
    • Its own removals are outside values (zap, os, grpc, otel, beego, pflag, structured-merge-diff).
    • The PR's own removals lost right links on range variables, field values and two-dot callees (prometheus 59, k8s 31); this branch leaves those to main's scan.
  • Outside-interface results (a project function's result written as an outside type), about 167 by-name links over the five repos, about 80% wrong:

    • wrong: grpc streams linked to a proxy adapter's Recv; every NewWriteHandler(…) result linked to readHandler::ServeHTTP; a reflect.Value linked to QuantityValue::Type; harbor's HTTPStatusCodeHealthChecker(…), docker's health.CheckFunc, linked to updater::Check;
    • right: factories that return their own type, like harbor's PeriodicHealthChecker (updater), kubernetes' NewTracker (TrackerValue), etcd's NewRaftLoggerFromZapCore (zapRaftLogger).
    • Follow-up, not in this PR: read the factory's return &T{…} to type that minority.
  • Known loss, as in fix(go): a call through a variable declared with another package's type reaches that type's method #2706: a type defined over an interface (type Codec Serializer) has no methods in the index, so a coincidental guess on such a value (codec := ….LegacyCodec(gv)) goes.

  • The ~199 kubernetes call-result sites fix(go): a call on an indexed element reaches its element type's method #2755 left are indexed calls, which fix(go): a call further along a chain links the method of the type the chain reaches #2782 already serves: no change (etcd 24, prometheus 161, k8s 48, harbor 24 sites; 0 differ).

  • CPU, profile self time of receiver typing, main → this branch: prometheus 1.82 → 1.72 s, kubernetes subtree 0.88 → 0.92 s, harbor 1.02 → 0.86 s. For comparison, the PR's goBoundCallResult alone took 390–699 ms on these.

  • Full suite on the exact commit (Windows, kernel built, CODEGRAPH_KERNEL_EXPECT=1, --maxWorkers=4): 777 files, 9,202 passed, 0 failed, 85 skipped.

Reimplements #1954 by @danusha2345, whose tests are included (co-author on the commit). Its overlap with first-time PRs: #1640's cases already pass on main (#2782), and #1521 (multi-module layouts) is outside this change.

🤖 Generated with Claude Code

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>
@colbymchenry
colbymchenry merged commit 5594843 into main Oct 28, 2026
@colbymchenry
colbymchenry deleted the claude/go-call-bound-receivers branch October 28, 2026 00:56
danusha2345 pushed a commit to danusha2345/codegraph that referenced this pull request Oct 28, 2026
…mchenry#2972; 10 landed PRs yield to main; EXTRACTION_VERSION 56)

Main landed ten more of our PRs as our own commits with the maintainer's additions
(colbymchenry#2923 ← colbymchenry#1936, colbymchenry#2924 ← colbymchenry#1951, colbymchenry#2925 ← colbymchenry#1939, colbymchenry#2930 ← colbymchenry#2921, colbymchenry#2940 ← colbymchenry#1736,
colbymchenry#2942 ← colbymchenry#1947, colbymchenry#2950 ← colbymchenry#1952, colbymchenry#2961 ← colbymchenry#1954, colbymchenry#2965 ← colbymchenry#1942, colbymchenry#2972 ← colbymchenry#1933) and the
module-path half of colbymchenry#2308 (colbymchenry#2955). Where the integration carried our version, main's
wins: the enclosing-scope member pick and its tests, the Go and Java receiver typers,
the delegating-call self-loop rule, the duplicated-module copy confidence, the C #if
collapse, the module-path resolution of Rust paths, the trail/path hardening in
utils.ts. Kept beside main: colbymchenry#1875's Verilog hooks and status fields, colbymchenry#2839's
implementations command, colbymchenry#1923, colbymchenry#2922, colbymchenry#2258, colbymchenry#1761, colbymchenry#1583, the bare-call half of
colbymchenry#2308 (rust-use-bindings.ts). CHANGELOG is main's plus the open PRs' bullets.

EXTRACTION_VERSION 56: the C #if group handling (colbymchenry#2940) and the kernel change.

Co-Authored-By: Claude Fable 5.1 <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.

1 participant