Skip to content

fix(go): a call through a parameter or local named like a standard-library package reaches the project's method - #2489

Merged
colbymchenry merged 1 commit into
mainfrom
claude/sweet-raman-ce4018
Oct 10, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/sweet-raman-ce4018

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Summary

isBuiltInOrExternal drops every dotted Go ref whose first segment is in GO_STDLIB_PACKAGES (context, log, path, user, token, printer, trace, plugin, …) before any resolution runs. That is right for fmt.Println and context.Background(), but a parameter or local can carry those names too, and a call through it is a method call on the variable:

  • gin's context.AbortWithError(…) inside router.Use(func(context *Context) {…}) (gin's own Context)
  • harbor's context.IsAuthenticated() after context := NewSecurityContext(…), and log.Errorf(…) after log := log.G(ctx)
  • etcd's printer.DBHashKV(…) after printer := initPrinterFromCmd(cmd), and trace.Step(…) after trace := traceutil.Get(ctx)

All of these linked to nothing on main.

What changes

The filter (index.ts). A call is still taken for the package unless its receiver is a parameter or local of the function around it at the call: the scope reader from #2448 (goLocalDecl) decides. It stays dropped when what the declaration says holds no project method: a type from outside the project, a predeclared one, or what an outside package's function hands out (scanner := bufio.NewScanner(r), url, err := url.Parse(raw), a context *gin.Context parameter). #2478's condition for locals bound from a type assertion stays as it was, beside this one.

