Skip to content

fix(go): type receivers from package-qualified parameters and call results; no guess for outside types - #1954

Open
danusha2345 wants to merge 1 commit into
colbymchenry:mainfrom
danusha2345:fix/go-receiver-type
Open

danusha2345 wants to merge 1 commit into
colbymchenry:mainfrom
danusha2345:fix/go-receiver-type

Conversation

@danusha2345

@danusha2345 danusha2345 commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Rebased onto main at ed199e6 and reduced again to what main still lacks. #2417 gave type aliases nodes and resolves a method through them, so the alias reader this PR carried is gone with its tests. #2410 registers the name goimports assumes for an import, so the standard-library check no longer matches on path segments. What is left is one commit.

Problem

Three receiver shapes still get no type on main, and a receiver typed outside the project still gets a guess by method name:

  • A variable named like a standard-library package. ring, token, parser, user and url are on the standard-library package list, so every ring.X() is skipped as a call into that package, whether or not the file imports it. func flush(ring *ringLog) { ring.Write(b) } never resolves.
  • A package-qualified parameter or receiver type: s *store.Store.
  • A receiver bound to a call: r := newRing(), e, err := NewEngine(), s := store.NewStore(), if r := find(id); r != nil, and a conversion such as list := model.BotList(bots).
  • A receiver typed outside the project. ctx.Done() on a context.Context goes to the one project type that declares a Done, and req.WithContext(…) on an *http.Request to a project WithContext.

Fix

