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:
Colby Mchenry
2026-10-07 02:27:30 +00:00
committed by GitHub
parent 9a515c3e64
commit 787f75fbc9
5 changed files with 286 additions and 37 deletions
+203
View File
@@ -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']);
});
});