Resolution (matchMethodCall). A call that got past the filter this way is resolved by its declaration only, never by the receiver's name:

  • a written or asserted project type (goWrittenType, from fix(go): a call through a local bound from a type assertion reaches the asserted type's method #2478) calls its own method, an interface's, or one embedding promotes into it, or nothing (a context PostStartHookContext gets Done from the context.Context it embeds);
  • a value a project function hands out (x := pkg.F(…), x := newFoo(…)) calls a method of that function's package: the one of a type named like the variable (printer := initPrinterFromCmd(cmd) is a printer), or the only one of its name there;
  • anything else (a range or map value, a method's result, or a value whose package has no method of the name) calls only a method no other type in the project declares, and never one with a standard-library method name.

The scope reader now also records x := T{…} / x := new(T) types and the own-package function a := calls (GoDecl.callee). GO_STDLIB_PACKAGES moved to name-matcher.ts unchanged, because both files need it.

Why the receiver's name never picks a type. The first version simply let these calls reach name matching. On kubernetes that added 811 edges. 346 of them were guesses by the receiver's name: 212 by a shared word, 129 by a type named like the capitalized receiver and 5 by the exact name. Most of those I read were wrong, because names like plugin, printer, path and user fit dozens of types in a big tree:

  • printer := NewTablePrinter(…) went to kubeadm's unrelated Printer interface;
  • plugin, err := pluginMgr.FindPluginByName(…) went to whichever of forty …Plugin types came first;
  • path, err := LookupSchemaForField(…) then path.GetPath() went to hostPath::GetPath;
  • func(context genericapiserver.PostStartHookContext) then context.Done() went to wait.channelContext::Done.

With evidence-only resolution, kubernetes adds 508 edges. That includes 43 the first version had sent elsewhere, such as printer := NewEventPrinter(…), which now reaches EventPrinter::PrintObj.

Validation

Natural-key edge diffs, main 1d3619d vs this branch, same native kernel in both arms. Node rows were identical and no edge was removed in any repo.

repo added written / asserted type field chain value's package unique method name
gin 2 2 – – –
prometheus 0 – – – –
etcd 44 8 – 35 1
harbor 60 6 – 53 1
kubernetes 508 278 122 84 24

Every added edge was triaged. A script checked each edge against the declaration at its site, and every edge the script couldn't confirm was read by hand, along with samples of those it did:

  • Written type. A script checked that the target's owner is the declared type. The 40 where it isn't were read, and each goes to a type the declared one embeds or an interface it extends: PreEnqueuePlugin → Plugin::Name, RuntimeService → ContainerManager::ListContainers, upgradePlanTextPrinter → TextPrinter::Fprintln. The rest are parameters on multi-line signatures, and a parameter that a loop-local of the same name doesn't shadow outside the loop.
  • Value's package. A script checked each call's result type against the target's owner, taking the position of a multi-value result into account (ctx, trace := traceutil.EnsureTrace(…) takes *Trace). The rest were read:
    • etcd: trace (25) and printer (2);
    • harbor: log := log.G(…) (31) reaches Logger, and context := NewSecurityContext(…) (19) reaches the concrete secret.SecurityContext, not the security.Context interface or another package's SecurityContext;
    • kubernetes: field.NewPath, jsonpath.New, testPlugin, NewPlugin[…], GetTestKubeletVolumePluginMgr and others.
    • Four take an interface or outside type and land on the package's one implementation, which is what runs. etcd's hash := crc.New(…) returns hash.Hash32 backed by &digest{}, so it reaches digest::Sum32/Reset/…. kubernetes' admissiontesting.WithReinvocationTesting reaches reinvoker::Admit.
  • Field chains. These go through the existing validated field-type path (plugin.host.GetMounter → VolumeHost, context.Path.String → field.Path, sync.kubeAPI.* on func (sync *NodeSync) receivers).
  • Unique method name. These are DRA test plugins from map/range values, path := restClient.Get()… → Request::Param, errors := ValidateX(…) → ErrorList::ToAggregate, etcd's tls := newTLS(…) → TLSInfo::ClientConfig, and harbor's cHash::toList range value.

The 44 added edges with a standard-library method name (String) are all typed (path *field.Path) or field chains; none is a guess.

Calls through such locals that stay unlinked:

  • Outside values, deliberately: gin 9, prometheus 30, etcd 7, harbor 11, kubernetes 215. Examples are bufio.Scanner, url.URL, hash.Hash32, gengo's *generator.Context, k8s.io/utils/trace, utilexec and math/rand.
  • No evidence: prometheus 10, etcd 7, harbor 9, kubernetes 491. These are untyped values with no package evidence, the guesses the first version made.

Overlap

Performance

The filter now reads the scope of files whose only dotted calls go through standard-library packages. Measured with interleaved full indexes, total process CPU, main vs this branch:

repo main this branch
etcd (3 rounds, median) 34.2 s 34.9 s
harbor (3 rounds, median) 55.4 s 55.1 s
kubernetes (2 rounds, mean) 415.2 s 414.6 s

Each difference is inside its own run-to-run spread (etcd main ranged 33.3–35.5 s).

Test plan

  • __tests__/go-stdlib-named-locals.test.ts (new, 9 cases): 5 fail on main (the positive cases), and all 9 pass with the fix, on both the native kernel and wasm (CODEGRAPH_KERNEL=0).
  • All Go suites plus extraction and frameworks-integration: 25 files and 953 tests on the kernel path; the Go suites on wasm: 22 files and 211 tests.
  • Full suite on the final commit: 6,906 passed, 0 failed, 82 skipped.
  • tsc --noEmit, npm run build
  • Before/after A/B on gin, prometheus, etcd, harbor and kubernetes, with every added edge triaged (above)

🤖 Generated with Claude Code

…brary package reaches the project's method

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>
@colbymchenry
colbymchenry merged commit 8a5ba3d into main Oct 10, 2026
@colbymchenry
colbymchenry deleted the claude/sweet-raman-ce4018 branch October 10, 2026 23:50
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>
danusha2345 pushed a commit to danusha2345/codegraph that referenced this pull request Oct 11, 2026
…o guess for outside types

Two receiver shapes still got no type, and a receiver typed outside the
project still got a guess by method name:

- A parameter or receiver of a package-qualified type (`s *store.Store`).
- A receiver bound to a call: `r := newRing()`, `s, err :=
  store.NewStore()`, `if r := find(id); r != nil`, a call spread over
  several lines, a conversion (`list := model.BotList(bots)`). The type is
  the callee's first result as its signature spells it, read in the
  callee's file. Only when the call is the whole right-hand side, and not
  when it takes the receiver itself.

A receiver whose declared type comes from a package outside the project's
modules (`conn net.Conn`, `ctx context.Context`, `req *http.Request`, an
alias of one, the result of an outside function) gets no edge:
`ctx.Done()` went to the one project type declaring a `Done`. A project
function's result declared as an outside type by value
(`http.RoundTripper`) is left as it was, since that is usually an
interface a project type implements.

Aliases of project types are followed by colbymchenry#2417, which gave them nodes; the
alias reader this change carried is gone, and an alias of an outside type
is recognized from that node's declaration.

A variable named like a standard-library package (`ring`, `token`,
`parser`) is colbymchenry#2489's now: this change carried its own rule for it (skip
only when the file imports the package), which is dropped in favour of
upstream's `isGoProjectLocalCall`, and the standard-library package check
is upstream's again, untouched.

Resolver only: no extraction change, the receiver scan keeps its
per-line memo.

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.

1 participant