mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-25 12:00:02 +08:00
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>
146 lines
4.0 KiB
TypeScript
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([]);
|
|
});
|
|
});
|