Repository navigation
Go: cross-package calls are not resolved when go.mod is not at the project root #2322
Description
Activity
Some numbers from public repos, to show how often this comes up. All measured with CodeGraph 1.6.1 on shallow clones; "module-only" means indexing just the directory that contains
go.mod, which is what a fix would give.Repo (commit) Layout Go→Go call edges, whole repo → module-only Cross-directory calls x.field.Method()callsgoharbor/harbor ( f25e9da)only src/go.mod16,308 → 21,656 (+33%) 8,963 → 14,268 (+59%) 798 → 1,323 mattermost/mattermost ( af4c3cf)server/go.mod, plusserver/public/go.mod114,068 → 122,459 (+7%) 51,719 → 59,848 (+16%) 2,395 → 7,938 Generated
.pb.gofiles are excluded. The mattermost figure is a lower bound:server/publicis a separate module, so even theserver/-only index still treats it as external.The same problem also appears with a root
go.mod, when the repo has several modules. etcd-io/etcd (64f26db) hasgo.modat the root (go.etcd.io/etcd/v3) and sub-modules such asserver/go.mod(go.etcd.io/etcd/server/v3) andclient/v3/go.mod. The sub-module paths are not under the root module path, so every import of them is treated as external: 2,111 import lines across 704 files, with only 2 using the root module path. For example, frometcdutl/intoserver/, calls such aswal.OpenForRead(...),backend.NewDefaultBackend(...)andschema.Migrate(...)get no import-resolved edge (0 in the index).Given that case, I'd adjust the fix direction from my original post: instead of picking the module that contains the file being resolved, keep a list of all modules found under the root and map an import path to a directory by the longest matching module path. That covers both layouts (nested module, and root plus sibling modules), and the resolver does not need the calling file's location for it. Still happy to open a PR with tests for this if it sounds right.
- added a commit that references this issue
on Oct 5, 2026 - added a commit that references this issue
on Oct 5, 2026 Thanks @GoDiao for the report, and for the follow-up about multi-module repos like etcd. It changed the design for the better.
Fixed on
mainin #2361, building on #2324 by @danusha2345:- Every module is found. For each directory with indexed Go files, the resolver reads the nearest
go.mod, never above the project root. An import path then maps to the module with the longest matching module path, as you suggested. This covers a nested module (svc/go.mod, aserver/backend next toweb/) and sibling modules (etcd's root module besideserver/go.modandclient/v3/go.mod). It discovers modules from the indexed files, so it never walks the disk, and modules undertestdata/or_/.-prefixed directories only serve their own files. - Names resolve within their package. A name written through a package, like
store.Manager,job.OPCommandorartifact.Manager, now links only to that package's symbol, not to a same-named one elsewhere. That was needed because module discovery alone sent some type references to same-named methods: about 180 on etcd. It also corrects links in projects with a singlego.mod.
On your repro,
callers Newandcallers CreateItemnow findNewServiceandAddItemwithgo.modinsvc/, and the root-go.modvariant is unchanged. Go→Go edges went from 52,530 to 66,284 on etcd and from 62,423 to 77,615 on harbor. Every removed edge checked was a correction.This ships in the next release. Re-index after upgrading (
codegraph index) to pick it up. You and @danusha2345 are credited in the changelog and as co-authors on the commit.- Every module is found. For each directory with indexed Go files, the resolver reads the nearest
- added a commit that references this issue
on Oct 7, 2026
When a Go module lives in a subdirectory of the indexed project (for example
svc/go.modwith nogo.modat the root), cross-package calls inside that module are not resolved.callersreturns no results for both package-qualified calls (store.New()) and calls made through a struct field (s.db.CreateItem()). Indexing the same code withgo.modat the root resolves both.This layout is common in repos that keep a Go backend next to a frontend or other services, e.g.
server/go.modandweb/package.json.Reproduction
Two copies of the same module, one with
go.modat the root and one insvc/. Both build withgo build ./....repro.sh
go.modat rootgo.modinsvc/store.New(), callers ofNewNewService)s.db.CreateItem(), callers ofCreateItemAddItem)Tested with CodeGraph 1.6.1.
Cause
loadGoModuleinsrc/resolution/go-module.tsreads only<projectRoot>/go.mod, and its comment notes that nestedgo.modfiles are not yet supported. With no module path, in-module imports such asexample.com/app/svc/internal/storeare treated as external, so:matchGoFieldChainCallskips the package-qualified field typestore.Managerbecause the package is not in-module.Possible fix
Discover every
go.modunder the project root (and possiblygo.work), and havegetGoModulereturn the module whose directory contains the file being resolved, instead of a single project-wide module. I'm happy to open a PR with a regression test if this direction works for you.