fix(resolution): Go cross-package qualified calls resolve via go.mod (#388) (#469)

`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:
Colby Mchenry
2026-05-26 17:14:35 -05:00
committed by GitHub
parent 7e0d9b9ec0
commit f1b79eeae1
7 changed files with 283 additions and 3 deletions
+12
View File
@@ -36,6 +36,18 @@ export const goExtractor: LanguageExtractor = {
if (typeChild.type === 'interface_type') return 'interface';
return undefined;
},
isExported: (node, source) => {
// Go: a symbol is exported when its identifier starts with an uppercase letter.
// Look at the `name` field directly (works for function_declaration,
// method_declaration, type_spec, and var_spec / const_spec via extractor flow).
const nameNode = getChildByField(node, 'name');
if (nameNode) {
const text = getNodeText(nameNode, source);
const first = text.charCodeAt(0);
return first >= 65 && first <= 90; // A-Z
}
return false;
},
getReceiverType: (node, source) => {
// Go method_declaration has a "receiver" field: func (sl *scrapeLoop) run(...)
// The receiver is a parameter_list containing a parameter_declaration