`pkga.FuncX(...)` cross-package calls in Go monorepos were dropping through the import resolver — `isExternalImport(go)` flagged any non-`/internal/` import as third-party because the resolver had no idea what the project's own module path was. Resolution fell back to name matching with path-proximity scoring, which on a layered codebase picks one accidental candidate per call site (~<1% recall per #388's 5,303-vs-1 figure). - `src/resolution/go-module.ts` (new) parses the `module ...` directive from project-root `go.mod`, exposed via `getGoModule()` on `ResolutionContext`. - `isExternalImport(go)` treats `<module-path>/...` imports as in-module; the existing `/internal/` escape hatch is preserved for repos without a parsed go.mod. - `resolveViaImport` gets a Go cross-package branch that strips the module prefix to a project-relative directory, then resolves the qualified member via `getNodesByName(member)` filtered to that exact directory and `isExported=true`. Sub-packages don't collide with their parents; same-name funcs in different packages don't false-merge. - Go extractor sets `isExported` from the identifier's first character (Go's universal uppercase=exported convention). The resolver depends on this to filter candidates. Validation on gRPC-Go (1,031 .go files, layered package tree): total `calls` edges: 23,803 -> 34,105 (+43%) cross-pkg `calls`: 10,880 -> 19,929 (+83%) fmt/strconv/etc. stdlib calls: stay external (no false positives) Tests cover in-module disambiguation with same-name funcs in two packages, aliased imports, and stdlib calls not being false-resolved to in-project nodes. Closes #388.
This commit is contained in:
@@ -645,6 +645,127 @@ def bootstrap():
|
||||
);
|
||||
expect(callsToUserService).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('resolves Go cross-package qualified calls via go.mod module path (#388)', async () => {
|
||||
// Pre-#388, every `pkga.FuncX(...)` call in a Go monorepo was flagged
|
||||
// external (isExternalImport returned true for any non-`/internal/`
|
||||
// import without `.`-prefix) and resolution fell through to name-match
|
||||
// with path proximity — recall on cross-package callers was ~<1%.
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, 'go.mod'),
|
||||
'module github.com/example/myproject\n\ngo 1.21\n'
|
||||
);
|
||||
|
||||
const pkgaDir = path.join(tempDir, 'pkga');
|
||||
const pkgbDir = path.join(tempDir, 'pkgb');
|
||||
const pkgcDir = path.join(tempDir, 'pkgc');
|
||||
fs.mkdirSync(pkgaDir);
|
||||
fs.mkdirSync(pkgbDir);
|
||||
fs.mkdirSync(pkgcDir);
|
||||
|
||||
// Same-name exported function in two packages — only the imported one
|
||||
// should resolve. Exercises disambiguation, not just connectivity.
|
||||
fs.writeFileSync(
|
||||
path.join(pkgaDir, 'conv.go'),
|
||||
'package pkga\nfunc Convert(x int) int { return x * 2 }\n'
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(pkgbDir, 'conv.go'),
|
||||
'package pkgb\nfunc Convert(x int) int { return x + 1 }\n'
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(pkgcDir, 'use.go'),
|
||||
`package pkgc
|
||||
|
||||
import "github.com/example/myproject/pkga"
|
||||
|
||||
func UsePkga() {
|
||||
pkga.Convert(5)
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
cg = await CodeGraph.init(tempDir, { index: true });
|
||||
|
||||
const usePkga = cg.getNodesByKind('function').filter((n) => n.name ==='UsePkga')[0];
|
||||
expect(usePkga).toBeDefined();
|
||||
|
||||
const outgoing = cg.getOutgoingEdges(usePkga!.id);
|
||||
const callEdges = outgoing.filter((e) => e.kind === 'calls');
|
||||
expect(callEdges).toHaveLength(1);
|
||||
|
||||
const target = cg.getNode(callEdges[0]!.target);
|
||||
expect(target?.name).toBe('Convert');
|
||||
// Critical: the resolver must pick the imported pkga's Convert,
|
||||
// not pkgb's. With the broken (pre-fix) resolver this lands on
|
||||
// whichever Convert happens to be cheaper under path proximity.
|
||||
expect(target?.filePath.replace(/\\/g, '/')).toBe('pkga/conv.go');
|
||||
});
|
||||
|
||||
it('resolves Go aliased imports across packages (#388)', async () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, 'go.mod'),
|
||||
'module github.com/example/myproject\n\ngo 1.21\n'
|
||||
);
|
||||
fs.mkdirSync(path.join(tempDir, 'pkgb'));
|
||||
fs.mkdirSync(path.join(tempDir, 'pkgd'));
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, 'pkgb', 'lib.go'),
|
||||
'package pkgb\nfunc Compute(x int) int { return x }\n'
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, 'pkgd', 'use.go'),
|
||||
`package pkgd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
alias "github.com/example/myproject/pkgb"
|
||||
)
|
||||
|
||||
func UseAliased() {
|
||||
fmt.Println("hi")
|
||||
alias.Compute(3)
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
cg = await CodeGraph.init(tempDir, { index: true });
|
||||
|
||||
const useAliased = cg.getNodesByKind('function').filter((n) => n.name ==='UseAliased')[0];
|
||||
expect(useAliased).toBeDefined();
|
||||
const calls = cg.getOutgoingEdges(useAliased!.id).filter((e) => e.kind === 'calls');
|
||||
// fmt.Println is stdlib — must stay external. alias.Compute must resolve.
|
||||
expect(calls).toHaveLength(1);
|
||||
const target = cg.getNode(calls[0]!.target);
|
||||
expect(target?.name).toBe('Compute');
|
||||
expect(target?.filePath.replace(/\\/g, '/')).toBe('pkgb/lib.go');
|
||||
});
|
||||
|
||||
it('Go: leaves stdlib calls (fmt.Println, etc.) external', async () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, 'go.mod'),
|
||||
'module github.com/example/myproject\n\ngo 1.21\n'
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, 'main.go'),
|
||||
`package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
fmt.Println("hi")
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
cg = await CodeGraph.init(tempDir, { index: true });
|
||||
|
||||
const mainFn = cg.getNodesByKind('function').filter((n) => n.name ==='main')[0];
|
||||
const calls = cg.getOutgoingEdges(mainFn!.id).filter((e) => e.kind === 'calls');
|
||||
// No spurious in-project edge — fmt.* must stay unresolved/external.
|
||||
expect(calls).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Name Matcher: kind bias for new ref kinds', () => {
|
||||
|
||||
Reference in New Issue
Block a user