`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:
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Go module path detection.
|
||||
*
|
||||
* A Go monorepo's cross-package calls (`pkga.FuncX(...)`) only resolve when
|
||||
* the resolver knows the project's module path (the `module ...` directive
|
||||
* in `go.mod`). Without it, `isExternalImport` treats every in-module import
|
||||
* — `github.com/example/myproject/pkga` — as a third-party package, so
|
||||
* resolution falls through to name-matching with path proximity and returns
|
||||
* a tiny fraction of the real call sites. See issue #388.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
export interface GoModule {
|
||||
/** The module path declared in `go.mod`, e.g. `github.com/example/myproject` */
|
||||
modulePath: string;
|
||||
/** Absolute path to the directory containing the `go.mod` file. */
|
||||
rootDir: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the `go.mod` file at the project root and extract the module path.
|
||||
* Returns `null` if no `go.mod` exists or it has no `module` directive.
|
||||
*
|
||||
* Limitation: only the project-root `go.mod` is read. Nested `go.mod` files
|
||||
* (Go workspaces, monorepos with multiple modules) are not yet resolved —
|
||||
* a follow-up if a real repro shows up.
|
||||
*/
|
||||
export function loadGoModule(projectRoot: string): GoModule | null {
|
||||
const goModPath = path.join(projectRoot, 'go.mod');
|
||||
let content: string;
|
||||
try {
|
||||
content = fs.readFileSync(goModPath, 'utf-8');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
// `module <path>` is the first non-comment directive in any valid go.mod.
|
||||
// Strip line comments so a `// module foo` doesn't false-match.
|
||||
const stripped = content.replace(/\/\/[^\n]*/g, '');
|
||||
const match = stripped.match(/^\s*module\s+(\S+)\s*$/m);
|
||||
if (!match) return null;
|
||||
// Strip optional quoting around the module path.
|
||||
const modulePath = match[1]!.replace(/^["']|["']$/g, '');
|
||||
if (!modulePath) return null;
|
||||
return { modulePath, rootDir: projectRoot };
|
||||
}
|
||||
Reference in New Issue
Block a user