Skip to content

Go: calls through a struct field are not resolved when the receiver type is unexported #2323

Description

@GoDiao

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions