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
co-authored by Claude Opus 5.5
parent 9a515c3e64
commit 787f75fbc9
5 changed files with 286 additions and 37 deletions
+34 -31
View File
@@ -7,6 +7,7 @@
import { Node } from '../../types';
import { FrameworkResolver, UnresolvedRef, ResolvedRef, ResolutionContext } from '../types';
import { stripCommentsForRegex } from '../strip-comments';
import { GO_TYPE_KINDS, isGoBareName } from '../name-matcher';
import { pickByNameAndKind } from './name-heuristic';
export const goResolver: FrameworkResolver = {
@@ -26,18 +27,24 @@ export const goResolver: FrameworkResolver = {
},
resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
// An embedded type is the one its package declares under that name: the
// same package's for `Node`, the import's for `pkg.Node`, which name
// matching and the import resolver work out. The patterns below pick among
// every same-named node by folder convention, and Pattern 4 sees structs
// only: `Node` embedded beside promql/parser's `type Node interface` went
// to discovery/kubernetes's struct `Node`, etcd's `Client` embedding its
// own `Lease` interface to the server's `Lease` struct.
// The patterns below guess a declaration from the shape of a name, but Go
// reads a name from one package only: one written bare from the
// reference's own, `pkg.Node` from that import's (the import resolver's
// job), `x[i].Error()` from whatever type `x[i]` has. So they guess only
// for a bare name, only in its own package, and never for an embedded
// type, which name matching and the import resolver work out. Past that,
// promql/parser's `Node` parameters and embeddings went to
// discovery/kubernetes's struct `Node` beside the package's own `Node`
// interface, `apiv1.Node` to the struct in the same file, etcd's `Client`
// embedding its own `Lease` interface to the server's `Lease` struct, and
// every `.String()` called through an expression to the struct
// `promql.String`, as an instantiation.
if (ref.referenceKind === 'extends' || ref.referenceKind === 'implements') return null;
if (!isGoBareName(ref, context)) return null;
// Pattern 1: Handler references
if (ref.referenceName.endsWith('Handler') || ref.referenceName.startsWith('Handle')) {
const result = resolveByNameAndKind(ref, 'function', HANDLER_DIRS, context);
const result = resolveInOwnPackage(ref, FUNCTION_KINDS, context);
if (result) {
return {
original: ref,
@@ -50,7 +57,7 @@ export const goResolver: FrameworkResolver = {
// Pattern 2: Service/Repository references
if (ref.referenceName.endsWith('Service') || ref.referenceName.endsWith('Repository') || ref.referenceName.endsWith('Store')) {
const result = resolveByNameAndKind(ref, null, SERVICE_DIRS, context, SERVICE_KINDS);
const result = resolveInOwnPackage(ref, SERVICE_KINDS, context);
if (result) {
return {
original: ref,
@@ -63,7 +70,7 @@ export const goResolver: FrameworkResolver = {
// Pattern 3: Middleware references
if (ref.referenceName.endsWith('Middleware') || ref.referenceName.startsWith('Auth') || ref.referenceName.startsWith('Log')) {
const result = resolveByNameAndKind(ref, 'function', MIDDLEWARE_DIRS, context);
const result = resolveInOwnPackage(ref, FUNCTION_KINDS, context);
if (result) {
return {
original: ref,
@@ -74,9 +81,12 @@ export const goResolver: FrameworkResolver = {
}
}
// Pattern 4: Model/Entity references (typically PascalCase structs)
// Pattern 4: Model/Entity references — a PascalCase type of any kind: the
// package's `Node` interface or `Entry` type, not only its structs. Left
// to name matching, the result type of `func (f *fanout) Appender(…)
// Appender` linked to the method the line declares.
if (/^[A-Z][a-zA-Z]+$/.test(ref.referenceName)) {
const result = resolveByNameAndKind(ref, 'struct', MODEL_DIRS, context);
const result = resolveInOwnPackage(ref, GO_TYPE_KINDS, context);
if (result) {
return {
original: ref,
@@ -175,24 +185,17 @@ function extractGoTailIdent(expr: string): string | null {
return m ? m[1]! : null;
}
// Directory patterns for framework resolution
const HANDLER_DIRS = ['handler', 'handlers', 'api', 'routes', 'controller', 'controllers'];
const SERVICE_DIRS = ['service', 'services', 'repository', 'store', 'pkg'];
const MIDDLEWARE_DIRS = ['middleware', 'middlewares'];
const MODEL_DIRS = ['model', 'models', 'entity', 'entities', 'domain', 'pkg'];
const SERVICE_KINDS = new Set(['struct', 'interface']);
const FUNCTION_KINDS: ReadonlySet<string> = new Set(['function']);
const SERVICE_KINDS: ReadonlySet<string> = new Set(['struct', 'interface']);
/** A framework name heuristic's pick (see name-heuristic.ts), preferring these folders. */
function resolveByNameAndKind(
ref: UnresolvedRef,
kind: string | null,
preferredDirs: string[],
context: ResolutionContext,
kinds?: Set<string>
): string | null {
const allowed: ReadonlySet<string> = kinds ?? (kind ? new Set([kind]) : GO_NAMED_KINDS);
return pickByNameAndKind(ref, allowed, (f) => preferredDirs.some((d) => f.includes(`/${d}/`)), context);
/**
* A framework name heuristic's pick (see name-heuristic.ts) among the
* declarations of the reference's own package — its directory — its own
* file's first. No folder convention applies: Go never reads a bare name from
* another package, a dot import aside.
*/
function resolveInOwnPackage(ref: UnresolvedRef, kinds: ReadonlySet<string>, context: ResolutionContext): string | null {
const dir = ref.filePath.slice(0, ref.filePath.lastIndexOf('/') + 1);
const inPackage = (n: Node) => n.filePath.startsWith(dir) && !n.filePath.slice(dir.length).includes('/');
return pickByNameAndKind(ref, kinds, () => false, context, { accept: inPackage });
}
/** Any declaration a name can be (the heuristic with no kind). */
const GO_NAMED_KINDS: ReadonlySet<string> = new Set(['function', 'method', 'struct', 'interface', 'type_alias', 'variable', 'constant']);