Repository navigation
Go: cross-package calls are not resolved when go.mod is not at the project root #2322
Description
When a Go module lives in a subdirectory of the indexed project (for example svc/go.mod with no go.mod at the root), cross-package calls inside that module are not resolved. callers returns no results for both package-qualified calls (store.New()) and calls made through a struct field (s.db.CreateItem()). Indexing the same code with go.mod at 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.mod and web/package.json.
Reproduction
Two copies of the same module, one with go.mod at the root and one in svc/. Both build with go build ./....
// internal/store/store.go
package store
type Manager struct{}
func New() *Manager { return &Manager{} }
func (m *Manager) CreateItem(name string) error { return nil }// internal/domain/service.go
package domain
import "example.com/app/svc/internal/store"
type Service struct {
db *store.Manager
}
func NewService() *Service {
return &Service{db: store.New()}
}
func (s *Service) AddItem(name string) error {
return s.db.CreateItem(name)
}repro.sh
#!/bin/sh
# Same Go module indexed twice: once with go.mod at the repo root, once with it in a subdirectory.
set -e
B=$(mktemp -d)
mk() {
M="$1/$2"
mkdir -p "$M/internal/store" "$M/internal/domain"
printf 'module example.com/app/svc\n\ngo 1.22\n' > "$M/go.mod"
cat > "$M/internal/store/store.go" <<'GO'
package store
type Manager struct{}
func New() *Manager { return &Manager{} }
func (m *Manager) CreateItem(name string) error { return nil }
GO
cat > "$M/internal/domain/service.go" <<'GO'
package domain
import "example.com/app/svc/internal/store"
type Service struct {
db *store.Manager
}
func NewService() *Service {
return &Service{db: store.New()}
}
func (s *Service) AddItem(name string) error {
return s.db.CreateItem(name)
}
GO
}
mk "$B/root" .
mk "$B/nested" svc
for r in root nested; do
codegraph init "$B/$r" >/dev/null 2>&1
for s in New CreateItem; do
echo "== $r: callers of $s"
codegraph callers --path "$B/$r" "$s" | grep -v '^$'
done
done| Call | go.mod at root |
go.mod in svc/ |
|---|---|---|
store.New(), callers of New |
1 (NewService) |
0 |
s.db.CreateItem(), callers of CreateItem |
1 (AddItem) |
0 |
Tested with CodeGraph 1.6.1.
Cause
loadGoModule in src/resolution/go-module.ts reads only <projectRoot>/go.mod, and its comment notes that nested go.mod files are not yet supported. With no module path, in-module imports such as example.com/app/svc/internal/store are treated as external, so:
- package-qualified calls fall back to name matching and are dropped;
matchGoFieldChainCallskips the package-qualified field typestore.Managerbecause the package is not in-module.
Possible fix
Discover every go.mod under the project root (and possibly go.work), and have getGoModule return 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.
Activity
GoDiao commentedon Oct 3, 2026
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.x.field.Method()callsf25e9da)src/go.modaf4c3cf)server/go.mod, plusserver/public/go.modGenerated
.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.
Phase 4: resolution and extraction accuracy
fix(go): resolve unexported receivers and nested modules within the r…
colbymchenry commentedon 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: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.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.fix(go): an interface links the interfaces it embeds (#2397)