Repository navigation
fix(go): a dotted call is written through its own receiver, and a local named like an import is that variable - #2448
Merged
Conversation
…al named like an import is that variable goRefQualification read a dotted Go call's package qualifier from its line, as the line's only `X.Name` spelling. A second spelling of the name left it with none (`Digest:` beside `digest.Digest(dig)`, `"Log using Infof"`), and another selector before it was taken instead (`klog` for `err.Error` in `klog.Error(err.Error())`). It now takes the reference name's first segment, the only one that can be a package. A call through a parameter or local that takes an import's name (etcd's `jwt, err := newTokenProviderJWT(…)`, testify suites' `suite` receivers) is a call on that variable. A per-file scope reader finds the parameters, receivers, results, `:=`, `var` and `const` names in scope at the call, so such a call is no longer one into the package. The variable's declared type, when its declaration writes one, decides what it reaches: an outside package's type has none of the project's methods, a project package's type is resolved as the receiver's type. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s it The Go scope reader keeps each file's declarations per resolution context. clearNameMatcherMemos now drops them with the other per-file memos, so a sync that removes a local named like an import stops treating the call as one on that local. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…own type `metadata.Type.String()` on a parameter `metadata prompb.MetricMetadata` calls String on the field, so the parameter's declared type only stands for a call made on the parameter itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d like an import Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…function, has no project method `clock := clocktesting.NewFakePassiveClock(…)` holds a value of a type that package declares, so `clock.Now()` is no call to a project `Clock`'s Now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts braces by regex The Go scope reader built its blanked text by appending each piece and replacing every character of each comment and string. It now collects the pieces and joins them once, and skips from one comment or quote to the next. Same text on every Go file of gin, prometheus, etcd, harbor and kubernetes; about twice as fast. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 7, 2026
Merged
colbymchenry
added a commit
that referenced
this pull request
Oct 10, 2026
…he asserted type's method (#2478) 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
goRefQualification(src/resolution/name-matcher.ts) decides which package a Go reference is written through, andisGoExternalQualified,isInGoQualifierPackageandresolveGoCrossPackageReferenceact on that answer (#2322, #2397). Two gaps were left after #2410.1. A dotted call's qualifier was read from its line. A call ref like
klog.Infofis recorded at its receiver's column, and the reader took the line's onlyX.Infofspelling. So:&distribution.Descriptor{Digest: digest.Digest(dig)}(theDigest:key; 9 calls →Suite::Digest), kubernetes'klog.Infof("Log using Infof, …")andklog.Errorf(…)incomponent-base/logs/example/example.go→ the etcd3 logger wrapper's methods, andcmp.Diffon a line that prints"Diff:"→ kubectl'sDiffer::Diff.klog.Error(err.Error()),err.Errorwas read as a call throughklog.2. A parameter or local named like an import was taken for the package.
isGoExternalQualifiedrejected every candidate of a call through it, methods included. Examples: etcd'sjwt, err := newTokenProviderJWT(…)thenjwt.assign(…), and harbor's testify suites, whosefunc (suite *DaoTestSuite) …receivers sit in files that import testify'ssuite.resolveGoCrossPackageReferencefollowed the import for it: kubernetes'cache := &atomic.Bool{}; cache.Store(false)linked to client-go'scache.Storeinterface, and harbor'slogger.Error(…)on a parameterlogger logger.Interfaceto the package-level functionlogger.Error.What changed
Resolver only (
name-matcher.ts, one line inimport-resolver.ts); extraction and the kernel are untouched.kloginklog.Infof,sins.cache.Get. Bare references are read from the line as before.funcwith a body (function literals included, function types excluded), and the names each:=,varandconstdeclares. Comments and string contents are blanked first.jwt, err := jwt.Parse(…)still calls the package on its right-hand side), anif/for/switchheader's names in that statement's blocks (elsechains included), aselectcase's in its clause, and a parameter in its function's body.quota.Name,args.PrefersProtobuf) from a method value; it is left as it was.clock clock.PassiveClock,cache := &atomic.Bool{}), or a value an outside package's function hands out (clock := clocktesting.NewFakePassiveClock(…),value := reflect.New(t)), has none of the project's methods. Such a call stays unlinked, as it was onmain.robot := &robot.Robot{…}reaches theToJSONthatcontroller/robot.Robotgets from the embeddedmodel.Robot). It also covers parameters declared through a package, likelogger logger.Interface, which Go's receiver patterns don't read. A field read through the local keeps its own type:metadata.Type.String()is notMetricMetadata::String.clearNameMatcherMemosdrops the scope reader's per-file cache with the other per-file memos, so a sync that edits the declaration is read afresh. There is a test for this.Validation
Tests
__tests__/go-ref-qualifier.test.ts, 6 tests:main(ed199e6). The sixth (the package stays the package elsewhere) passes there and pins that the scope reader doesn't overreach. On this branch, the sync test also fails if the new cache clear is taken out.CODEGRAPH_KERNEL=0.elsechains,selectclauses,var (…)groups holding multi-line literals,map[string]func(jwt string){…}types, generics, multi-line parameter lists, comments, strings and labels.tscpasses. 27 Go / resolution / extraction / framework suites (1,174 tests) pass withCODEGRAPH_KERNEL_EXPECT=1, plus the sync, sync-import-retry and dead-code suites (116 tests). The one exception isfunction-ref's Caller/impact graph misses methods passed as first-class references (callbacks), e.g. executor.submit(obj.method, …) #1820 Python case, which hit its own 60 s timeout at 100% CPU and passes with a longer limit.Before / after (full index, kernel path,
main@ed199e60 vs this branch, same kernel; natural-key edge diff,vendor/excluded; each moved or re-labelled edge counts once in removed and once in added)Re-run on the final base (
main@1fb8691d, after #2430, #2442, #2444, #2433, #2449 and #2434 landed during the merge): prometheus, etcd and harbor change exactly the same edges, edge for edge. Only the base totals moved: prometheus 97,315, etcd 63,177, harbor 98,475.Every changed edge was triaged at its site. A site was matched across arms by caller, line and column, and the declaration of the receiver was read where the receiver mattered.
zap.Error(err)→zapRaftLogger::Error, read through another selector on the line.digest.Digest→Suite::Digest, andconfig.ReadOnly(r), a func-typed field of the parameterconfig Configthat was linked tolib/config'sReadOnly.cmp.Diff→Differ::Diff;gomega.Expect→GomegaInstance::Expect;klog.Infof/klog.Errorf→klogWrapper;fwk.EnqueueExtensions()onfwk, err := newFramework(…)and 2cache.Store(false)oncache := &atomic.Bool{}, which had gone to the imports' interfaces;client.Doonclient := &http.Client{…}→HTTPClient::Do;server.Start()onserver := httptest.NewUnstartedServer(…)→ a kubeletserver::Start.rest rest.Interface,meta metav1.Object,clientset clientset.Interface,watch watch.Interface,version *version.Version,apps appsv1.AppsV1Interface,cadvisor cadvisor.Interface. On main these were receiver-word guesses (0.65 to 0.85) into unrelated packages, likeRESTClient::Patchor kubelet'sVersion::String.logger.Error/Infof/Debugfonlogger logger.Interfacenow reachInterface::*instead of the package's function.storage.Appenderonstorage *teststorage.TestStorage→DB::Appender, through the embedded*tsdb.DB;labels.String→ model/labels'Labels::Stringinstead of prompb's.client.Compact/client.Txnonclient *client.RecordingClient.logger.*incopy.go(logger := log.GetLogger(ctx), whose type nothing reads), and prometheus'sstorage := promqltest.LoadedStorage(…). They went from the import's function or interface to a guessedLoggerorStoragemethod.reg reg.Client,repository *model.RepoRecordand an allowlist local.config.UnmarshalYAMLonvar config SDConfig).jwt.assign/jwt.info(correct);hcn.*onhcn := (proxier.hcn).(*fakehcn.HcnMock)andstatus.FindContainerStatusByName(correct);status.GetInfoandclock.Nowonclock := testKubelet.fakeClock(wrong);api.QueryRange→ the project'squeryRangeAPIinterface, onapi, err := newAPI(…), which returns client_golang'sv1.API(a guess).http.Error(w, err.Error(), …),zap.String("id", c.cid.String())).Correct, 113: etcd's 102 two-hop field chains (0.85, e.g.
c.cid.String()on atypes.IDfield) and 10 typed calls, plus prometheus'q.stmt.Stringchain.Plausible, 20: etcd's 18
id.String()ontypes.IDparameters →ID::String, and 2reqStringer.String.Wrong, 196: guesses for a standard method name.
X.Error()→notRegisteredErr::Errorby a shared word, incl.maxinflight.go:53from the report, and 2 by a capitalized receiver.err.Error()and the like →ParseErr::Error.err.Error()→ErrKeepAliveHalted::Error, and 9String()calls → a protobuf request'sStringby a shared word, likeleaderID→MoveLeaderRequest.This is an existing guess class, not a new one. On main, kubernetes already has 2,374 guesses for a standard method name (1,995 for
Error), prometheus 193 and etcd 323. Main skipped these sites only because of the misreading this PR fixes. Follow-up below.Performance
goRefQualificationitself got cheaper, since a dotted ref no longer builds two regexes from its line.Limits and follow-ups (not in this PR)
Error()/String(). Strategy 3 accepts a project method of a standard name when the receiver shares a word with its owner, soerrmatchesnotRegisteredErrorParseErr. It also guesses through a receiver declared with a predeclared type (err erroratmaxinflight.go:53), because the typed path stops only at an undeclared capitalized type. Fixing it changes thousands of edges on main, so it deserves its own A/B.isBuiltInOrExternalskips any dotted Go call whose head is inGO_STDLIB_PACKAGES(log,user,token,parser,scanner,context, …) before resolution, whether or not it is a local. Examples: gin'scontext.AbortWithErroron acontext *Contextparameter, and harbor'suser.Username. The scope reader isn't consulted there. fix(go): a project package named like a standard-library package is not the standard library #2368 (@danusha2345, open) lets project packages named like the standard library through. That composes with this change but doesn't cover locals. Un-skipping locals also exposes calls on locals of standard-library types (scanner.Text(),url.String()) to the guess above, so it needs the guess fix first.c clock.PassiveClock,pod *v1.Pod) unless the parameter is named like an import, which this PR covers. Doing it for every receiver is a much wider change.:=locals whose value comes from a project function or a field (newAPI(…),testKubelet.fakeClock) keep the usual guesses.Overlap
No open PR touches these functions. #2368 edits
isBuiltInOrExternalinsrc/resolution/index.ts, with no textual overlap; see the follow-ups.🤖 Generated with Claude Code