mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-17 05:07:37 +08:00
fix(go): a type written bare links its own package's, not a namesake struct elsewhere (#2408)
* 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>
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
/**
|
||||
* 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']);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user