Files
codegraph/__tests__/go-external-test-package.test.ts
T
Colby MchenryandClaude Opus 5.5 800e582d55 fix(go): conversions, external test packages and build-tag twins link as Go builds them (#2784)
Three Go shapes recorded edges the source doesn't make. A conversion
like Category(s), (*Pair)(p) or model.Kind(3) became a calls edge to
the type, or an instantiates edge to a struct. A bare call in a
package's external tests (package a_test) could bind to a same-named
helper of package a in the same directory, which a_test can't see. And
a name a package defines once per build target, like sep in sep_unix.go
and sep_other.go, linked to whichever definition came first, so a Linux
caller reached the !unix file.

A Go call whose target is a type is now a references edge. Two files of
one directory under different package clauses see each other's
top-level names only through an import: qualified, or bare after a dot
import and exported. A reference whose definition has build-constrained
twins in its package reaches the one that builds for the most typical
target its own file builds for: linux/amd64, then darwin/arm64,
windows/amd64 and the rest. Constraints are read from the file name's
GOOS/GOARCH suffix and the //go:build or // +build lines, with tags as
go build sets them by default.

Validation: 9 new tests in three files, all failing on main, and each
part switched off fails its own file; the Go tests pass. A/B against
main 0c3e12dd, edges changed as conversions / twin retargets: gin 56/0,
etcd 556/89, prometheus 812/5270, sonic 49/0, x/sys 550/3072, gopsutil
31/179, fsnotify 3/7, kubernetes 16039/328. Nodes and refs are
unchanged, and express and flask dumps are byte-identical. Every kind
change sits on a conversion-shaped call, and the twin retargets read
right by their files' build lines (prometheus labels to the default
stringlabels file, x/sys syscalls to syscall_unix_gc.go). Indexing
cost, wall/CPU seconds, median of 3: x/sys 6.7/20.5 -> 7.1/21.7, etcd
9.6/29.9 -> 9.8/30.0, prometheus 12.5/49.2 -> 12.9/50.2; kubernetes, 2
runs each, 108.1/385.8 -> 110.4/390.0.

Fixes #2581

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 06:19:32 +00:00

146 lines
4.0 KiB
TypeScript

/**
* A package's external tests (`package a_test`) share its directory but are
* a package of their own. A name one of them declares at the top level
* reaches the other only through an import: written `a.Real`, or bare after
* `import . "…/a"`, and then only an exported one. Before, a bare call in an
* external test bound to package `a`'s test helper of the same name (#2581).
*/
import { describe, it, expect, afterEach } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import CodeGraph from '../src/index';
const GO_MOD = 'module example.com/a\n\ngo 1.22\n';
let tempDir: string;
let cg: CodeGraph | null = null;
afterEach(() => {
cg?.close();
cg = null;
if (tempDir) fs.rmSync(tempDir, { recursive: true, force: true });
});
async function index(files: Record<string, string>): Promise<CodeGraph> {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-2581-xtest-'));
for (const [rel, source] of Object.entries({ 'go.mod': GO_MOD, ...files })) {
fs.mkdirSync(path.dirname(path.join(tempDir, rel)), { recursive: true });
fs.writeFileSync(path.join(tempDir, rel), source);
}
cg = await CodeGraph.init(tempDir, { index: true });
cg.resolveReferences();
return cg;
}
/** `<kind> <target> <target file>` of every call or type use out of the function `fromName`, sorted. */
function edgesFrom(graph: CodeGraph, fromName: string): string[] {
const from = graph.getNodesByKind('function').find((n) => n.name === fromName);
expect(from, fromName).toBeDefined();
return graph
.getOutgoingEdges(from!.id)
.filter((e) => e.kind === 'calls' || e.kind === 'references')
.map((e) => {
const target = graph.getNode(e.target);
return `${e.kind} ${target?.qualifiedName} ${target?.filePath}`;
})
.sort();
}
const PACKAGE_A = `package a
func Real() {}
func helper() {}
type fixture struct{}
`;
describe('Go external test packages (#2581)', () => {
it("binds the issue's external test to its own package's helper", async () => {
const graph = await index({
'a.go': 'package a\n\nfunc Real() {}\n',
'a_internal_test.go': `package a
import "testing"
func corpus(t *testing.T) []string { return nil }
func TestInternal(t *testing.T) { _ = corpus(t) }
`,
'b_ext_helpers_test.go': `package a_test
import "testing"
func corpus(t *testing.T, root string) []string { return nil }
`,
'a_ext_test.go': `package a_test
import "testing"
func TestExt(t *testing.T) { _ = corpus(t, "") }
`,
});
expect(edgesFrom(graph, 'TestExt')).toEqual(['calls corpus b_ext_helpers_test.go']);
expect(edgesFrom(graph, 'TestInternal')).toEqual(['calls corpus a_internal_test.go']);
});
it('reaches the package under test only through its import', async () => {
const graph = await index({
'a.go': PACKAGE_A,
'qualified_test.go': `package a_test
import (
"testing"
"example.com/a"
)
func TestQualified(t *testing.T) { a.Real() }
`,
'dot_test.go': `package a_test
import (
"testing"
. "example.com/a"
)
func TestDot(t *testing.T) { Real() }
func TestUnexported(t *testing.T) { helper() }
`,
'plain_test.go': `package a_test
type fixture struct{ name string }
`,
'use_test.go': `package a_test
func check(f fixture) {}
`,
});
expect(edgesFrom(graph, 'TestQualified')).toEqual(['calls Real a.go']);
expect(edgesFrom(graph, 'TestDot')).toEqual(['calls Real a.go']);
// A dot import brings only the exported names.
expect(edgesFrom(graph, 'TestUnexported')).toEqual([]);
expect(edgesFrom(graph, 'check')).toEqual(['references fixture plain_test.go']);
});
it("does not bind package a's code to a name only its external tests declare", async () => {
const graph = await index({
'a.go': 'package a\n\nfunc Real() {}\n',
'a_internal_test.go': `package a
import "testing"
func TestInternal(t *testing.T) { extOnly() }
`,
'ext_test.go': `package a_test
func extOnly() {}
`,
});
expect(edgesFrom(graph, 'TestInternal')).toEqual([]);
});
});