Repository navigation
fix(go): a call through a local bound from a type assertion reaches the asserted type's method - #2478
Merged
Merged
Conversation
…he asserted type's method
Receiver inference read `v := T{}`, `var v T` and parameters, never a
local bound from a type assertion. So `flusher.Flush()` after
`flusher, ok := w.(http.Flusher)` went to whichever project method name
matching picked, and calls like etcd's `fi.Action()` after
`fi := s.kvindex.(*fakeIndex)` linked to nothing.
A Go local whose declaration in scope at the call binds the value of a
type assertion (`v := x.(T)`, `v, ok := x.(T)`, `var v, _ = x.(T)`) now
has the asserted type, found where Go finds it, as #2444 does for a call
made through the assertion itself (`x.(T).M()`). Calls, method values and
field chains through the local (`wr.job.Start()`) reach the type's own
method, its interface's, or one embedding promotes into it. The binding is
the one the scope reader from #2448 finds in scope at the call: an `if`
header's ends with its statement, and a nearer declaration in a block that
has ended does not hide it. A type switch's variable and a value that only
starts with an assertion (`x.(T).field`) resolve as before.
A type from outside the project, a predeclared type or an alias of an
outside type links nothing, as for the assertion itself. A type the index
can't tell is left to name matching, as before: a type literal
(`interface{ GetDeadline() time.Time }`) or a type declared inside a
function (grpc-go's `internalInfo` and `grpcstatus`). A local bound from an
assertion and named like a standard-library package
(`parser, ok := builder.(balancer.ConfigParser)`) is no longer dropped as
a call into that package.
On etcd, harbor, kratos and grpc-go, 358 call sites change. 216 gain a
link, 38 move to the asserted type's method and 89 keep their target, now
resolved through the asserted type. 15 lose a link that went through a
type from outside the project, or from a field read to a same-named
function.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
5 tasks done
colbymchenry
added a commit
that referenced
this pull request
Oct 10, 2026
…brary package reaches the project's method (#2489) isBuiltInOrExternal dropped every dotted Go ref whose first segment is in GO_STDLIB_PACKAGES before any resolution, so a call through a parameter or local of that name linked to nothing: gin's `context.AbortWithError(…)` in `func(context *Context)`, harbor's `context.IsAuthenticated()` after `context := NewSecurityContext(…)` and `log.Errorf(…)` after `log := log.G(ctx)`, etcd's `printer.DBHashKV(…)` and `trace.Step(…)`. Such a call now gets past the package list when #2448's scope reader finds the name declared in the function around it, unless the declaration says it holds no project method: a type from outside the project or a predeclared one, or what an outside package's function hands out (`scanner := bufio.NewScanner(r)`, `url, err := url.Parse(raw)`). #2478's exemption for locals bound from a type assertion stays beside it. matchMethodCall resolves these calls by the declaration alone, never by the receiver's name, which fits dozens of a big tree's types (kubernetes' `printer := NewTablePrinter(…)` is no kubeadm `Printer`): - a written or asserted project type (goWrittenType) calls its own method, an interface's or a promoted one, or nothing; - a value a project function hands out calls a method of that function's package: the type named like the variable, or the only one of the name; - anything else calls only a method no other type declares, never one with a standard-library method name. The scope reader also records `x := T{…}` / `new(T)` types and the own-package function a `:=` calls. GO_STDLIB_PACKAGES moves to name-matcher. A/B against main 1d3619d, natural-key edge diffs with identical nodes and no edge removed: gin +2, prometheus 0, etcd +44, harbor +60, kubernetes +508, every one triaged correct. The first version, which let these calls reach name matching, added 811 on kubernetes, 346 of them guesses by the receiver's name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bompus
added a commit
to bompus/codegraph
that referenced
this pull request
Oct 11, 2026
* fix(go): a call through a parameter or local named like a standard-library package reaches the project's method (colbymchenry#2489) isBuiltInOrExternal dropped every dotted Go ref whose first segment is in GO_STDLIB_PACKAGES before any resolution, so a call through a parameter or local of that name linked to nothing: gin's `context.AbortWithError(…)` in `func(context *Context)`, harbor's `context.IsAuthenticated()` after `context := NewSecurityContext(…)` and `log.Errorf(…)` after `log := log.G(ctx)`, etcd's `printer.DBHashKV(…)` and `trace.Step(…)`. Such a call now gets past the package list when colbymchenry#2448's scope reader finds the name declared in the function around it, unless the declaration says it holds no project method: a type from outside the project or a predeclared one, or what an outside package's function hands out (`scanner := bufio.NewScanner(r)`, `url, err := url.Parse(raw)`). colbymchenry#2478's exemption for locals bound from a type assertion stays beside it. matchMethodCall resolves these calls by the declaration alone, never by the receiver's name, which fits dozens of a big tree's types (kubernetes' `printer := NewTablePrinter(…)` is no kubeadm `Printer`): - a written or asserted project type (goWrittenType) calls its own method, an interface's or a promoted one, or nothing; - a value a project function hands out calls a method of that function's package: the type named like the variable, or the only one of the name; - anything else calls only a method no other type declares, never one with a standard-library method name. The scope reader also records `x := T{…}` / `new(T)` types and the own-package function a `:=` calls. GO_STDLIB_PACKAGES moves to name-matcher. A/B against main 1d3619d, natural-key edge diffs with identical nodes and no edge removed: gin +2, prometheus 0, etcd +44, harbor +60, kubernetes +508, every one triaged correct. The first version, which let these calls reach name matching, added 811 on kubernetes, 346 of them guesses by the receiver's name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(changelog): drop the Go entries the merge listed twice and the log example --------- Co-authored-by: Colby Mchenry <me@colbymchenry.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Receiver inference reads
v := T{},var v Tand parameters, but never a local bound from a type assertion. #2444 resolved a call made through the assertion itself (x.(T).M()), but the more common shape binds the value first:Such calls went to whichever project method name matching picked, or nowhere. etcd's
fi.Action()afterfi := s.kvindex.(*fakeIndex)linked to nothing. grpc-go'scurrBal.waitForClose()aftercurrBal := gsb.balancerCurrent.Balancer.(*mockBalancer)went tobuildCallbackBal::waitForClose, andsimpleResp.GetPayload()on a*testpb.SimpleResponsewent toSimpleRequest::GetPayload.Fix
A Go local has the asserted type when its declaration in scope at the call binds a type assertion's value (
v := x.(T),v, ok := x.(T),var v, _ = x.(T)). The type is found where Go finds it, with the lookup fix(go): a call through a type assertion reaches the asserted type's method #2444 uses: the own package or a dot import for a bareT, the imported project package forpkg.T, aliases followed. Calls, method values (register(st.Fetch)) and field chains (wr.job.Start()) through the local reach the type's own method, its interface's, or one promoted by embedding.The binding comes from fix(go): a dotted call is written through its own receiver, and a local named like an import is that variable #2448's scope reader:
ifheader's binding ends with its statement;A type switch's variable, an assignment to an already-declared variable (
st = v.(*sink)) and a value that only starts with an assertion (x.(T).field) resolve as before.A type from outside the project, a predeclared type (
error) or an alias of an outside type links nothing, as for the assertion itself.Some types the index can't tell: a type literal (
interface{ GetDeadline() time.Time }), or a type declared inside a function (grpc-go'sinternalInfoandgrpcstatus). Calls through those are left to name matching, as before. The chain formx.(interface{…}).M()keeps fix(go): a call through a type assertion reaches the asserted type's method #2444's "nothing", because its bare-name ref gives name matching no receiver to go on.A local bound from an assertion and named like a standard-library package (
parser, ok := builder.(balancer.ConfigParser)) is no longer dropped by the built-in filter as a call intogo/parser.Validation
Each repo was fully indexed with main and with this branch, and every call or method-value site whose edges differ was compared. The comparison ran on b635dd4, the branch's base before it was rebased onto 0951105. The seven commits in between touch C++, React/JS, sync and context code, and no Go resolution.
An oracle independent of the resolver classified every changed site: a brace-depth scan for the binding in scope, with T located through the repos'
go.modfiles and imports. Sites whose target isn't owned by T itself (field chains, embedding) were checked by hand against the declarations.Added and retargeted (254 sites): every one lands on T's method, or for a field chain on the field type's method:
b.tx.Action()→testutil.Recorder::Action, throughfakeBatchTx's embeddedRecorder;conn.CloseCh.Receive()→testutils.Channel::Receive;a.client.GetClient()→common_http.Client::GetClient;parser.ParseConfig(…)calls now reachbalancer.ConfigParser::ParseConfig.Retargets in grpc-go include
mockBalancer/verifyBalancer(16, frombuildCallbackBal),SimpleResponse::GetPayload(6, fromSimpleRequest),alts.AuthInfo::PeerRPCVersions(4, from the internalaltsAuthInfo) andbalancer.ClientConn::ResolveNow(4, fromccBalancerWrapper).Dropped (15 sites): each was a link through a type from outside the project, or a field read linked to a function:
hf.StreamEnded()→parsedDataFrame::StreamEnded);syscall.WaitStatusand*descriptorpb.EnumValueOptionsin etcd;robotVal.String()→CreateRobotEvent::String;tv.CreateRevision→ theclientv3.CreateRevisionfunction, andonFinishOpt.OnFinish→grpc.OnFinish.Four are
netErr.Timeout()throughnet.Errorin grpc-go's bufconn tests. There the error does come from bufconn's ownnetErrorTimeout, but as with fix(go): a call through a type assertion reaches the asserted type's method #2444'sx.(net.Error).Timeout(), a method of an outside interface is not guessed.Left to name matching, unchanged from main:
ci.GetCommonAuthInfo()→CommonAuthInfo::GetCommonAuthInfo);w.WithArtifactDigest(…)→vuln.Report, which is right, andwrappedErr.Unwrap()throughinterface{ Unwrap() []error }→ErrImmutable::Unwrap() error, which is wrong, as it was before.Tests
__tests__/go-asserted-locals.test.ts, on both the kernel and wasm extraction paths, covers:parser.A mutation check fails exactly the two new cases on both backends. Disabling the fall-back to name matching fails one; disabling the standard-library exemption fails the other. The 56 test files that index Go pass.
Notes
isBuiltInOrExternal. Whichever lands second keeps both conditions.parser := bb{}(3 sites) and gin'scontext.AbortWithError, a gap noted in fix(go): a dotted call is written through its own receiver, and a local named like an import is that variable #2448.🤖 Generated with Claude Code