mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-18 13:53:01 +08:00
787f75fbc9
* fix(go): a type written bare links its own package's, not a namesake struct elsewhere The Go framework resolver's name heuristics (Patterns 1-4) picked a same-named declaration by folder convention and fell back to the first candidate anywhere, outranking name matching on a tie. Go reads a bare name only from the reference's own package, so every cross-package pick was wrong: promql/parser's `Node` parameters went to discovery/kubernetes's struct, `apiv1.Node` to the same file's struct, and method calls through an expression (`err[i].Error()`, a chained `.String()`) became instantiations of structs named like the method. - The patterns guess only for a name written bare, and only in its own package; Pattern 4 takes any type there, not only structs. - isGoBareName is the one check for "Go reads this name from its own package"; a Go call through an expression never lands on a type. - Go's if/switch/for/range join the bare-call keywords, so `if T(x)` conversions stay bare. - goRefQualifier reads a variadic `...pkg.T` as written through `pkg`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(changelog): the Go type fix covers exported types; name the before-state precisely Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
204 lines
5.8 KiB
TypeScript
204 lines
5.8 KiB
TypeScript
/**
|
||
* 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', () => {
|
||
expect(linksFrom('errors.go', 'Error', { name: '_' })).toEqual(['instantiates 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']);
|
||
});
|
||
});
|