Files
codegraph/src/resolution/go-module.ts
T
Colby Mchenry deac7710f2 fix(go): resolve unexported receivers and nested modules within the right package (#2323, #2322) (#2361)
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>
2026-10-05 21:16:14 +00:00

82 lines
3.3 KiB
TypeScript

/**
* 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.
*
* A project can hold several modules — a Go backend in `server/` next to a
* frontend, or etcd's root module beside `server/go.mod` and
* `client/v3/go.mod` — so the resolver reads the `go.mod` nearest every
* directory with indexed Go files and maps an import path to the module
* that declares the longest prefix of it (#2322).
*/
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 in `moduleDir` and extract the module path.
* Returns `null` if no `go.mod` exists there or it has no `module` directive.
*/
export function loadGoModule(moduleDir: string): GoModule | null {
const goModPath = path.join(moduleDir, '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: moduleDir };
}
/**
* The module an import path belongs to: the one whose module path equals it
* or is a `/`-bounded prefix of it. Nested modules (`example.com/app` and
* `example.com/app/tools`) take the longest module path, as Go does; two
* modules declaring the same path prefer `own`, the importing file's module.
* `null` for the standard library and third-party modules.
*/
export function findGoModuleForImport(
importPath: string,
modules: readonly GoModule[],
own?: GoModule | null
): GoModule | null {
let best: GoModule | null = null;
for (const mod of own && !modules.includes(own) ? [...modules, own] : modules) {
if (importPath !== mod.modulePath && !importPath.startsWith(`${mod.modulePath}/`)) continue;
const length = best ? best.modulePath.length : -1;
if (mod.modulePath.length > length || (mod.modulePath.length === length && mod === own)) best = mod;
}
return best;
}
/**
* The project-relative directory (`/`-separated, `.` for the project root)
* of the package `importPath` names inside module `mod`: the module's own
* directory followed by the rest of the import path.
*/
export function goModulePackageDir(importPath: string, mod: GoModule, projectRoot: string): string {
const modDir = path.relative(projectRoot, mod.rootDir).split(path.sep).join('/');
const rest = importPath === mod.modulePath ? '' : importPath.slice(mod.modulePath.length + 1);
return path.posix.join(modDir, rest);
}