mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-24 19:30:10 +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>
115 lines
3.3 KiB
TypeScript
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',
|
|
]);
|
|
});
|
|
});
|