Resolver only, with no extraction change. The receiver scan keeps its per-line memo.

  • Standard-library names. A pkg.X reference is skipped as a standard-library call only when the file imports a package under that name. Otherwise the receiver is typed like any other. When its type can't be read it still gets no edge, as before.
  • Qualified parameters. The parameter pattern fix(go): resolve unexported receivers and nested modules within the right package (#2323, #2322) #2361 added for unexported types also reads name *pkg.Type. The package is found through the file's imports.
  • Call results. x[, err] := f(…) is typed from the first result in the callee's signature, read in the callee's file. The callee must be a function of the same package or of an imported project package, or a conversion to a project type. Two guards:
    • the call must be the whole right-hand side, so r := newRing().Reader() is not typed by newRing;
    • a call that takes the receiver itself (e := wrap(e.Run())) is skipped, because on that line e is still the outer variable.
  • Outside types. A receiver gets no edge when its declared type comes from a package outside the project's modules: conn net.Conn, ctx context.Context, req *http.Request, an alias of one, or the result of an outside function such as net.Dial(…). "Outside" is the same test main already applies to a type reference written through an import.

Measured

goharbor/harbor at f25e9da, src/ only (1,595 Go files), indexed from scratch with builds of main ed199e60 and of this branch, native kernel. Counts are Go calls edges. A call site is a file, line, column and callee name. Both builds index it identically run to run.

main this branch
edges 25,538 25,861
call sites with an edge 20,586 20,909
nodes 38,264 38,264

Index time was 22–54 s for either build on a shared machine, with no difference I could separate from the noise.

change call sites checked
added 478 12 read, all correct
retargeted 237 21 read (15 spread over the target pairs, 6 from the less common ones), all correct
dropped 155 all classified by a script, 47 read in the source
  • Added. Typed through a call result (mgr := config.GetCfgManager(ctx), cx, exist := security.FromContext(d.Ctx), st := job.Status(ack.Status)), a qualified parameter (ctx job.Context, userMgr pkguser.Manager, resources types.ResourceList) or a variable named like a standard-library package (parser := &license{…}, context := NewSecurityContext(…); 28 of the 478).

  • Retargeted. A guess by name moved to the method of the declared type. Examples: logger := log.GetLogger(ctx) from a replication Logger::Warningf to lib/log's; ctx := NewSecurityContext(nil) from the v2token context to the local one; err := New(cause) from an allowlist error to lib/errors; client := registryclient.NewClient(…) from the unexported client::BlobExist to the Client interface the constructor returns.

  • Dropped. Every one of the 155 has a receiver declared with a type of a package outside the module, or bound to a call into one. The script reads the receiver's declaration and the file's imports independently of the resolver; it files 146 that way, and the other 9 were read by hand (an io.ReadCloser, a distribution.Manifest ×4, an http.Handler ×2, and two project functions returning *redis.Pool and *sdktrace.TracerProvider). By what main linked them to:

    receiver main's target sites
    req.WithContext(…) on an *http.Request a jobservice Worker::WithContext 84
    w.WriteHeader(…) / w.Header() on an http.ResponseWriter, an httptest recorder, a tar.Writer a project response recorder or log writer 13
    man.Payload() / man.References() on a distribution.Manifest a mock under testing/ 11
    handler.ServeHTTP(…) on an http.Handler a registryctl blob handler 8
    file.Close() on an *os.File / io.ReadCloser a job logger's Close 6
    client.Do / Get / Post / Head on an *http.Client and similar common/http's Client 6
    ctx.Done() on a context.Context, wg.Done() on a sync.WaitGroup an ORM context wrapper 5
    other (server.Client() on an httptest.Server, err.Error(), rootRouter.Path(…) on a gorilla router, …) 22

    114 of them are concrete outside types. The other 41 are outside interfaces (http.ResponseWriter, http.Handler, distribution.Manifest, context.Context, health.Checker, io.ReadCloser, error) that main linked by name to one project type declaring the method. Two of those are arguable: checker.Check() on a health.Checker parameter (an interface of docker/distribution) went to the project's updater::Check, which is what a registered checker can be at run time. The declared type is an outside interface, so the call gets no edge, the same as ctx.Done().

Outside calls, 13 references edges are gone and nothing else changed. All 13 were read: each is a field read through a receiver that is now typed (opts.IgnoreLimitation, params.WithTag, model.AuthMode, u.ReferenceID), which main linked to a package-level function of the same name.

With #2368 merged in as well (the two touch the same condition; only CHANGELOG.md conflicts), harbor gets exactly the sum of the two changes: 25,538 → 28,537 calls edges, with the same 155 dropped and 237 retargeted sites.

Known limits

  • A method's result (x := v.Method()) and a range variable are not typed.
  • A := binding whose type can't be read does not hide an outer declaration of the same name, and a name bound in an if / for header or a function literal is not scoped to its block. Both are as on main.
  • A binding from a call through a variable named like one of the file's imports (req := suite.NewRequest(…) in a method with receiver suite, beside import "github.com/stretchr/testify/suite") is read as a call into that package, so its result counts as an outside type and gets no edge. That is 12 of the dropped sites on harbor, all *http.Request or distribution.Manifest values. fix(go): an unaliased import is also known by the name goimports assumes for its package #2410 lists the same shadowing for qualified references.
  • Whether an import is "outside the project" follows the project's go.mod files. A project indexed below its go.mod, or with none, treats every non-relative import as outside, as main already does for type references.

Tests

__tests__/go-receiver-type.test.ts, 21 cases, 16 of which fail on main at ed199e6:

  • three receivers named like a standard-library package;
  • five call bindings: unexported and exported constructors, a multi-line call, an if header, a constructor of another package;
  • a package-qualified parameter;
  • four receivers typed outside the project: a qualified pointer, a qualified interface, an outside function's result, a project function returning *os.File;
  • a conversion;
  • ctx.Done() on a context.Context;
  • ctx.Done() on an alias of context.Context, with a struct of the alias's name in another package.

The five controls pass on main and here:

  • a binding is not typed by the head of a call chain, nor on its own line;
  • a variable named like a standard-library package with an unreadable type gets no edge;
  • an outside interface returned by a project function still reaches its implementation;
  • a defined type is not followed;
  • an alias of a project type still resolves on that type, as fix(go): a type alias type A = B is indexed and links to the type it names #2417 made it.

The three alias cases the previous version had (type Context = web.Context, a grouped pointer alias, an alias embedded in a struct) pass on main since #2417 and were removed.

The file passes with the native kernel and with CODEGRAPH_KERNEL=0, together with the Go suites (go-*, goframe, rust-go-call-shape, gin-middleware-chain), resolution, function-ref, member-function-ref-owner, frameworks-integration, dead-code and the receiver suites around them (27 files, 557 tests native; 18 files, 386 tests WASM).

🤖 Generated with Claude Code

@danusha2345
danusha2345 force-pushed the fix/go-receiver-type branch 4 times, most recently from eacc1d1 to 0afc052 Compare October 3, 2026 09:01
danusha2345 pushed a commit to danusha2345/codegraph that referenced this pull request Oct 3, 2026
Ports colbymchenry#1954's goImportedPackageDirs / goIsExternalImport onto colbymchenry#2324's
per-import module lookup (getGoModuleForImport), with a small
getGoModuleOfFile so a file outside every module still treats only the
standard library as certainly external. EXTRACTION_VERSION 40: Go edges
change for modules below the project root.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@GoDiao

GoDiao commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

I ran this branch (0afc052) on a few public Go repos while comparing it with #2325. Two shapes lose edges that resolve correctly on main:

Type aliases. mattermost/mattermost (af4c3cf), indexing server/: channels/api4/handlers.go declares type Context = web.Context, and every handler is func x(c *Context, ...). Calls such as c.App.SessionHasPermissionTo(...), c.App.Config() and c.MakeAuditRecord(...) resolve on main and are dropped here. That is 3,161 edges from api4 into channels/web and channels/app, out of 4,441 dropped call sites in that index. The type lookup seems to stop at the alias in the caller's package instead of following it to web.Context.

Embedded package-qualified struct. goharbor/harbor (f25e9da), src/: pkg/cached/project/redis/manager.go has type Manager struct { *cached.BaseManager }, and m.CacheClient(ctx) (declared on BaseManager in pkg/cached/manager.go) is dropped.

Separately, in etcd-io/etcd (64f26db) receivers typed from a sibling module, like cfg config.ServerConfig or id types.ID, are dropped as external. That is the #2322 module-path problem rather than this PR, and should resolve once #2324 lands.

The full comparison table is in #2325 (comment).

danusha2345 pushed a commit to danusha2345/codegraph that referenced this pull request Oct 3, 2026
danusha2345 pushed a commit to danusha2345/codegraph that referenced this pull request Oct 3, 2026
@danusha2345
danusha2345 force-pushed the fix/go-receiver-type branch from 54a2adf to 96d8906 Compare October 6, 2026 08:31
@danusha2345 danusha2345 changed the title fix(go): type receivers from unexported/package-qualified params and constructors; no guess for external types fix(go): type receivers from package-qualified parameters, call results and aliases; no guess for outside types Oct 6, 2026
@danusha2345

Copy link
Copy Markdown
Contributor Author

Rebased onto main and redone as one commit on top of #2361. That PR covers a large part of what this one did, so those parts and their tests are dropped here:

  • Unexported receiver and parameter types (func (r *ringLog) M(), func f(h *holder)), including a field of such a receiver (h.ring.Reset()).
  • Looking a method up in the package that declares the receiver's type, so site.API is not resolved on another package's API.
  • Methods promoted from an embedded struct, also one from another package (*cached.BaseManager) and several levels deep.
  • Nested and sibling go.mod modules. This branch's own import-to-directory matching is gone; it now asks fix(go): resolve unexported receivers and nested modules within the right package (#2323, #2322) #2361's module lookup.

Two scoping rules from the earlier version are also left out, because they need the receiver scan to run without its per-line memo and they drop edges main gets today: a := or range binding hiding an outer declaration, and a binding in an if / for header or a function literal ending with its block.

What this PR still adds on main, all with tests that fail there:

  • a variable named like a standard-library package (ring, token, parser) is no longer skipped as a call into that package;
  • a package-qualified parameter type (s *store.Store);
  • a receiver bound to a call or a conversion (r := newRing(), s, err := store.NewStore(), list := model.BotList(bots));
  • type aliases (type Context = web.Context), for a receiver and for an embedded field;
  • no edge for a receiver whose declared type comes from outside the project (ctx.Done() on a context.Context, req.WithContext(…) on an *http.Request).

One rule is narrower than before: a project function that returns an outside type by value, such as http.RoundTripper, is left as on main. On harbor that kept got.RoundTrip(req) after got := NewRateLimitedTransport(…) on the project's own transport.

Re-measured on harbor f25e9da (src/, .pb.go excluded) against main 6421644: 25,103 → 25,404 Go calls edges, 455 call sites added, 237 retargeted, 154 dropped, node count unchanged. All 154 dropped sites were read: they are receivers typed outside the project. The description has the breakdown.

🤖 Generated with Claude Code

@danusha2345
danusha2345 force-pushed the fix/go-receiver-type branch from 96d8906 to 1107ed0 Compare October 7, 2026 11:57
@danusha2345 danusha2345 changed the title fix(go): type receivers from package-qualified parameters, call results and aliases; no guess for outside types fix(go): type receivers from package-qualified parameters and call results; no guess for outside types Oct 7, 2026
@danusha2345

Copy link
Copy Markdown
Contributor Author

Rebased onto main at ed199e6 and reduced again to what main still lacks. One commit; the description above is rewritten to match.

Dropped, because main covers it

Kept, still failing on main (16 of the 21 remaining test cases fail on ed199e6; the other 5 are controls)

  • a variable named like a standard-library package the file does not import;
  • a package-qualified parameter or receiver type (s *store.Store);
  • a receiver bound to a call or a conversion (r := newRing(), list := web.Users(names));
  • no edge for a receiver typed outside the project.

#2408, #2414 and #2416 change how a type reference and an interface call resolve; none of them types a receiver, and none of the 16 cases moved.

Ported onto #2417 rather than dropped

An alias of an outside type (type Ctx = context.Context) is one of the outside types that get no edge. #2417 leaves that case to resolve by name, and says so. On main that gives no edge when no project type shares the alias's name, but ctx.Done() on a Ctx goes to Ctx::Done of another package when one declares a struct Ctx. The check now reads the target from the alias node's declaration, through a small helper split out of goAliasTarget; the test fixture gained that namesake struct, so the case fails on main.

Re-measured on goharbor/harbor src/ at f25e9da against ed199e6: Go calls edges 25,538 → 25,861; 478 call sites added, 237 retargeted, 155 dropped. All 155 dropped sites have a receiver declared with, or bound to a call into, a package outside the module. Details are in the description.

…o guess for outside types

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

- A variable named like a standard-library package (`ring`, `token`,
  `parser`). Every `ring.X()` was skipped as a call into that package,
  whether or not the file imports it. It is skipped now only when the file
  imports the package; otherwise the receiver is typed like any other, and
  one whose type is unknown still gets no edge.
- 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.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@danusha2345
danusha2345 force-pushed the fix/go-receiver-type branch from 1107ed0 to 3b1a538 Compare October 7, 2026 16:03

This branch has not been deployed

No deployments
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.

2 participants