mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-16 20:53:57 +08:00
deac7710f2
Calls through a method receiver or parameter of an unexported type
(`func (s *server) Create() { s.service.AddItem() }`) had no edge: the
Go receiver inference only accepted PascalCase types (#2323). And only
the project-root go.mod was read, so a module in a subdirectory, or one
beside other modules as in etcd, treated imports of itself as
third-party (#2322).
Fixing either alone sends calls to the wrong package, because Go type
names are unique only per directory while types and methods were looked
up by name across the project. The receiver pattern alone sent 1,042 of
harbor's 1,998 new calls to another package's same-named type; reading
nested modules alone made harbor's caching wrappers call themselves and
put about 180 of etcd's type references on same-named methods. Both
share one fix:
- A lowercase receiver or parameter type is read only where a parameter
list puts it (receivers, parameters, multi-line lists, var blocks).
- Receiver inference also reports the type as written. For Go,
resolveMethodOnType takes the declaring package's directory (the
caller's for a bare name, the import's for a qualified one) and counts
only that package's methods, then the types its declaration embeds
(struct and interface embedding, read from the source) instead of the
name-based supertype union. The field-chain matcher scopes its struct
and field types the same way.
- The resolver reads the nearest go.mod of every directory holding
indexed Go files (no disk walk; a module under testdata/ or a _/.
directory serves only its own files) and maps an import path to the
module with the longest module path, the importing file's own module
breaking a tie. getGoPackageDir replaces getGoModule.
- A bare name written through a package (`job.OPCommand`) resolves
through the import into that package's directory, and a name-matched
candidate outside the qualifier's package is rejected; a method reached
through a variable that shadows an import is exempt.
Verification: new go-unexported-receiver (3) and go-nested-module (11)
tests: 12 fail on main (the 2 controls pass), all 14 pass. The issues'
repros go from 0 to 1 caller; the root-go.mod and `Server` variants are
unchanged.
Validation, main 26e8488a -> this branch, whole repos, Go->Go edges keyed
by source/target qualified name, file, line and column:
- etcd 6bb7e5e: 52,530 -> 66,284 (+13,827 / -73), calls 18,074 -> 25,265
- harbor 721c6d4: 62,423 -> 77,615 (+15,819 / -627), calls 16,308 -> 25,236
- prometheus 770ca8f: 90,970 -> 92,399 (+2,249 / -820), calls 40,050 -> 41,493
Of the 1,520 removed edges, 1,458 are the same call site or reference
now resolved to the right package's symbol; the other 62 were field
reads taken for method values, names whose real target is not indexed
(a `type T = string` alias), and a call to a method the receiver's type
does not have. Indexing time and CPU are unchanged.
Co-authored-by: Diao Shengjia <104132148+GoDiao@users.noreply.github.com>
Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
141 lines
4.1 KiB
TypeScript
141 lines
4.1 KiB
TypeScript
/**
|
|
* Go calls through a method receiver of an unexported type (#2323).
|
|
*
|
|
* `func (s *server) Create()` is the idiomatic shape of gRPC and HTTP
|
|
* handlers, but only a PascalCase typed parameter used to give a receiver its
|
|
* type, so `s.service.AddItem()` — and every other call through `s` — lost its
|
|
* edge. Unexported names repeat across packages (each package may have its
|
|
* own `server`), so a receiver's type is its own package's: a same-named type
|
|
* elsewhere, with the same method or the same field, is never the target.
|
|
*/
|
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import { CodeGraph } from '../src';
|
|
|
|
let root = '';
|
|
let cg: CodeGraph;
|
|
|
|
beforeAll(async () => {
|
|
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-go-unexported-recv-'));
|
|
const files: Record<string, string> = {
|
|
'go.mod': 'module example.com/app\n\ngo 1.22\n',
|
|
'internal/domain/service.go': `package domain
|
|
|
|
type Service struct{}
|
|
|
|
func (s *Service) AddItem(name string) error { return nil }
|
|
|
|
func (s *Service) Count() int { return 0 }
|
|
`,
|
|
// The issue's handler, plus calls on the receiver itself, through a type
|
|
// it embeds and through a field of an unexported type.
|
|
'internal/handlers/server.go': `package handlers
|
|
|
|
import "example.com/app/internal/domain"
|
|
|
|
type server struct {
|
|
BaseAPI
|
|
service *domain.Service
|
|
store *store
|
|
}
|
|
|
|
func (s *server) Create(name string) error {
|
|
s.store.Put(name)
|
|
s.Close()
|
|
s.SendError()
|
|
return s.service.AddItem(name)
|
|
}
|
|
|
|
type cache[T any] struct {
|
|
service *domain.Service
|
|
}
|
|
|
|
func (c *cache[T]) Size() int {
|
|
return c.service.Count()
|
|
}
|
|
`,
|
|
'internal/handlers/store.go': `package handlers
|
|
|
|
type store struct{}
|
|
|
|
func (st *store) Put(name string) {}
|
|
|
|
func (s *server) Close() {}
|
|
`,
|
|
'internal/handlers/base.go': `package handlers
|
|
|
|
type BaseAPI struct{}
|
|
|
|
func (b *BaseAPI) SendError() {}
|
|
`,
|
|
// Another package declaring the same unexported names: never a target.
|
|
'internal/admin/server.go': `package admin
|
|
|
|
type server struct {
|
|
BaseAPI
|
|
store *store
|
|
}
|
|
|
|
type store struct{}
|
|
|
|
func (st *store) Put(name string) {}
|
|
|
|
func (s *server) Close() {}
|
|
|
|
type BaseAPI struct{}
|
|
|
|
func (b *BaseAPI) SendError() {}
|
|
`,
|
|
};
|
|
for (const [rel, content] of Object.entries(files)) {
|
|
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
|
|
fs.writeFileSync(path.join(root, rel), content);
|
|
}
|
|
cg = await CodeGraph.init(root, { index: true });
|
|
});
|
|
|
|
afterAll(() => {
|
|
cg?.close();
|
|
if (root) fs.rmSync(root, { recursive: true, force: true });
|
|
});
|
|
|
|
/** `file::qualifiedName` of every symbol the function `name` in `file` calls. */
|
|
function callTargets(file: string, name: string): string[] {
|
|
const fn = cg.getNodesInFile(file).find((n) => n.name === name);
|
|
expect(fn, `${name} in ${file}`).toBeDefined();
|
|
return cg
|
|
.getOutgoingEdges(fn!.id)
|
|
.filter((e) => e.kind === 'calls')
|
|
.map((e) => cg.getNode(e.target)!)
|
|
.map((n) => `${n.filePath.replace(/\\/g, '/')}::${n.qualifiedName}`)
|
|
.sort();
|
|
}
|
|
|
|
describe('Go calls through an unexported method receiver (#2323)', () => {
|
|
it('resolves a call through a field of the receiver', () => {
|
|
expect(callTargets('internal/handlers/server.go', 'Create')).toContain(
|
|
'internal/domain/service.go::Service::AddItem'
|
|
);
|
|
});
|
|
|
|
it('resolves a generic receiver past its type parameters', () => {
|
|
expect(callTargets('internal/handlers/server.go', 'Size')).toEqual([
|
|
'internal/domain/service.go::Service::Count',
|
|
]);
|
|
});
|
|
|
|
it("resolves every call within the receiver's own package, never a same-named type's in another", () => {
|
|
expect(callTargets('internal/handlers/server.go', 'Create')).toEqual([
|
|
'internal/domain/service.go::Service::AddItem',
|
|
'internal/handlers/base.go::BaseAPI::SendError',
|
|
'internal/handlers/store.go::server::Close',
|
|
'internal/handlers/store.go::store::Put',
|
|
]);
|
|
for (const decoy of cg.getNodesInFile('internal/admin/server.go').filter((n) => n.kind === 'method')) {
|
|
expect(cg.getIncomingEdges(decoy.id).filter((e) => e.kind === 'calls'), decoy.qualifiedName).toEqual([]);
|
|
}
|
|
});
|
|
});
|