Files
codegraph/__tests__/go-framework-own-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

205 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* The Go framework resolver's name heuristics ("a `…Handler` is a handler
* function", "a PascalCase name is a model struct") outrank name matching on a
* tie, so where they guess wrong nothing corrects them. Go reads a name from
* one package only — one written bare from the reference's own, `pkg.Name`
* from that import's, `x[i].Name()` from whatever type `x[i]` has — so they
* guess only for a bare name, and only in its own package. Before, they took
* a struct of that name from any package: prometheus's promql/parser `Node`
* parameters went to discovery/kubernetes's struct `Node` instead of the
* package's own `Node` interface, and every `.String()` called through an
* expression became an instantiation of the struct `promql.String`.
*/
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
let root = '';
let cg: CodeGraph;
beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-go-own-pkg-'));
const files: Record<string, string> = {
'go.mod': 'module example.com/app\n\ngo 1.22\n',
'promql/parser/ast.go': `package parser
type Node interface {
String() string
}
type Visitor interface {
Visit(node Node, path []Node) (Visitor, error)
}
func Walk(v Visitor, node Node, path []Node) error {
return nil
}
`,
'discovery/kubernetes/node.go': `package kubernetes
import apiv1 "k8s.io/api/core/v1"
type Node struct {
name string
}
func nodeSource(n *apiv1.Node) string {
return n.Name
}
`,
'storage/interface.go': `package storage
type Appender interface {
Append(ref uint64) error
}
type Filter interface {
Accept(value string) bool
}
`,
'storage/fanout.go': `package storage
type fanout struct{}
func (f *fanout) Appender() Appender {
return nil
}
`,
'discovery/aws/aws.go': `package aws
type Filter struct {
Name string
}
`,
'web/api/v1/search.go': `package v1
import "example.com/app/storage"
type ChainFilter struct{}
func NewChainFilter(filters ...storage.Filter) *ChainFilter {
return &ChainFilter{}
}
`,
'errors.go': `package gin
type Error struct {
Err error
}
func (msg *Error) Error() string {
return msg.Err.Error()
}
var _ error = (*Error)(nil)
func newError(err error) *Error {
return &Error{Err: err}
}
`,
'binding/validator.go': `package binding
import "strings"
type SliceValidationError []error
func (err SliceValidationError) Error() string {
var b strings.Builder
for i := range err {
b.WriteString(err[i].Error())
}
return b.String()
}
`,
'handler/handler.go': `package handler
type Handler struct{}
func (h *Handler) Follow(c Context) error {
return nil
}
type Context interface{}
`,
'handler/routes.go': `package handler
type Group interface {
POST(path string, h func(Context) error)
}
func (h *Handler) Register(g Group) {
g.POST("/:username/follow", h.Follow)
}
`,
'model/user.go': `package model
type Follow struct {
FollowerID uint
}
`,
};
for (const [rel, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), content);
}
cg = await CodeGraph.init(root, { index: true });
});
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
/**
* `kind file:qualifiedName` of each edge out of `file`'s nodes (of one kind,
* or named `from`) whose target is named `name`, deduplicated and sorted.
*/
const linksFrom = (file: string, name: string, from?: { kind?: string; name?: string }) => {
const sources = cg.getNodesInFile(file).filter((n) => (!from?.kind || n.kind === from.kind) && (!from?.name || n.name === from.name));
const links = cg.getOutgoingEdgesFrom(sources.map((n) => n.id))
.filter((e) => e.kind !== 'contains')
.map((e) => ({ kind: e.kind, target: cg.getNode(e.target)! }))
.filter(({ target }) => target.name === name)
.map(({ kind, target }) => `${kind} ${target.filePath}:${target.qualifiedName}`);
return [...new Set(links)].sort();
};
describe('Go framework heuristics pick only in the package Go reads the name from', () => {
it('a bare type is its own package’s interface, not another package’s struct', () => {
expect(linksFrom('promql/parser/ast.go', 'Node')).toEqual(['references promql/parser/ast.go:Node']);
});
it('a type written through an outside import is not the same-named struct of its own file', () => {
expect(linksFrom('discovery/kubernetes/node.go', 'Node')).toEqual([]);
});
it('a method called through an expression is not an instantiation of a same-named struct', () => {
const links = linksFrom('binding/validator.go', 'Error');
expect(links.filter((l) => l.startsWith('instantiates'))).toEqual([]);
expect(links).not.toContain('calls errors.go:Error');
});
it('a result type is its own package’s type, not the method the line declares', () => {
expect(linksFrom('storage/fanout.go', 'Appender')).toEqual(['references storage/interface.go:Appender']);
});
it('a variadic `...pkg.T` is that import’s type', () => {
expect(linksFrom('web/api/v1/search.go', 'Filter')).toEqual(['references storage/interface.go:Filter']);
});
it('a conversion and a composite literal still reach their own package’s struct', () => {
// `(*Error)(nil)` is a conversion: a use of the type, not an instantiation (#2581).
expect(linksFrom('errors.go', 'Error', { name: '_' })).toEqual(['references errors.go:Error']);
expect(linksFrom('errors.go', 'Error', { name: 'newError' })).toEqual([
'instantiates errors.go:Error',
'references errors.go:Error',
]);
});
it('a route’s handler written through a value is that value’s method, not a model struct', () => {
expect(linksFrom('handler/routes.go', 'Follow', { kind: 'route' })).toEqual(['references handler/handler.go:Handler::Follow']);
});
});