mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-15 13:14:03 +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:
co-authored by
Claude Opus 5.5
parent
9a515c3e64
commit
787f75fbc9
@@ -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']);
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
isImportableKind,
|
||||
CPP_DEFINE_SIGNATURE,
|
||||
} from './types';
|
||||
import { isPythonSelfCall, matchJsStoreBindingCall, isUnresolvedJsMemberCall, matchObjectPathCall, thisScopeCaller, isVisibleAcrossFiles, matchReference, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, matchMethodCall, sameLanguageFamily, crossesCodeBoundary, gateLanguageMatch, dumpNameMatcherProfile, clearNameMatcherMemos, isRustNameInScope, CASE_INSENSITIVE_LANGUAGES, isDartMemberRead, matchDartMemberRead, isDartChainLink, matchDartChainLink, isDartAnnotation, matchDartAnnotation, isStdMethodName, isGoUnknownQualified } from './name-matcher';
|
||||
import { isPythonSelfCall, matchJsStoreBindingCall, isUnresolvedJsMemberCall, matchObjectPathCall, thisScopeCaller, isVisibleAcrossFiles, matchReference, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, matchMethodCall, sameLanguageFamily, crossesCodeBoundary, gateLanguageMatch, dumpNameMatcherProfile, clearNameMatcherMemos, isRustNameInScope, CASE_INSENSITIVE_LANGUAGES, isDartMemberRead, matchDartMemberRead, isDartChainLink, matchDartChainLink, isDartAnnotation, matchDartAnnotation, isStdMethodName, isGoUnknownQualified, isGoBareName, GO_TYPE_KINDS } from './name-matcher';
|
||||
import { isVisibleCppMacro, clearCppMacroVisibility } from './cpp-macro-visibility';
|
||||
import { isCppConstructorRef, matchCppConstructor } from './cpp-constructor';
|
||||
import { gateSwiftTypeTarget, clearSwiftTypeVisibility, swiftExtendedConformances } from './swift-type-visibility';
|
||||
@@ -3036,6 +3036,12 @@ export class ReferenceResolver {
|
||||
if (ref.referenceKind === 'calls') {
|
||||
const target = this.nodeById(result.targetNodeId);
|
||||
if (target?.kind === 'constant' && CPP_DEFINE_SIGNATURE.test(target.signature ?? '')) return null;
|
||||
// A Go call made through an expression — `h.Validate().Error()`, a
|
||||
// `.Metrics()` chained onto the line above — calls a method of whatever
|
||||
// the expression is. A type is called only bare (`T(x)`, `(*T)(x)`) or
|
||||
// through its package; as a struct here it read as an instantiation.
|
||||
if (target && ref.language === 'go' && GO_TYPE_KINDS.has(target.kind) &&
|
||||
!ref.referenceName.includes('.') && !isGoBareName(ref, this.context)) return null;
|
||||
}
|
||||
|
||||
// An `imports` reference names something importable — never a member that
|
||||
|
||||
@@ -1905,9 +1905,10 @@ function goRefQualification(ref: UnresolvedRef, context: ResolutionContext): GoQ
|
||||
const line = context.getFileLines?.(ref.filePath)?.[ref.line - 1] ?? context.readFile(ref.filePath)?.split(/\r?\n/)[ref.line - 1] ?? '';
|
||||
const at = Math.max(0, ref.column);
|
||||
// The qualifier right before the name at the reference's column, or the
|
||||
// line's only spelling of the name.
|
||||
const written = line.startsWith(name, at) ? /(?:^|[^\w.])([A-Za-z_]\w*)\.$/.exec(line.slice(0, at))?.[1]
|
||||
: !new RegExp(`(?<![\\w.])${name}\\b`).test(line) ? new RegExp(`(?:^|[^\\w.])([A-Za-z_]\\w*)\\.${name}\\b`).exec(line)?.[1] : undefined;
|
||||
// line's only spelling of the name. A variadic `...chunks.Meta` is written
|
||||
// through `chunks` too: the ellipsis is no receiver.
|
||||
const written = line.startsWith(name, at) ? /(?:^|[^\w.]|\.{3})([A-Za-z_]\w*)\.$/.exec(line.slice(0, at))?.[1]
|
||||
: !new RegExp(`(?<![\\w.])${name}\\b`).test(line) ? new RegExp(`(?:^|[^\\w.]|\\.{3})([A-Za-z_]\\w*)\\.${name}\\b`).exec(line)?.[1] : undefined;
|
||||
const imported = written ? context.getImportMappings(ref.filePath, 'go').find((m) => m.localName === written) : undefined;
|
||||
const found = { written, imported };
|
||||
memo.set(key, found);
|
||||
@@ -2396,6 +2397,37 @@ function isBareGoCall(ref: UnresolvedRef, context: ResolutionContext): boolean {
|
||||
return ref.language === 'go' && isReceiverLessCall(ref, context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a Go reference's name is written bare, so that Go reads it from the
|
||||
* reference's own package: a type `Node` (variadic `...Node` too), a composite
|
||||
* literal `&Event{}`, a route's handler `Index`, a call `Walk(v, n)` or a
|
||||
* conversion `(*Block)(pb)`. Not `parser.Node` or `...chunks.Meta`, written
|
||||
* through an import, nor a name reached through a value: `err[i].Error()`, a
|
||||
* `.String()` chained onto the line above, a route's handler `h.Follow`.
|
||||
*/
|
||||
export function isGoBareName(ref: UnresolvedRef, context: ResolutionContext): boolean {
|
||||
const name = ref.referenceName;
|
||||
if (ref.language !== 'go' || !/^[A-Za-z_]\w*$/.test(name)) return false;
|
||||
if (ref.referenceKind === 'calls' && isReceiverLessCall(ref, context)) return true;
|
||||
const line = context.getFileLines?.(ref.filePath)?.[ref.line - 1]
|
||||
?? context.readFile(ref.filePath)?.split(/\r?\n/)[ref.line - 1];
|
||||
if (line === undefined) return false;
|
||||
// A call's column is its expression's: only a conversion's parenthesized
|
||||
// type is still bare there.
|
||||
if (ref.referenceKind === 'calls') {
|
||||
return line[ref.column] === '(' && new RegExp(`^\\(\\s*\\*?\\s*${name}\\s*\\)\\s*\\(`).test(line.slice(ref.column));
|
||||
}
|
||||
if (line.startsWith(name, ref.column)) {
|
||||
let end = ref.column;
|
||||
while (end > 0 && WHITESPACE.test(line[end - 1]!)) end--;
|
||||
return line[end - 1] !== '.' || (end >= 3 && line.slice(end - 3, end) === '...');
|
||||
}
|
||||
// A route's handler is recorded at the start of its line: its spelling
|
||||
// there, outside the path string.
|
||||
const code = line.replace(/"(?:[^"\\]|\\.)*"|`[^`]*`/g, (s) => ' '.repeat(s.length));
|
||||
return new RegExp(`(?<![\\w.])${name}\\b`).test(code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether an R call is a plain function call — `range(x)`, `vars(a)` — not a
|
||||
* ggproto / R6 method through `obj$m(…)` or `self$m(…)`. A method is only
|
||||
@@ -2467,9 +2499,13 @@ const WHITESPACE = /\s/;
|
||||
const WORD_CHAR = /\w/;
|
||||
/** A character that ends a receiver: `.`, a word character, `$`, `]` or `)`. */
|
||||
const RECEIVER_TAIL_CHAR = /[.\w$\])]/;
|
||||
/** Keywords after which a name starts an expression, so the call has no receiver. */
|
||||
/**
|
||||
* Keywords after which a name starts an expression, so the call has no
|
||||
* receiver — Go's `if Type(b) != Series`, `switch dirType(name)` included.
|
||||
*/
|
||||
const BARE_CALL_KEYWORDS: ReadonlySet<string> = new Set([
|
||||
'return', 'await', 'yield', 'typeof', 'void', 'new', 'else', 'case', 'throw', 'in', 'of', 'instanceof', 'go', 'defer',
|
||||
'if', 'switch', 'for', 'range',
|
||||
]);
|
||||
|
||||
/**
|
||||
@@ -10611,7 +10647,7 @@ function goDeclaredTypePackage(raw: string | undefined, filePath: string, contex
|
||||
}
|
||||
|
||||
/** The node kinds a Go `type` declaration produces. */
|
||||
const GO_TYPE_KINDS: ReadonlySet<string> = new Set(['struct', 'interface', 'type_alias']);
|
||||
export const GO_TYPE_KINDS: ReadonlySet<string> = new Set(['struct', 'interface', 'type_alias']);
|
||||
|
||||
/**
|
||||
* `methodName` on the Go type `typeName` that the package in directory
|
||||
|
||||
Reference in New Issue
Block a user