fix(go): an interface's methods link the types their signatures name (#2659)

A Go interface's methods are `method` nodes, but their signatures named
no types: etcd's `KV.Put(ctx context.Context, key, val string, opts
...OpOption) (*PutResponse, error)` had no edge to `OpOption` or
`PutResponse`. A call through the interface reaches that method, so
impact on those types missed the interface method and the callers that go
through it.

TS extractor and Rust kernel, mirrored:
- extractGoInterfaceMethods / extract_go_interface_methods give each
  method a `references` ref per type name in its parameters and results
  (goInterfaceMethodTypeNames / interface_method_type_names, on #2655's
  collectGoTypeNames).
- Skipped: the predeclared types and, written bare, the interface's own
  type parameters (goTypeParameterNames / type_parameter_names, split out
  of goDeclaredNames). The interface's own name is a type its method names
  (`Children() []Node`), so it is kept.

A/B on main 6692e051: gin +34, etcd +775, prometheus +294, kubernetes
+11079 edges; every one judged against the real package clause (bare →
own package, `pkg.T` → the import's package, kubernetes' one dot import
included); 0 dropped/retargeted/metadata-only. Kernel and wasm indexes
are identical. Explore keeps every named body on 29 flow queries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-19 10:38:55 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 6692e05107
commit 383c2cd9e3
6 changed files with 383 additions and 10 deletions
+26 -1
View File
@@ -70,9 +70,15 @@ export function goEmbeddedTypeName(type: SyntaxNode | null | undefined, source:
* its own name, which in a recursive type is the declaration itself.
*/
function goDeclaredNames(decl: SyntaxNode, source: string): Set<string> {
const names = new Set<string>();
const names = goTypeParameterNames(decl, source);
const nameNode = getChildByField(decl, 'name');
if (nameNode) names.add(getNodeText(nameNode, source));
return names;
}
/** The type parameters a Go type declaration declares: `T` and `K` in `type Store[T any, K comparable] …`. */
function goTypeParameterNames(decl: SyntaxNode, source: string): Set<string> {
const names = new Set<string>();
for (const param of getChildByField(decl, 'type_parameters')?.namedChildren ?? []) {
for (const c of param.namedChildren) {
if (c.type === 'identifier') names.add(getNodeText(c, source));
@@ -150,6 +156,25 @@ export function goStructFieldTypeNames(node: SyntaxNode, source: string): Syntax
return names;
}
/**
* The name nodes of the types a Go interface method's signature names, in
* source order: `OpOption` and `PutResponse` in etcd's `Put(ctx
* context.Context, key, val string, opts ...OpOption) (*PutResponse, error)`,
* and `Context`, whose package stays in the source. `decl` declares the
* interface. Skipped: Go's predeclared types and, written bare, the
* interface's own type parameters (`T` in `type Store[T any] interface`). Its
* own name is a type the method names (`Children() []Node`).
*/
export function goInterfaceMethodTypeNames(method: SyntaxNode, decl: SyntaxNode, source: string): SyntaxNode[] {
const params = goTypeParameterNames(decl, source);
const names: SyntaxNode[] = [];
for (const field of ['parameters', 'result']) {
const part = getChildByField(method, field);
if (part) collectGoTypeNames(part, params, source, names);
}
return names;
}
export const goExtractor: LanguageExtractor = {
functionTypes: ['function_declaration'],
classTypes: [], // Go doesn't have classes
+18 -4
View File
@@ -25,7 +25,7 @@ import { stripCppTemplateArgs, isCppConstructorDeclaration, cDeclaratorIdentifie
import { NestedIntervals, scanCppBraceScopes, type CppBraceScopes } from './languages/cpp-brace-scopes';
import { appendCppMacroMethods } from './languages/cpp-macro-methods';
import { rustImplTypeName } from './languages/rust';
import { goAliasTypeNames, goEmbeddedTypeName, goStructFieldTypeNames } from './languages/go';
import { goAliasTypeNames, goEmbeddedTypeName, goInterfaceMethodTypeNames, goStructFieldTypeNames } from './languages/go';
import { dartMisparsedGenericCall, dartReceiverOf, isDartTypeName, pushDartTypeRefs } from './languages/dart';
import { LiquidExtractor } from './liquid-extractor';
import { RazorExtractor } from './razor-extractor';
@@ -3909,7 +3909,7 @@ export class TreeSitterExtractor {
// impl-navigation can see the contract. Go has no `implements` keyword, so
// without the interface's method set there's nothing to match against.
if (this.language === 'go' && typeChild) {
this.extractGoInterfaceMethods(typeChild, interfaceNode.id);
this.extractGoInterfaceMethods(typeChild, interfaceNode.id, node);
}
return true;
}
@@ -3959,8 +3959,11 @@ export class TreeSitterExtractor {
* interface). tree-sitter-go names these `method_elem` (newer) or
* `method_spec` (older). Embedded interfaces (`Reader` inside `ReadWriter`)
* are `type_elem`s, not methods, and are left to inheritance extraction.
* Each method references the types its signature names, as a function does
* (`decl` declares the interface): a call through the interface reaches the
* method, so impact on a type reaches its callers.
*/
private extractGoInterfaceMethods(interfaceType: SyntaxNode, ifaceId: string): void {
private extractGoInterfaceMethods(interfaceType: SyntaxNode, ifaceId: string, decl: SyntaxNode): void {
this.nodeStack.push(ifaceId);
for (let i = 0; i < interfaceType.namedChildCount; i++) {
const m = interfaceType.namedChild(i);
@@ -3969,9 +3972,20 @@ export class TreeSitterExtractor {
if (!nameNode) continue;
const mname = getNodeText(nameNode, this.source);
if (mname) {
this.createNode('method', mname, m, {
const methodNode = this.createNode('method', mname, m, {
signature: this.extractor?.getSignature?.(m, this.source),
});
if (methodNode) {
for (const type of goInterfaceMethodTypeNames(m, decl, this.source)) {
this.unresolvedReferences.push({
fromNodeId: methodNode.id,
referenceName: getNodeText(type, this.source),
referenceKind: 'references',
line: type.startPosition.row + 1,
column: type.startPosition.column,
});
}
}
}
}
this.nodeStack.pop();