When a method's receiver type is unexported (func (s *server) ...), calls through one of its fields, such as s.service.AddItem(), are not resolved. Renaming the type to Server makes the same call resolve. Unexported receiver types are idiomatic Go and common for gRPC/HTTP handler structs, so this drops a lot of edges in typical code.
Reproduction
go.mod is at the root, so this is independent of #2322.
// internal/domain/service.go
package domain
type Service struct{}
func (s *Service) AddItem(name string) error { return nil }
// internal/handlers/server.go
package handlers
import "example.com/app/internal/domain"
type server struct {
service *domain.Service
}
func (s *server) Create(name string) error {
return s.service.AddItem(name)
}
repro.sh
#!/bin/sh
# Same code indexed twice: the handler struct is `server` in one copy and `Server` in the other.
set -e
B=$(mktemp -d)
mk() {
M="$B/$1"
mkdir -p "$M/internal/domain" "$M/internal/handlers"
printf 'module example.com/app\n\ngo 1.22\n' > "$M/go.mod"
cat > "$M/internal/domain/service.go" <<'GO'
package domain
type Service struct{}
func (s *Service) AddItem(name string) error { return nil }
GO
cat > "$M/internal/handlers/server.go" <<GO
package handlers
import "example.com/app/internal/domain"
type $2 struct {
service *domain.Service
}
func (s *$2) Create(name string) error {
return s.service.AddItem(name)
}
GO
}
mk lower server
mk upper Server
for r in lower upper; do
codegraph init "$B/$r" >/dev/null 2>&1
echo "== $r: callers of AddItem"
codegraph callers --path "$B/$r" AddItem | grep -v '^$'
done
| Receiver type |
callers of AddItem |
server |
0 |
Server |
1 (Create) |
Tested with CodeGraph 1.6.1.
Cause
matchGoFieldChainCall first infers the type of s with inferLocalReceiverType. For Go, the only pattern that matches a method receiver is the typed-parameter one in localReceiverTypePatterns:
new RegExp(`\\b${r}\\s+\\*?([A-Z][\\w.]*)`), // func use(lg Logger) / (l Logger)
It requires an uppercase first letter, as a guard against matching unrelated ident Type pairs. So (s *server) yields no type, and the chain is dropped before the field lookup. The field step itself already accepts unexported types (the tree *node case from chi).
Suggested fix
Add a pattern anchored on func (, which allows a lowercase type without the false-positive risk the guard protects against:
new RegExp(`\\bfunc\\s*\\(\\s*${r}\\s+\\*?([A-Za-z_]\\w*)\\s*(?:\\[[^\\]]*\\])?\\s*\\)`), // func (s *server) / (s *store[T])
The existing uppercase-guarded pattern stays as it is for plain parameters. Lowercase types in plain parameters (func f(h *handler)) have the same problem, but relaxing that is a separate question.
Measured impact
With only this pattern added, indexing four public repos (shallow clones, CodeGraph 1.6.1, .pb.go excluded):
| Repo (commit) |
Methods with unexported receiver |
Go→Go call edges |
x.field.Method() calls |
Edges removed |
prometheus/prometheus (961c9ba) |
39% |
38,990 → 40,120 (+1,123) |
1,011 → 1,557 |
0 |
etcd-io/etcd (64f26db) |
64% |
16,323 → 17,035 (+707) |
175 → 601 |
0 |
goharbor/harbor (f25e9da), src/ |
48% |
21,656 → 24,985 (+3,328) |
1,323 → 3,631 |
0 |
mattermost/mattermost (af4c3cf), server/ |
11% |
122,459 → 122,999 (+542) |
7,938 → 8,405 |
2 |
I checked a random sample of 32 added edges against the source and all were correct, including calls through embedded structs and to interface methods. The 2 removed edges in mattermost were wrong before and now point to the right method. For example, src.set(cfg) inside func (src *fileSrc) Set used to resolve to jsonSrc::set and now resolves to fileSrc::set.
I can open a PR with this change and a regression test.
When a method's receiver type is unexported (
func (s *server) ...), calls through one of its fields, such ass.service.AddItem(), are not resolved. Renaming the type toServermakes the same call resolve. Unexported receiver types are idiomatic Go and common for gRPC/HTTP handler structs, so this drops a lot of edges in typical code.Reproduction
go.modis at the root, so this is independent of #2322.repro.sh
AddItemserverServerCreate)Tested with CodeGraph 1.6.1.
Cause
matchGoFieldChainCallfirst infers the type ofswithinferLocalReceiverType. For Go, the only pattern that matches a method receiver is the typed-parameter one inlocalReceiverTypePatterns:It requires an uppercase first letter, as a guard against matching unrelated
ident Typepairs. So(s *server)yields no type, and the chain is dropped before the field lookup. The field step itself already accepts unexported types (thetree *nodecase from chi).Suggested fix
Add a pattern anchored on
func (, which allows a lowercase type without the false-positive risk the guard protects against:The existing uppercase-guarded pattern stays as it is for plain parameters. Lowercase types in plain parameters (
func f(h *handler)) have the same problem, but relaxing that is a separate question.Measured impact
With only this pattern added, indexing four public repos (shallow clones, CodeGraph 1.6.1,
.pb.goexcluded):x.field.Method()calls961c9ba)64f26db)f25e9da),src/af4c3cf),server/I checked a random sample of 32 added edges against the source and all were correct, including calls through embedded structs and to interface methods. The 2 removed edges in mattermost were wrong before and now point to the right method. For example,
src.set(cfg)insidefunc (src *fileSrc) Setused to resolve tojsonSrc::setand now resolves tofileSrc::set.I can open a PR with this change and a regression test.