Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Fixes

- Go calls made through a struct field, like `s.service.AddItem()`, now resolve when the method's receiver type is unexported (`func (s *server) ...`), the usual shape for gRPC and HTTP handlers. These calls were missing from callers and impact before. Re-index Go projects after upgrading. (#2323)

## [1.6.2] - 2026-10-03

Expand Down
67 changes: 67 additions & 0 deletions __tests__/resolution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3428,6 +3428,73 @@ func (mx *Mux) dispatch() {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
}, 30000);

it('resolves field chains when the method receiver type is unexported (#2323)', async () => {
// `func (s *server)` is the idiomatic gRPC/HTTP handler shape. The
// typed-parameter pattern is PascalCase-guarded, so a lowercase receiver
// type used to yield no base type and the whole chain was dropped.
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-2323-'));
try {
fs.writeFileSync(path.join(tmpDir, 'go.mod'), 'module example.com/app\n\ngo 1.22\n');
fs.mkdirSync(path.join(tmpDir, 'domain'));
fs.mkdirSync(path.join(tmpDir, 'handlers'));
fs.writeFileSync(
path.join(tmpDir, 'domain', 'service.go'),
`package domain

type Service struct{}

func (s *Service) AddItem(name string) error { return nil }

func (s *Service) Count() int { return 0 }
`
);
fs.writeFileSync(
path.join(tmpDir, 'handlers', 'server.go'),
`package handlers

import "example.com/app/domain"

type server struct {
service *domain.Service
}

func (s *server) Create(name string) error {
return s.service.AddItem(name)
}

type cache[T any] struct {
service *domain.Service
}

func (c *cache[T]) Size() int {
return c.service.Count()
}
`
);

const cg = CodeGraph.initSync(tmpDir);
await cg.indexAll();

const addItem = (await cg.searchNodes('AddItem', { limit: 5 })).find(
(r) => r.node.kind === 'method'
);
expect(addItem).toBeDefined();
const addCallers = await cg.getCallers(addItem!.node.id);
expect(addCallers.map((c) => c.node.name)).toContain('Create');

// Generic receiver: the type parameter list must not hide the type.
const count = (await cg.searchNodes('Count', { limit: 5 })).find(
(r) => r.node.kind === 'method'
);
expect(count).toBeDefined();
const countCallers = await cg.getCallers(count!.node.id);
expect(countCallers.map((c) => c.node.name)).toContain('Size');
cg.close();
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
}, 30000);
});

describe('Imported singleton instance-method calls (#1292)', () => {
Expand Down
4 changes: 4 additions & 0 deletions src/resolution/name-matcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6770,6 +6770,10 @@ function buildLocalReceiverTypePatterns(language: Language, r: string): RegExp[]
return [
new RegExp(`\\b${r}\\b\\s*:=\\s*&?([A-Za-z_][\\w.]*)\\s*{`), // lg := Logger{} / &Logger{}
new RegExp(`\\bvar\\s+${r}\\s+\\*?([A-Za-z_][\\w.]*)`), // var lg Logger / *Logger
// A method receiver: anchored on `func (`, so an unexported type
// (`func (s *server)`, `func (c *cache[T])`) is safe to accept here,
// unlike the keyword-free pattern below (#2323).
new RegExp(`\\bfunc\\s*\\(\\s*${r}\\s+\\*?([A-Za-z_]\\w*)\\s*(?:\\[[^\\]]*\\])?\\s*\\)`), // func (s *server)
// A typed parameter / method receiver (`func use(lg Logger)`,
// `func (l Logger) M()`) — name-before-type with no `var`/`:=` (#1125).
// PascalCase-guarded (unlike the anchored patterns above) to keep the
Expand Down