mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-15 02:44:22 +08:00
fix(go): a call through a type assertion reaches the asserted type's method (#2444)
Both extractors record `srv.(KVServer).Range(ctx, in)` as a bare `Range` call at the column where its receiver expression starts, as they record any call through an expression. Resolution matched the name alone, so every handler protoc-gen-go-grpc emits went to the `UnimplementedKVServer` stub beside the `KVServer` interface (72 edges in etcd), and calls like `c.Reader.(*pipe).Close()`, `p.(Pausable).Pause()` or `v.(featuregate.MutableFeatureGate).Set(…)` linked to a namesake or to nothing. The call's chain is now read back from its column (operand, selectors, assertions, calls and indexes, past strings, runes and comments, and on to the next line after a trailing `.`). When the link of the call's name follows an assertion, the call is a method of the asserted type, found where Go finds it: - a bare `T` or `*T` in the call's own package, or one it dot-imports; - a `pkg.T` in the imported project package; - its own method, the one its interface declares, or one promoted from a type it embeds, through the package-scoped lookup #2361 added; an alias is followed to the type it names (#2417). A type from outside the project (`http.Flusher`), a predeclared one (`error`), an alias of an outside type (`type Ctx = context.Context`, for which the package-scoped lookup would fall back to matching by name) or a type literal (`interface{ Flush() }`) links nothing. The check runs ahead of the framework, import and name strategies, and ahead of the built-in filter, which dropped a method named `close` or `copy` called through an assertion. Two calls of one name in a chain share the column (`b.(*Builder).Add(1).Add(2)`); both are taken for the one made through the assertion, whose edge is there either way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
9a1d4bd91d
commit
3de5bef0d2
@@ -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, isGoBareName, goTypePositionTarget, GO_TYPE_KINDS } 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, goTypePositionTarget, GO_TYPE_KINDS, matchGoAssertedCall } from './name-matcher';
|
||||
import { isVisibleCppMacro, clearCppMacroVisibility } from './cpp-macro-visibility';
|
||||
import { isCppConstructorRef, matchCppConstructor } from './cpp-constructor';
|
||||
import { isCppSupertypeRef, matchCppSupertype, clearCppSupertypeMemos } from './cpp-supertypes';
|
||||
@@ -1233,6 +1233,11 @@ export class ReferenceResolver {
|
||||
// A Dart annotation (`@riverpod`, `@Riverpod(…)`) is a constant or a
|
||||
// constructor call, as written — never a method or function by its name.
|
||||
if (isDartAnnotation(ref)) return matchDartAnnotation(ref, this.context);
|
||||
// A Go call through a type assertion (`srv.(KVServer).Range(ctx, in)`),
|
||||
// which arrives by its bare name, is to a method of the asserted type, or
|
||||
// to nothing the project declares.
|
||||
const asserted = matchGoAssertedCall(ref, this.context);
|
||||
if (asserted !== undefined) return asserted;
|
||||
|
||||
// A section or snippet a Shopify theme's file names is that theme's own,
|
||||
// or nothing: Shopify never looks in another theme (see ./shopify-themes).
|
||||
|
||||
@@ -9189,6 +9189,7 @@ export function clearNameMatcherMemos(context: ResolutionContext): void {
|
||||
GO_QUALIFIERS.delete(context);
|
||||
GO_EMBEDS.delete(context);
|
||||
GO_ALIAS_TARGETS.delete(context);
|
||||
GO_DOT_IMPORTS.delete(context);
|
||||
JAVA_FILE_SCOPES.delete(context);
|
||||
JAVA_ANCESTORS.delete(context);
|
||||
SCALA_SUPERS.delete(context);
|
||||
@@ -11014,6 +11015,162 @@ function goEmbeddedTypes(typeNode: Node, context: ResolutionContext): Array<{ na
|
||||
return embedded;
|
||||
}
|
||||
|
||||
/**
|
||||
* A Go call made through a type assertion — `srv.(KVServer).Range(ctx, in)`,
|
||||
* `c.Reader.(*pipe).Close()`, `v.(storage.Store).Fetch(k)` — calls a method of
|
||||
* the asserted type: its own, the one its interface declares, or one embedding
|
||||
* promotes into it. The call reaches the resolver by its bare name, at the
|
||||
* column where its receiver expression starts, and name matching took any
|
||||
* method of that name: every gRPC handler etcd generates went to the
|
||||
* `UnimplementedKVServer` stub beside the `KVServer` interface. The type is
|
||||
* the one Go finds: in the call's own package or a package it dot-imports for
|
||||
* a bare name, in the imported package for a qualified one, and an alias is
|
||||
* the type it names. A type found in none — one from outside the project
|
||||
* (`http.Flusher`), a predeclared one (`error`) — an alias of such a type, or
|
||||
* a type literal (`interface{ Flush() }`) links nothing.
|
||||
* Undefined when the call is not made through an assertion.
|
||||
*/
|
||||
export function matchGoAssertedCall(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null | undefined {
|
||||
if (ref.language !== 'go' || ref.referenceKind !== 'calls' || !/^[A-Za-z_]\w*$/.test(ref.referenceName)) return undefined;
|
||||
const asserted = goAssertedType(ref, context);
|
||||
if (asserted === undefined) return undefined;
|
||||
// `KVServer`, `*pipe`, `storage.Store`, `*List[int]`: anything else is a type literal.
|
||||
const type = /^\*?\s*(?:([A-Za-z_]\w*)\s*\.\s*)?([A-Za-z_]\w*)\s*(?:\[[\s\S]*\])?$/.exec(asserted);
|
||||
if (!type) return null;
|
||||
const [, qualifier, typeName] = type;
|
||||
const declares = (dir: string | null | undefined): dir is string => !!dir && goPackageTypes(typeName!, dir, context).length > 0;
|
||||
const dir = qualifier !== undefined
|
||||
? goImportPackageDir(qualifier, ref.filePath, context)
|
||||
: [goPackageDir(ref.filePath), ...goDotImportDirs(ref.filePath, context)].find(declares);
|
||||
if (!declares(dir)) return null;
|
||||
// An alias of a type from outside the project (`type Ctx = context.Context`)
|
||||
// has that type's methods, none the project declares.
|
||||
if (goPackageTypes(typeName!, dir, context).every((t) => goAliasTarget(t, context) === null)) return null;
|
||||
return resolveMethodOnType(typeName!, ref.referenceName, ref, context, 0.9, 'instance-method', dir);
|
||||
}
|
||||
|
||||
/** How many lines past a Go call's first its chain is read on. */
|
||||
const GO_CHAIN_LINES = 30;
|
||||
|
||||
/**
|
||||
* The type a Go call's receiver is asserted to, as written (`KVServer`,
|
||||
* `*pipe`, `storage.Store`). Every call of a chain records the chain's start,
|
||||
* so the call is a `.name(…)` link of the chain read from there, and the link
|
||||
* before it is the assertion. Two links of one name in a chain
|
||||
* (`b.(*Builder).Add(1).Add(2)`) are told apart by nothing, and both are taken
|
||||
* for the one made through the assertion: that call is there either way.
|
||||
* Undefined when no link of the name follows an assertion, or no chain
|
||||
* starts at the column.
|
||||
*/
|
||||
function goAssertedType(ref: UnresolvedRef, context: ResolutionContext): string | undefined {
|
||||
const lines = context.getFileLines?.(ref.filePath) ?? context.readFile(ref.filePath)?.split(/\r?\n/);
|
||||
const line = lines?.[ref.line - 1];
|
||||
if (!lines || line === undefined || ref.column >= line.length) return undefined;
|
||||
// Most calls through an expression assert nothing: no `.(` after the
|
||||
// column, nor a chain that goes on past the line.
|
||||
if (line.indexOf('.(', ref.column) < 0 && !/\.\s*$/.test(line)) return undefined;
|
||||
const links = goChainLinks(lines.slice(ref.line - 1, ref.line + GO_CHAIN_LINES).join('\n'), ref.column);
|
||||
for (let k = 1; k + 1 < links.length; k++) {
|
||||
if (links[k - 1]!.kind === 'assert' && links[k]!.kind === 'select' && links[k]!.text === ref.referenceName &&
|
||||
links[k + 1]!.kind === 'call') return links[k - 1]!.text;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
interface GoChainLink {
|
||||
kind: 'operand' | 'select' | 'assert' | 'call' | 'index';
|
||||
/** An operand's or selector's name, an assertion's type. */
|
||||
text: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The links of the Go expression that starts at `start`: its operand (a name
|
||||
* or a parenthesized expression), then each selector, type assertion, call
|
||||
* and index. A line break ends it, except right after a `.`, where Go
|
||||
* inserts no semicolon.
|
||||
*/
|
||||
function goChainLinks(text: string, start: number): GoChainLink[] {
|
||||
const links: GoChainLink[] = [];
|
||||
const NAME = /[A-Za-z_]\w*/y;
|
||||
let i = start;
|
||||
const name = (): string | null => {
|
||||
NAME.lastIndex = i;
|
||||
const m = NAME.exec(text);
|
||||
if (m) i += m[0].length;
|
||||
return m ? m[0] : null;
|
||||
};
|
||||
const group = (): string | null => {
|
||||
const close = goClosingBracket(text, i);
|
||||
if (close < 0) return null;
|
||||
const inner = text.slice(i + 1, close);
|
||||
i = close + 1;
|
||||
return inner;
|
||||
};
|
||||
const operand = text[i] === '(' ? group() : name();
|
||||
if (operand === null) return links;
|
||||
links.push({ kind: 'operand', text: operand });
|
||||
for (;;) {
|
||||
while (text[i] === ' ' || text[i] === '\t') i++;
|
||||
if (text[i] === '.') {
|
||||
i++;
|
||||
while (i < text.length && WHITESPACE.test(text[i]!)) i++;
|
||||
const type = text[i] === '(' ? group() : null;
|
||||
const member = type === null ? name() : null;
|
||||
if (type !== null) links.push({ kind: 'assert', text: type.trim() });
|
||||
else if (member !== null) links.push({ kind: 'select', text: member });
|
||||
else break;
|
||||
} else if (text[i] === '(' || text[i] === '[') {
|
||||
// A call whose arguments run past what was read is still a call.
|
||||
links.push({ kind: text[i] === '(' ? 'call' : 'index', text: '' });
|
||||
if (group() === null) break;
|
||||
} else break;
|
||||
}
|
||||
return links;
|
||||
}
|
||||
|
||||
/** The bracket closing the one at `open`, past Go strings, runes and comments; -1 when it does not close. */
|
||||
function goClosingBracket(text: string, open: number): number {
|
||||
let depth = 0;
|
||||
for (let i = open; i < text.length; i++) {
|
||||
const c = text[i]!;
|
||||
if (c === '"' || c === '\'') {
|
||||
for (i++; i < text.length && text[i] !== c && text[i] !== '\n'; i++) if (text[i] === '\\') i++;
|
||||
} else if (c === '`') {
|
||||
i = text.indexOf('`', i + 1);
|
||||
if (i < 0) return -1;
|
||||
} else if (c === '/' && text[i + 1] === '/') {
|
||||
i = text.indexOf('\n', i);
|
||||
if (i < 0) return -1;
|
||||
} else if (c === '/' && text[i + 1] === '*') {
|
||||
i = text.indexOf('*/', i + 2);
|
||||
if (i < 0) return -1;
|
||||
i++;
|
||||
} else if (c === '(' || c === '[' || c === '{') depth++;
|
||||
else if ((c === ')' || c === ']' || c === '}') && --depth === 0) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
const GO_DOT_IMPORTS = new WeakMap<ResolutionContext, Map<string, string[]>>();
|
||||
|
||||
/** The project packages a Go file dot-imports (`import . "example.com/app/storage"`), by directory: their names it reads bare. */
|
||||
function goDotImportDirs(filePath: string, context: ResolutionContext): string[] {
|
||||
let memo = GO_DOT_IMPORTS.get(context);
|
||||
if (!memo) GO_DOT_IMPORTS.set(context, (memo = new Map()));
|
||||
let dirs = memo.get(filePath);
|
||||
if (dirs) return dirs;
|
||||
dirs = [];
|
||||
for (const line of context.getFileLines?.(filePath) ?? context.readFile(filePath)?.split(/\r?\n/) ?? []) {
|
||||
// The imports come before any other declaration.
|
||||
if (/^(?:func|type|var|const)\b/.test(line)) break;
|
||||
const dot = /^\s*(?:import\s+)?\.\s+"([^"]+)"/.exec(line);
|
||||
const dir = dot ? context.getGoPackageDir?.(dot[1]!, filePath) : null;
|
||||
if (dir) dirs.push(dir);
|
||||
}
|
||||
memo.set(filePath, dirs);
|
||||
return dirs;
|
||||
}
|
||||
|
||||
/** Go builtin/primitive field types that can never carry a project method. */
|
||||
const GO_BUILTIN_FIELD_TYPES = new Set([
|
||||
'string', 'bool', 'byte', 'rune', 'error', 'any',
|
||||
|
||||
Reference in New Issue
Block a user