Files
codegraph/__tests__/go-conversion-references.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

115 lines
3.3 KiB
TypeScript

/**
* A Go call whose callee is a type is a conversion: `Category(s)` with
* `type Category string`, `(*Pair)(p)`, `model.Kind(3)`. It uses the type,
* and calls or instantiates nothing, so it is recorded as a `references`
* edge to the type. Before, it was a `calls` edge to a named non-struct type
* and an `instantiates` edge to a struct (#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/app\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-conv-'));
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 kind> <target> :<line>` of every edge 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 === 'instantiates' || e.kind === 'references')
.map((e) => {
const target = graph.getNode(e.target);
return `${e.kind} ${target?.kind} ${target?.qualifiedName} :${e.line}`;
})
.sort();
}
describe('Go type conversions (#2581)', () => {
it("records the issue's `Category(s)` as a use of the type, not a call", async () => {
const graph = await index({
'main.go': `package main
type Category string
func use(s string) Category {
return Category(s) // a conversion, not a call
}
func main() { use("") }
`,
});
expect(edgesFrom(graph, 'use')).toEqual([
'references type_alias Category :5',
'references type_alias Category :6',
]);
});
it('records conversions to struct, pointer, slice and imported types the same way, and keeps real calls', async () => {
const graph = await index({
'model/model.go': `package model
type Kind int
type Point struct{ X, Y int }
func Make() Kind { return 0 }
`,
'main.go': `package main
import "example.com/app/model"
type Celsius float64
type Pair struct{ X, Y int }
type Names []string
func convert(p *Pair, raw []string, v float64, pt model.Point) {
_ = Celsius(v)
_ = (*Pair)(p)
_ = Names(raw)
_ = model.Kind(3)
_ = Pair(pt)
_ = model.Make()
_ = Pair{X: 1}
}
func main() { convert(nil, nil, 0, model.Point{}) }
`,
});
const edges = edgesFrom(graph, 'convert');
expect(edges.filter((e) => /:1[2-8]$/.test(e))).toEqual([
'calls function Make :17',
'instantiates struct Pair :18',
'references struct Pair :13',
'references struct Pair :16',
'references type_alias Celsius :12',
'references type_alias Kind :15',
'references type_alias Names :14',
]);
});
});