feat(go): infer structural interface implementations (#1966)

This commit is contained in:
evolution 2026-06-02 16:27:44 +08:00 • committed by GitHub
parent e466ee7451
commit eb2ba3dcad
32 changed files with 2562 additions and 169 deletions

View file

@ -1,9 +1,9 @@
{
"_comment": "Per-language baselines for bench/scope-capture/measure.mjs --check. fingerprint = order-independent sha256 over the lang-resolution/<lang>-* fixture corpus + a 20-entity synthetic source (correctness gate; re-baseline intentionally on a legitimate capture change). scaling_budget = max allowed (t800/t250)/(800/250); ~1.0 is linear, ~3.2 is quadratic. The synthetic source is now HERITAGE-BEARING for every language (each Entity extends/implements/embeds/uses-trait/conforms-to a shared base) so the #1951 @reference.inherits synth is gated at scale, not just the base capture loop. All languages thread the tree-sitter captured node instead of re-deriving it with findNodeAtRange(tree.rootNode,...) per match, so all are linear (go #1915, python #1918, ruby/php/rust/csharp #1951, java #1956).",
"go": {
"fingerprint": "976bfd17cee048db11e06a27298e48919b7d45d5277d11923faeb61b138737dd",
"fingerprint": "a909c197b07921f974de1a8a47cc997b9580153db2d400ef13d205b6c1de5865",
"scaling_budget": 1.5,
"_rebaselined": "#1956 synth-widening: + go-qualified-base fixture; synthesizeGoInheritanceReferences now emits embeds for qualified_type (pkg.Base), generic_type (Box[T]), pointer, AND interface_type embeds (matching the #1940 legacy leg), reduced to bare names at parity. go-ambiguous gains an embed inherits capture. Linear (~1.01). (Earlier #1956: heritage-bearing scale source so the synth is gated at scale.)"
"_rebaselined": "#1966: Go structural interface implementation detection changes capture output (method_elem, return-type, pointer receiver raw form, struct_type/interface_type containers)."
},
"cobol": {
"fingerprint": "68ee0e95eb9f86f2d92ca35f730f4c2d4d83abc1b5241ae767ff3437780ec8d1",

View file

@ -4,12 +4,21 @@ export interface GoArityMetadata {
readonly parameterCount?: number;
readonly requiredParameterCount?: number;
readonly parameterTypes?: readonly string[];
readonly returnType?: string;
}
export function computeGoDeclarationArity(node: SyntaxNode): GoArityMetadata {
const params = node.childForFieldName('parameters');
if (params === null) return {};
const returnType = extractGoReturnType(node);
if (params === null) return returnType === undefined ? {} : { returnType };
return {
...computeGoParameterMetadata(params),
...(returnType === undefined ? {} : { returnType }),
};
}
function computeGoParameterMetadata(params: SyntaxNode): GoArityMetadata {
let count = 0;
let required = 0;
const types: string[] = [];
@ -44,3 +53,23 @@ export function computeGoCallArity(callNode: SyntaxNode): number {
if (args === null) return 0;
return args.namedChildCount;
}
function extractGoReturnType(node: SyntaxNode): string | undefined {
const result = node.childForFieldName('result');
if (result === null) return undefined;
if (result.type !== 'parameter_list') return result.text;
const returnTypes: string[] = [];
for (const child of result.namedChildren) {
if (child.type !== 'parameter_declaration') continue;
const typeNode = child.childForFieldName('type');
if (typeNode === null) continue;
const names = child.namedChildren.filter((c) => c.type === 'identifier');
const n = Math.max(1, names.length);
for (let i = 0; i < n; i++) {
returnTypes.push(typeNode.text);
}
}
if (returnTypes.length === 0) return undefined;
return returnTypes.length === 1 ? returnTypes[0] : `(${returnTypes.join(', ')})`;
}

View file

@ -88,7 +88,8 @@ export function emitGoScopeCaptures(
// the function_declaration / method_declaration node.
const fnNode =
declAnchorNode.type === 'function_declaration' ||
declAnchorNode.type === 'method_declaration'
declAnchorNode.type === 'method_declaration' ||
declAnchorNode.type === 'method_elem'
? declAnchorNode
: null;
if (fnNode !== null) {
@ -114,6 +115,13 @@ export function emitGoScopeCaptures(
JSON.stringify(arity.parameterTypes),
);
}
if (arity.returnType !== undefined) {
grouped['@declaration.return-type'] = syntheticCapture(
'@declaration.return-type',
fnNode,
arity.returnType,
);
}
}
out.push(grouped);
continue;

View file

@ -1,87 +1,598 @@
import type { ParsedFile, SymbolDefinition } from 'gitnexus-shared';
import type { ParsedFile, ReferenceSite, SymbolDefinition } from 'gitnexus-shared';
import type { SemanticModel } from '../../model/semantic-model.js';
import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
import { simpleQualifiedName } from '../../scope-resolution/graph-bridge/ids.js';
import { resolveInheritanceBaseInScope } from '../../scope-resolution/scope/walkers.js';
type MethodSet = ReadonlyMap<string, readonly SymbolDefinition[]>;
type MutableMethodSet = Map<string, SymbolDefinition[]>;
type MethodSetEntry = {
readonly overloads: readonly SymbolDefinition[];
readonly depth: number;
readonly ambiguous: boolean;
};
type MutableMethodSetEntries = Map<string, MethodSetEntry>;
type GoMethodDefinition = SymbolDefinition & { readonly goReceiverKind?: 'value' | 'pointer' };
type SignatureContext = {
readonly packageQualifier: string | undefined;
readonly importQualifiers: ReadonlyMap<string, string>;
};
type DetectionIndexes = {
readonly interfaces: readonly SymbolDefinition[];
readonly structsById: ReadonlyMap<string, SymbolDefinition>;
readonly methodsByOwner: ReadonlyMap<string, MethodSet>;
readonly effectiveMethodsByStructId: ReadonlyMap<string, MethodSet>;
readonly interfaceById: ReadonlyMap<string, SymbolDefinition>;
readonly interfaceOwnMethodsById: ReadonlyMap<string, MethodSet>;
readonly embeddedSitesByInterfaceId: ReadonlyMap<string, readonly ReferenceSite[]>;
readonly parentStructIdsByStructId: ReadonlyMap<string, readonly string[]>;
readonly structIdsByMethodName: ReadonlyMap<string, ReadonlySet<string>>;
readonly signatureContextByDefId: ReadonlyMap<string, SignatureContext>;
readonly scopeIndexes: ScopeResolutionIndexes;
};
export function detectGoInterfaceImplementations(
parsedFiles: readonly ParsedFile[],
_indexes: ScopeResolutionIndexes,
_model: SemanticModel,
): Map<string, string[]> {
// 1. Collect interface defs → method names (from scope.ownedDefs)
const interfaceMethods = new Map<string, Set<string>>();
const interfaceDefsById = new Map<string, SymbolDefinition>();
return detectGoInterfaceImplementationsFromIndexes(buildDetectionIndexes(parsedFiles, _indexes));
}
// 2. Collect struct defs → method names
const structMethods = new Map<string, Set<string>>();
function buildDetectionIndexes(
parsedFiles: readonly ParsedFile[],
indexes: ScopeResolutionIndexes,
): DetectionIndexes {
const interfaces: SymbolDefinition[] = [];
const structsById = new Map<string, SymbolDefinition>();
const methodsByOwner = new Map<string, Map<string, SymbolDefinition[]>>();
const effectiveMethodsByStructId = new Map<string, MethodSet>();
const interfaceById = new Map<string, SymbolDefinition>();
const interfaceOwnMethodsById = new Map<string, MethodSet>();
const embeddedSitesByInterfaceId = new Map<string, ReferenceSite[]>();
const parentStructIdsByStructId = new Map<string, string[]>();
const structIdsByMethodName = new Map<string, Set<string>>();
const signatureContextByDefId = new Map<string, SignatureContext>();
const interfaceIdByScopeId = new Map<string, string>();
const structIdByScopeId = new Map<string, string>();
for (const parsed of parsedFiles) {
// Collect interface defs and their owned methods
for (const scope of parsed.scopes) {
if (scope.kind !== 'Class') continue;
// Find the type def for this scope
const typeDef = scope.ownedDefs.find((d) => d.type === 'Interface' || d.type === 'Struct');
if (typeDef === undefined) continue;
if (typeDef.type === 'Interface') {
interfaceDefsById.set(typeDef.nodeId, typeDef);
const methodNames = new Set<string>();
// Methods are in child scopes (Function kind) or ownedDefs
for (const childScope of parsed.scopes) {
if (childScope.parent === scope.id && childScope.kind === 'Function') {
for (const def of childScope.ownedDefs) {
if (def.type === 'Method' || def.type === 'Function') {
methodNames.add(def.qualifiedName?.split('.').pop() ?? '');
}
}
}
}
// Also check if methods have ownerId pointing to this interface
for (const def of parsed.localDefs) {
if (
(def as { ownerId?: string }).ownerId === typeDef.nodeId &&
(def.type === 'Method' || def.type === 'Function')
) {
methodNames.add(def.qualifiedName?.split('.').pop() ?? '');
}
}
interfaceMethods.set(typeDef.nodeId, methodNames);
const signatureContext = signatureContextForFile(parsed, indexes);
for (const def of parsed.localDefs) {
signatureContextByDefId.set(def.nodeId, signatureContext);
if (def.type === 'Interface') {
interfaces.push(def);
interfaceById.set(def.nodeId, def);
continue;
}
if (typeDef.type === 'Struct') {
const methodNames = new Set<string>();
for (const def of parsed.localDefs) {
if (
(def as { ownerId?: string }).ownerId === typeDef.nodeId &&
(def.type === 'Method' || def.type === 'Function')
) {
methodNames.add(def.qualifiedName?.split('.').pop() ?? '');
}
}
structMethods.set(typeDef.nodeId, methodNames);
if (def.type === 'Struct') {
structsById.set(def.nodeId, def);
continue;
}
if (def.type !== 'Method' && def.type !== 'Function') continue;
if (def.ownerId === undefined) continue;
if (isPointerReceiverMethod(def)) continue;
const methodName = simpleQualifiedName(def);
if (methodName === undefined || methodName.length === 0) continue;
addMethod(methodsByOwner, def.ownerId, methodName, def);
}
}
// 3. For each interface, find structs whose method set is a superset
const impls = new Map<string, string[]>();
for (const [ifaceId, ifaceMethods] of interfaceMethods) {
if (ifaceMethods.size === 0) continue;
for (const parsed of parsedFiles) {
for (const scope of parsed.scopes) {
const iface = scope.ownedDefs.find((def) => def.type === 'Interface');
if (iface !== undefined) interfaceIdByScopeId.set(scope.id, iface.nodeId);
const struct = scope.ownedDefs.find((def) => def.type === 'Struct');
if (struct !== undefined) structIdByScopeId.set(scope.id, struct.nodeId);
}
}
for (const parsed of parsedFiles) {
const childScopesByParent = new Map<string, (typeof parsed.scopes)[number][]>();
for (const scope of parsed.scopes) {
if (scope.parent === null) continue;
const children = childScopesByParent.get(scope.parent) ?? [];
children.push(scope);
childScopesByParent.set(scope.parent, children);
}
for (const scope of parsed.scopes) {
const ifaceId = interfaceIdByScopeId.get(scope.id);
if (ifaceId === undefined) continue;
const methods = new Map<string, SymbolDefinition[]>();
for (const childScope of childScopesByParent.get(scope.id) ?? []) {
for (const def of childScope.ownedDefs) {
if (def.type !== 'Method' && def.type !== 'Function') continue;
const methodName = simpleQualifiedName(def);
if (methodName === undefined || methodName.length === 0) continue;
addMethodOverload(methods, methodName, def);
}
}
interfaceOwnMethodsById.set(ifaceId, methods);
}
for (const site of parsed.referenceSites) {
if (site.kind !== 'inherits') continue;
const ifaceId = interfaceIdByScopeId.get(site.inScope);
if (ifaceId !== undefined) {
const sites = embeddedSitesByInterfaceId.get(ifaceId) ?? [];
sites.push(site);
embeddedSitesByInterfaceId.set(ifaceId, sites);
continue;
}
const structId = structIdByScopeId.get(site.inScope);
if (structId === undefined) continue;
const parent = resolveInheritanceBaseInScope(site.inScope, site.name, indexes);
if (parent === undefined || parent.type !== 'Struct') continue;
addParentStruct(parentStructIdsByStructId, structId, parent.nodeId);
}
}
const structMethodSetCache = new Map<string, MutableMethodSetEntries>();
for (const structId of structsById.keys()) {
const effective = collectStructMethodSet(
structId,
{
parentStructIdsByStructId,
methodsByOwner,
},
new Set(),
structMethodSetCache,
);
if (effective === undefined) continue;
effectiveMethodsByStructId.set(structId, effective);
for (const methodName of effective.keys()) {
addStructMethodCandidate(structIdsByMethodName, methodName, structId);
}
}
return {
interfaces,
structsById,
methodsByOwner,
effectiveMethodsByStructId,
interfaceById,
interfaceOwnMethodsById,
embeddedSitesByInterfaceId,
parentStructIdsByStructId,
structIdsByMethodName,
signatureContextByDefId,
scopeIndexes: indexes,
};
}
function detectGoInterfaceImplementationsFromIndexes(
indexes: DetectionIndexes,
): Map<string, string[]> {
const implementations = new Map<string, string[]>();
const methodSetCache = new Map<string, MutableMethodSet>();
for (const iface of indexes.interfaces) {
const required = collectInterfaceMethodSet(iface, indexes, new Set(), methodSetCache);
if (required === undefined || required.size === 0) continue;
if (!methodSetHasVerifiableSignatures(required)) continue;
const implementors: string[] = [];
for (const [structId, methods] of structMethods) {
if (isSuperset(methods, ifaceMethods)) {
for (const structId of candidateStructIdsFor(required, indexes)) {
const actual = indexes.effectiveMethodsByStructId.get(structId);
if (actual === undefined) continue;
if (methodSetSatisfies(actual, required, indexes.signatureContextByDefId)) {
implementors.push(structId);
}
}
if (implementors.length > 0) impls.set(ifaceId, implementors);
if (implementors.length > 0) implementations.set(iface.nodeId, implementors);
}
return impls;
return implementations;
}
function isSuperset(superset: Set<string>, subset: Set<string>): boolean {
for (const item of subset) {
if (!superset.has(item)) return false;
function addMethod(
methodsByOwner: Map<string, Map<string, SymbolDefinition[]>>,
ownerId: string,
methodName: string,
def: SymbolDefinition,
): void {
let methods = methodsByOwner.get(ownerId);
if (methods === undefined) {
methods = new Map<string, SymbolDefinition[]>();
methodsByOwner.set(ownerId, methods);
}
addMethodOverload(methods, methodName, def);
}
function addMethodOverload(
methods: Map<string, SymbolDefinition[]>,
methodName: string,
def: SymbolDefinition,
): void {
const overloads = methods.get(methodName) ?? [];
overloads.push(def);
methods.set(methodName, overloads);
}
function addStructMethodCandidate(
structIdsByMethodName: Map<string, Set<string>>,
methodName: string,
structId: string,
): void {
const structIds = structIdsByMethodName.get(methodName) ?? new Set<string>();
structIds.add(structId);
structIdsByMethodName.set(methodName, structIds);
}
function addParentStruct(
parentStructIdsByStructId: Map<string, string[]>,
structId: string,
parentStructId: string,
): void {
const parents = parentStructIdsByStructId.get(structId) ?? [];
parents.push(parentStructId);
parentStructIdsByStructId.set(structId, parents);
}
function collectStructMethodSet(
structId: string,
indexes: Pick<DetectionIndexes, 'methodsByOwner' | 'parentStructIdsByStructId'>,
visiting: Set<string>,
cache: Map<string, MutableMethodSetEntries>,
): MutableMethodSet | undefined {
const entries = collectStructMethodEntries(structId, indexes, visiting, cache);
return entries === undefined ? undefined : methodEntriesToMethodSet(entries);
}
function collectStructMethodEntries(
structId: string,
indexes: Pick<DetectionIndexes, 'methodsByOwner' | 'parentStructIdsByStructId'>,
visiting: Set<string>,
cache: Map<string, MutableMethodSetEntries>,
): MutableMethodSetEntries | undefined {
const cached = cache.get(structId);
if (cached !== undefined) return cloneMethodEntries(cached);
if (visiting.has(structId)) return undefined;
visiting.add(structId);
const merged = directMethodEntries(indexes.methodsByOwner.get(structId));
for (const parentStructId of indexes.parentStructIdsByStructId.get(structId) ?? []) {
const parentEntries = collectStructMethodEntries(parentStructId, indexes, visiting, cache);
if (parentEntries === undefined) {
visiting.delete(structId);
return undefined;
}
for (const [methodName, entry] of parentEntries) {
if (entry.ambiguous) continue;
mergePromotedMethodEntry(merged, methodName, {
overloads: entry.overloads,
depth: entry.depth + 1,
ambiguous: false,
});
}
}
visiting.delete(structId);
cache.set(structId, cloneMethodEntries(merged));
return merged;
}
function collectInterfaceMethodSet(
iface: SymbolDefinition,
indexes: DetectionIndexes,
visiting: Set<string>,
cache: Map<string, MutableMethodSet>,
): MutableMethodSet | undefined {
const cached = cache.get(iface.nodeId);
if (cached !== undefined) return cloneMethodSet(cached);
if (visiting.has(iface.nodeId)) return undefined;
visiting.add(iface.nodeId);
const ownMethods =
indexes.methodsByOwner.get(iface.nodeId) ?? indexes.interfaceOwnMethodsById.get(iface.nodeId);
const merged = cloneMethodSet(ownMethods);
const embeddedInterfaces = embeddedInterfacesFor(iface, indexes);
if (embeddedInterfaces === undefined) {
visiting.delete(iface.nodeId);
return undefined;
}
for (const embeddedIface of embeddedInterfaces) {
const embeddedMethods = collectInterfaceMethodSet(embeddedIface, indexes, visiting, cache);
if (embeddedMethods === undefined) {
visiting.delete(iface.nodeId);
return undefined;
}
mergeMethodSet(merged, embeddedMethods);
}
visiting.delete(iface.nodeId);
cache.set(iface.nodeId, cloneMethodSet(merged));
return merged;
}
function embeddedInterfacesFor(
iface: SymbolDefinition,
indexes: DetectionIndexes,
): SymbolDefinition[] | undefined {
const embedded: SymbolDefinition[] = [];
for (const site of indexes.embeddedSitesByInterfaceId.get(iface.nodeId) ?? []) {
const resolved = resolveEmbeddedInterface(site, indexes);
if (resolved === undefined) return undefined;
embedded.push(resolved);
}
return embedded;
}
function candidateStructIdsFor(required: MethodSet, indexes: DetectionIndexes): readonly string[] {
let best: ReadonlySet<string> | undefined;
for (const name of required.keys()) {
const candidates = indexes.structIdsByMethodName.get(name);
if (candidates === undefined) return [];
if (best === undefined || candidates.size < best.size) best = candidates;
}
return best === undefined ? [...indexes.structsById.keys()] : [...best];
}
function resolveEmbeddedInterface(
site: ReferenceSite,
indexes: DetectionIndexes,
): SymbolDefinition | undefined {
const bound = resolveInheritanceBaseInScope(site.inScope, site.name, indexes.scopeIndexes);
if (bound !== undefined) return bound.type === 'Interface' ? bound : undefined;
const simpleName = simpleTypeName(site.name);
const matches: SymbolDefinition[] = [];
for (const iface of indexes.interfaceById.values()) {
if (iface.qualifiedName === site.name || iface.qualifiedName === simpleName) {
matches.push(iface);
}
}
return matches.length === 1 ? matches[0] : undefined;
}
function simpleTypeName(name: string): string {
const dot = name.lastIndexOf('.');
return dot === -1 ? name : name.slice(dot + 1);
}
function cloneMethodSet(methods: MethodSet | undefined): MutableMethodSet {
const clone = new Map<string, SymbolDefinition[]>();
if (methods === undefined) return clone;
for (const [name, overloads] of methods) {
clone.set(name, [...overloads]);
}
return clone;
}
function directMethodEntries(methods: MethodSet | undefined): MutableMethodSetEntries {
const entries = new Map<string, MethodSetEntry>();
if (methods === undefined) return entries;
for (const [name, overloads] of methods) {
entries.set(name, { overloads: [...overloads], depth: 0, ambiguous: false });
}
return entries;
}
function cloneMethodEntries(entries: ReadonlyMap<string, MethodSetEntry>): MutableMethodSetEntries {
const clone = new Map<string, MethodSetEntry>();
for (const [name, entry] of entries) {
clone.set(name, { ...entry, overloads: [...entry.overloads] });
}
return clone;
}
function methodEntriesToMethodSet(entries: ReadonlyMap<string, MethodSetEntry>): MutableMethodSet {
const methods = new Map<string, SymbolDefinition[]>();
for (const [name, entry] of entries) {
if (entry.ambiguous) continue;
methods.set(name, [...entry.overloads]);
}
return methods;
}
function mergePromotedMethodEntry(
target: MutableMethodSetEntries,
methodName: string,
candidate: MethodSetEntry,
): void {
const existing = target.get(methodName);
if (existing === undefined || candidate.depth < existing.depth) {
target.set(methodName, candidate);
return;
}
if (candidate.depth > existing.depth) return;
target.set(methodName, { overloads: [], depth: candidate.depth, ambiguous: true });
}
function mergeMethodSet(target: MutableMethodSet, source: MethodSet): void {
for (const [name, overloads] of source) {
const existing = target.get(name) ?? [];
existing.push(...overloads);
target.set(name, existing);
}
}
function methodSetSatisfies(
actual: MethodSet,
required: MethodSet,
signatureContextByDefId: ReadonlyMap<string, SignatureContext>,
): boolean {
for (const [name, requiredOverloads] of required) {
const actualOverloads = actual.get(name);
if (actualOverloads === undefined) return false;
for (const requiredMethod of requiredOverloads) {
if (!hasCompatibleMethod(actualOverloads, requiredMethod, signatureContextByDefId)) {
return false;
}
}
}
return true;
}
function hasCompatibleMethod(
actualOverloads: readonly SymbolDefinition[],
requiredMethod: SymbolDefinition,
signatureContextByDefId: ReadonlyMap<string, SignatureContext>,
): boolean {
if (!hasVerifiableSignature(requiredMethod)) return false;
return actualOverloads.some((actualMethod) =>
signaturesCompatible(actualMethod, requiredMethod, signatureContextByDefId),
);
}
function methodSetHasVerifiableSignatures(methods: MethodSet): boolean {
for (const overloads of methods.values()) {
if (!overloads.some(hasVerifiableSignature)) return false;
}
return true;
}
function isPointerReceiverMethod(def: SymbolDefinition): boolean {
return (def as GoMethodDefinition).goReceiverKind === 'pointer';
}
function hasVerifiableSignature(def: SymbolDefinition): boolean {
return (
def.parameterCount !== undefined ||
def.requiredParameterCount !== undefined ||
(def.parameterTypes !== undefined && def.parameterTypes.length > 0) ||
def.returnType !== undefined
);
}
function signaturesCompatible(
actual: SymbolDefinition,
required: SymbolDefinition,
signatureContextByDefId: ReadonlyMap<string, SignatureContext>,
): boolean {
const actualContext = signatureContextByDefId.get(actual.nodeId);
const requiredContext = signatureContextByDefId.get(required.nodeId);
return (
countsCompatible(actual.parameterCount, required.parameterCount) &&
countsCompatible(actual.requiredParameterCount, required.requiredParameterCount) &&
parameterTypesCompatible(
actual.parameterTypes,
required.parameterTypes,
actualContext,
requiredContext,
) &&
returnTypesCompatible(actual.returnType, required.returnType, actualContext, requiredContext)
);
}
function countsCompatible(actual: number | undefined, required: number | undefined): boolean {
return actual === undefined || required === undefined || actual === required;
}
function parameterTypesCompatible(
actual: readonly string[] | undefined,
required: readonly string[] | undefined,
actualContext: SignatureContext | undefined,
requiredContext: SignatureContext | undefined,
): boolean {
if (actual === undefined || required === undefined) return true;
if (actual.length !== required.length) return false;
return actual.every((type, index) => {
const actualType = normalizeSignatureType(type, actualContext);
const requiredType = normalizeSignatureType(required[index]!, requiredContext);
return actualType !== undefined && requiredType !== undefined && actualType === requiredType;
});
}
function returnTypesCompatible(
actual: string | undefined,
required: string | undefined,
actualContext: SignatureContext | undefined,
requiredContext: SignatureContext | undefined,
): boolean {
if (required === undefined) return actual === undefined;
if (actual === undefined) return false;
const actualType = normalizeSignatureType(actual, actualContext);
const requiredType = normalizeSignatureType(required, requiredContext);
return actualType !== undefined && requiredType !== undefined && actualType === requiredType;
}
function normalizeSignatureType(typeName: string, context?: SignatureContext): string | undefined {
// Go type identity includes pointer/slice/map/variadic shape and package
// qualifiers. Only erase whitespace and qualify bare local type names; stripping
// `*`, `[]`, `...`, or `pkg.` would make non-identical signatures compare equal.
const compact = typeName.replace(/\s+/g, '');
if (context === undefined) return compact;
return qualifyGoSignatureTypes(compact, context);
}
function qualifyGoSignatureTypes(typeName: string, context: SignatureContext): string | undefined {
let unresolvedQualifier = false;
const qualified = typeName.replace(/[A-Za-z_][A-Za-z0-9_]*/g, (token, offset, source) => {
if (GO_BUILTIN_TYPES.has(token)) return token;
if (hasPackageQualifierDot(source, offset)) return token;
if (source[offset + token.length] === '.') {
const qualifier = context.importQualifiers.get(token);
if (qualifier !== undefined) return qualifier;
unresolvedQualifier = true;
return token;
}
if (context.packageQualifier === undefined) return token;
return `${context.packageQualifier}.${token}`;
});
return unresolvedQualifier ? undefined : qualified;
}
function hasPackageQualifierDot(source: string, offset: number): boolean {
return source[offset - 1] === '.' && source[offset - 2] !== '.';
}
function signatureContextForFile(
parsed: ParsedFile,
indexes: ScopeResolutionIndexes,
): SignatureContext {
const importQualifiers = new Map<string, string>();
const importEdges = indexes.imports?.get(parsed.moduleScope) ?? [];
for (const edge of importEdges) {
if (edge.kind !== 'namespace' || edge.targetFile === null) continue;
const qualifier = packageQualifierForFile(edge.targetFile);
if (qualifier !== undefined) importQualifiers.set(edge.localName, qualifier);
}
return {
packageQualifier: packageQualifierForFile(parsed.filePath),
importQualifiers,
};
}
function packageQualifierForFile(filePath: string): string | undefined {
const normalized = filePath.replace(/\\/g, '/');
const slash = normalized.lastIndexOf('/');
if (slash === -1) return undefined;
const packageDir = normalized.slice(0, slash);
return packageDir.length === 0 ? undefined : packageDir;
}
const GO_BUILTIN_TYPES = new Set([
'any',
'bool',
'byte',
'comparable',
'complex64',
'complex128',
'error',
'float32',
'float64',
'func',
'int',
'int8',
'int16',
'int32',
'int64',
'interface',
'map',
'rune',
'string',
'struct',
'uint',
'uint8',
'uint16',
'uint32',
'uint64',
'uintptr',
'chan',
]);

View file

@ -28,7 +28,10 @@ export function interpretGoTypeBinding(captures: CaptureMatch): ParsedTypeBindin
let normalizedType: string;
if (captures['@type-binding.self'] !== undefined) {
source = 'self';
normalizedType = normalizeGoTypeName(type);
// Preserve pointer shape on receiver self-bindings (`*T` vs `T`).
// Method-owner enrichment consumes that raw shape to model Go value and
// pointer receiver method sets conservatively.
normalizedType = type.trim();
} else if (captures['@type-binding.constructor'] !== undefined) {
source = 'constructor-inferred';
normalizedType = normalizeGoTypeName(type);

View file

@ -44,12 +44,15 @@ export function populateGoWorkspaceOwners(
function populateGoOwnersInPackage(parsedFiles: readonly ParsedFile[]): void {
// Build struct name → def map from ALL scopes' ownedDefs (struct defs
// live in Class scopes now, not Module scope).
const structByQualifiedName = new Map<string, string>(); // qname → nodeId
const structByQualifiedName = new Map<string, { nodeId: string; qualifiedName: string }>();
for (const parsed of parsedFiles) {
for (const scope of parsed.scopes) {
for (const def of scope.ownedDefs) {
if (isClassLike(def.type) && def.qualifiedName) {
structByQualifiedName.set(def.qualifiedName, def.nodeId);
structByQualifiedName.set(def.qualifiedName, {
nodeId: def.nodeId,
qualifiedName: def.qualifiedName,
});
}
}
}
@ -69,26 +72,35 @@ function populateGoOwnersInPackage(parsedFiles: readonly ParsedFile[]): void {
// Find the self typeBinding in this Function scope.
let receiverType: string | undefined;
let receiverKind: 'value' | 'pointer' = 'value';
for (const [, tb] of scope.typeBindings) {
if (tb.source === 'self') {
receiverType = tb.rawName;
receiverKind = tb.rawName.trim().startsWith('*') ? 'pointer' : 'value';
break;
}
}
if (receiverType === undefined) continue;
receiverType = receiverType.replace(/^\*+/, '').trim();
let ownerId = structByQualifiedName.get(receiverType);
if (ownerId === undefined) {
for (const [qname, nodeId] of structByQualifiedName) {
let owner = structByQualifiedName.get(receiverType);
if (owner === undefined) {
for (const [qname, candidate] of structByQualifiedName) {
if (qname.endsWith('.' + receiverType)) {
ownerId = nodeId;
owner = candidate;
break;
}
}
}
if (ownerId !== undefined) {
if (owner !== undefined) {
for (const def of methodDefs) {
(def as { ownerId?: string }).ownerId = ownerId;
(def as { ownerId?: string }).ownerId = owner.nodeId;
(def as { goReceiverKind?: 'value' | 'pointer' }).goReceiverKind = receiverKind;
const simpleName = def.qualifiedName?.split('.').pop() ?? def.qualifiedName;
if (simpleName !== undefined && !def.qualifiedName?.includes('.')) {
(def as { qualifiedName?: string }).qualifiedName =
`${owner.qualifiedName}.${simpleName}`;
}
}
}
}

View file

@ -39,6 +39,10 @@ const GO_SCOPE_QUERY = `
(method_declaration
name: (field_identifier) @declaration.name) @declaration.method
;; Declarations — interface methods
(method_elem
name: (field_identifier) @declaration.name) @declaration.method
;; Declarations — struct fields
(struct_type
(field_declaration_list

View file

@ -71,10 +71,12 @@ export function populateGoRangeBindings(
// Resolve range expression type
let elementType: string | null = null;
const functionScope = findEnclosingFunctionScope(rangeNode, scopeMap);
if (rangeExpr.type === 'identifier') {
// Look up the identifier's type in scope typeBindings (V1: module scope only)
const binding = moduleScope.typeBindings.get(rangeExpr.text);
const binding =
functionScope?.typeBindings.get(rangeExpr.text) ??
moduleScope.typeBindings.get(rangeExpr.text);
if (binding !== null && binding !== undefined) {
elementType = extractElementType(binding);
}
@ -96,7 +98,6 @@ export function populateGoRangeBindings(
if (elementType !== null && valueVar !== null) {
// Inject type binding for the range variable onto the enclosing function scope
const functionScope = findEnclosingFunctionScope(rangeNode, scopeMap);
const targetScope = functionScope ?? moduleScope;
const mutable = targetScope.typeBindings as Map<string, TypeRef>;
mutable.set(valueVar, {
@ -137,7 +138,7 @@ function findEnclosingFunctionScope(
for (const scope of scopeMap.values()) {
if (
scope.kind === 'Function' &&
scope.range.startLine === current.startPosition.row &&
scope.range.startLine === current.startPosition.row + 1 &&
scope.range.startCol === current.startPosition.column
) {
return scope;

View file

@ -11,7 +11,7 @@ export function synthesizeGoReceiverBinding(fnNode: SyntaxNode): CaptureMatch |
const nameNode = param.childForFieldName('name');
const typeNode = param.childForFieldName('type');
if (nameNode === null || typeNode === null) return null;
const typeName = typeNode.text.replace(/^\*/, '');
const typeName = typeNode.text;
return {
'@type-binding.self': syntheticCapture('@type-binding.self', fnNode, nameNode.text),

View file

@ -48,8 +48,8 @@ export const goScopeResolver: ScopeResolver = {
populateNamespaceSiblings: populateGoPackageSiblings,
mirrorNamespaceTypeBindings: mirrorGoNamespaceTypeBindings,
// Staged/V2: registered but not yet wired in run.ts — method-name-only
// matching produces false IMPLEMENTS edges; awaits signature-level comparison.
// Go has structural interfaces: implementations are inferred by signature,
// then fed into generic MRO/interface-dispatch.
detectInterfaceImplementations: detectGoInterfaceImplementations,
populateRangeBindings: populateGoRangeBindings,
};

View file

@ -12,10 +12,11 @@ export function goBindingScopeFor(
innermost: Scope,
_tree: ScopeTree,
): ScopeId | null {
// Keep self typeBindings in the method's Function scope (prevent
// auto-hoist to Module) so populateGoOwners can match Method defs
// to their receiver types by inspecting each Function scope.
if (decl['@type-binding.self'] !== undefined) {
// Keep receiver and parameter typeBindings in the function scope
// (prevent auto-hoist to Module). Parameters are local variables:
// hoisting `repo Repository` from one function to module scope can
// pollute another function's local receiver resolution.
if (decl['@type-binding.self'] !== undefined || decl['@type-binding.parameter'] !== undefined) {
return innermost.id;
}
return null; // default auto-hoist for other bindings
@ -32,7 +33,12 @@ export function goImportOwningScope(
export function goReceiverBinding(functionScope: Scope): TypeRef | null {
if (functionScope.kind !== 'Function') return null;
for (const binding of functionScope.typeBindings.values()) {
if (binding.source === 'self') return binding;
if (binding.source === 'self') return normalizeGoSelfTypeRef(binding);
}
return null;
}
function normalizeGoSelfTypeRef(binding: TypeRef): TypeRef {
const rawName = binding.rawName.replace(/^\*+/, '').trim();
return rawName === binding.rawName ? binding : { ...binding, rawName };
}

View file

@ -4,6 +4,27 @@ import { syntheticCapture, type SyntaxNode } from '../../utils/ast-helpers.js';
export function synthesizeGoTypeBindings(rootNode: SyntaxNode): CaptureMatch[] {
const out: CaptureMatch[] = [];
for (const node of rootNode.descendantsOfType('var_spec')) {
const name = node.childForFieldName('name');
const value = node.childForFieldName('value');
if (name?.type !== 'identifier' || value === null) continue;
const rhsExpr = value.namedChildren[0];
if (rhsExpr === undefined) continue;
const typeNode = extractCompositeLiteralTypeNode(rhsExpr);
if (typeNode === null) continue;
out.push({
'@type-binding.constructor': syntheticCapture('@type-binding.constructor', node, node.text),
'@type-binding.name': syntheticCapture('@type-binding.name', name, name.text),
'@type-binding.type': syntheticCapture(
'@type-binding.type',
typeNode,
extractSimpleTypeNameText(typeNode),
),
});
}
for (const node of rootNode.descendantsOfType('short_var_declaration')) {
const right = node.childForFieldName('right');
if (right === null) continue;
@ -232,6 +253,21 @@ function extractSimpleTypeNameText(node: SyntaxNode): string {
}
/** Extract the type/signature node from a RHS expression. */
function extractCompositeLiteralTypeNode(expr: SyntaxNode): SyntaxNode | null {
if (expr.type === 'composite_literal') {
return (
expr.childForFieldName('type') ??
expr.namedChildren.find((c) => ['type_identifier', 'qualified_type'].includes(c.type)) ??
null
);
}
if (expr.type === 'unary_expression') {
const operand = expr.childForFieldName('operand');
return operand === null ? null : extractCompositeLiteralTypeNode(operand);
}
return null;
}
function extractTypeNode(expr: SyntaxNode): SyntaxNode | null {
if (expr.type === 'composite_literal') {
return (

View file

@ -170,7 +170,7 @@ export function emitReceiverBoundCalls(
const graphIdToClassDef = new Map<string, SymbolDefinition>();
for (const parsed of parsedFiles) {
for (const def of parsed.localDefs) {
if (def.type !== 'Class' && def.type !== 'Interface') continue;
if (def.type !== 'Class' && def.type !== 'Struct' && def.type !== 'Interface') continue;
const graphId = resolveDefGraphId(parsed.filePath, def, nodeLookup);
if (graphId !== undefined) graphIdToClassDef.set(graphId, def);
}

View file

@ -25,6 +25,7 @@
import type { ParsedFile, RegistryProviders } from 'gitnexus-shared';
import type { KnowledgeGraph } from '../../../graph/types.js';
import { generateId } from '../../../../lib/utils.js';
import { lookupOwnedMembersByOwner } from '../../model/owned-members-lookup.js';
import type { MutableSemanticModel, SemanticModel } from '../../model/semantic-model.js';
import { reconcileOwnership, validateOwnershipParity } from './reconcile-ownership.js';
@ -41,11 +42,7 @@ import { emitFreeCallFallback } from '../passes/free-call-fallback.js';
import { emitReferencesViaLookup } from '../graph-bridge/references-to-edges.js';
import { emitImportEdges } from '../graph-bridge/imports-to-edges.js';
import type { ScopeResolver } from '../contract/scope-resolver.js';
import {
findClassBindingInScope,
findEnclosingClassDef,
resolveAmbiguousInheritanceBaseViaImports,
} from '../scope/walkers.js';
import { findEnclosingClassDef, resolveInheritanceBaseInScope } from '../scope/walkers.js';
import { buildWorkspaceResolutionIndex } from '../workspace-index.js';
import type { ResolutionOutcome, ResolutionOutcomeRecorder } from '../resolution-outcome.js';
@ -134,14 +131,7 @@ function preEmitInheritanceEdges(
handledSites.add(siteKey);
}
const targetDef =
findClassBindingInScope(site.inScope, site.name, scopes) ??
// Import-aware disambiguation fallback (#1951). Only engages when the
// scope-chain + single-match lookups above returned undefined because
// the simple name is ambiguous (multiple same-named class-like defs).
// Picks the candidate whose defining file is imported/included by the
// referencing file. Never changes behavior for single-match cases.
resolveAmbiguousInheritanceBaseViaImports(site.inScope, site.name, scopes);
const targetDef = resolveInheritanceBaseInScope(site.inScope, site.name, scopes);
if (targetDef === undefined) continue;
const callerClass = findEnclosingClassDef(site.inScope, scopes);
@ -166,6 +156,68 @@ function preEmitInheritanceEdges(
return handledSites;
}
/**
* Emit language-inferred structural interface implementations before MRO and
* interface dispatch are built. Languages such as Go do not declare
* `implements` explicitly, so their resolver can infer defId-level interface
* satisfaction from parsed files and this bridge converts those defIds to
* graph node ids.
*
* Existing explicit IMPLEMENTS edges win: the local `existing` set prevents
* duplicate structural edges and keeps this hook language-neutral. The reason
* string carries the provider language (`go-structural-implements`) so callers
* can distinguish inferred edges from source-declared heritage.
*/
function emitDetectedInterfaceImplementations(
graph: KnowledgeGraph,
parsedFiles: readonly ParsedFile[],
nodeLookup: ReturnType<typeof buildGraphNodeLookup>,
provider: ScopeResolver,
indexes: ReturnType<typeof finalizeScopeModel>,
model: SemanticModel,
): number {
if (provider.detectInterfaceImplementations === undefined) return 0;
const graphIdByDefId = new Map<string, string>();
for (const parsed of parsedFiles) {
for (const def of parsed.localDefs) {
if (def.type !== 'Class' && def.type !== 'Struct' && def.type !== 'Interface') continue;
const graphId = resolveDefGraphId(parsed.filePath, def, nodeLookup);
if (graphId !== undefined) graphIdByDefId.set(def.nodeId, graphId);
}
}
const existing = new Set<string>();
for (const rel of graph.iterRelationshipsByType('IMPLEMENTS')) {
existing.add(`${rel.sourceId}->${rel.targetId}`);
}
let emitted = 0;
const detected = provider.detectInterfaceImplementations(parsedFiles, indexes, model);
for (const [interfaceDefId, implementorDefIds] of detected) {
const targetId = graphIdByDefId.get(interfaceDefId);
if (targetId === undefined) continue;
for (const implementorDefId of implementorDefIds) {
const sourceId = graphIdByDefId.get(implementorDefId);
if (sourceId === undefined) continue;
const edgeKey = `${sourceId}->${targetId}`;
if (existing.has(edgeKey)) continue;
existing.add(edgeKey);
graph.addRelationship({
id: generateId('IMPLEMENTS', edgeKey),
sourceId,
targetId,
type: 'IMPLEMENTS',
confidence: 0.85,
reason: `${provider.language}-structural-implements`,
});
emitted++;
}
}
return emitted;
}
export type ScopeResolutionSubPhase =
| 'extracting'
| 'analyzing types'
@ -377,6 +429,14 @@ export function runScopeResolution(
// heritage hook are invisible and ACCESSES edges silently fail to emit.
const postHeritageNodeLookup =
provider.emitHeritageEdges !== undefined ? buildGraphNodeLookup(graph) : nodeLookup;
emitDetectedInterfaceImplementations(
graph,
parsedFiles,
postHeritageNodeLookup,
provider,
finalized,
readonlyModel,
);
const mroByClassDefId = provider.buildMro(graph, parsedFiles, postHeritageNodeLookup);
const extendsOnlyMroByClassDefId = provider.buildExtendsOnlyMro?.(
graph,

View file

@ -300,6 +300,22 @@ export function findClassBindingInScope(
return undefined;
}
/**
* Resolve a class-like inheritance target using the shared inheritance
* resolution chain. Keeps pre-emitted heritage edges and language-specific
* consumers of `inherits` sites aligned.
*/
export function resolveInheritanceBaseInScope(
startScope: ScopeId,
baseName: string,
scopes: ScopeResolutionIndexes,
): SymbolDefinition | undefined {
return (
findClassBindingInScope(startScope, baseName, scopes) ??
resolveAmbiguousInheritanceBaseViaImports(startScope, baseName, scopes)
);
}
/**
* Import/include-aware disambiguation for an *ambiguous* class-like base
* name. Engages ONLY as a fallback after `findClassBindingInScope` has

View file

@ -905,6 +905,7 @@ export const GO_QUERIES = `
; Functions & Methods
(function_declaration name: (identifier) @name) @definition.function
(method_declaration name: (field_identifier) @name) @definition.method
(method_elem name: (field_identifier) @name) @definition.method
; Types
(type_declaration (type_spec name: (type_identifier) @name type: (struct_type))) @definition.struct

View file

@ -182,6 +182,9 @@ export const CLASS_CONTAINER_TYPES = new Set([
// Kotlin
'object_declaration',
'companion_object',
// Go
'struct_type',
'interface_type',
]);
export const CONTAINER_TYPE_TO_LABEL: Record<string, string> = {
@ -214,6 +217,8 @@ export const CONTAINER_TYPE_TO_LABEL: Record<string, string> = {
singleton_class: 'Class', // Ruby: class << self inherits enclosing class name
object_declaration: 'Class',
companion_object: 'Class',
struct_type: 'Struct',
interface_type: 'Interface',
};
/**

View file

@ -9,35 +9,35 @@
},
"go-ambiguous/internal/models/handler.go": {
"captureGroups": 9,
"digest": "619c516a5791095bc6380de62fa861364b3f9480f2ec87d4499c2c998f228713"
"digest": "737d7ca26cf03aebcae6b5df6a13267d400b42cd37799d72eda92db3485a1add"
},
"go-ambiguous/internal/other/handler.go": {
"captureGroups": 9,
"digest": "08a61721581c4f17741ef0c4ee1c8945235ee7f2aca7d88d2fe122071cd62e6f"
"digest": "645a526694111ba6066715515ef4055d84f5c328505f7fa92d00b0b253d0aeca"
},
"go-ambiguous/internal/services/user.go": {
"captureGroups": 9,
"digest": "642da0df644ebd5a789e2de2d473ced2c9a77e5f6f1b4a1b1d53cf04845b820c"
"digest": "0a988c48838a34eb7a6f29b324191c39c19087e728914fffa06bbb9221841d23"
},
"go-assignment-chain/cmd/main.go": {
"captureGroups": 50,
"digest": "47ba5fd2ea96ee202b3a3de5c0db75ea18d0889064ed2138d62c67594f7d22e9"
"digest": "570d1eb9d287345a00a5a05bde00724b609523b41953cb1c277f5c056980d7b5"
},
"go-assignment-chain/models/repo.go": {
"captureGroups": 8,
"digest": "1b3acbf48751105d056253488f34159266bdb0b229248c85065e927b72bf4801"
"digest": "63cb96d06478f4b2039d6eaeab1468a8af3c503ec45e5785ff3e4de5aabe9240"
},
"go-assignment-chain/models/user.go": {
"captureGroups": 8,
"digest": "457c76ebf1c86efac7a7d9f384a8e49acd5649c006e6cc8aa29a59c36f0b102a"
"digest": "fcee44ed373eda2ff00778ff951fc2fa1503c5187b9e7d43dea3ee8b63656907"
},
"go-call-result-binding/cmd/main.go": {
"captureGroups": 16,
"digest": "83d611dcee826ec848a0b3fd5a18a98103896d17c353d8d63cd15551039818e9"
"digest": "ecf35e5a0e148d55f98a0de636f743db026fb75ae6a609b3fa90bd285c322145"
},
"go-call-result-binding/models/user.go": {
"captureGroups": 10,
"digest": "5c31199e148340ee32dd48e32a00003e094be963b1287b9b40c6b4effca12175"
"digest": "4193f6356f75505415e36d4b090dc9f5265937efe72b4251c33e5b5f06aaa99d"
},
"go-calls/cmd/main.go": {
"captureGroups": 6,
@ -45,23 +45,23 @@
},
"go-calls/internal/onearg/log.go": {
"captureGroups": 6,
"digest": "a101eafe9f08396bb176cf3cadd3960d752802048e47c0ac9e9ace07244a46fb"
"digest": "762e8143c4ab116272c38d46586106ec4d5e01153c7abe831ca434269da58d94"
},
"go-calls/internal/zeroarg/log.go": {
"captureGroups": 5,
"digest": "7b322767a38298de8c6ce99fa6b1d69dda8bbb79704053644bfaa7d2e4473fdb"
"digest": "eae0988b2f001f06b275487d4c8154ce64c222642fff2cbabaff8d51b389dea3"
},
"go-chain-call/cmd/main.go": {
"captureGroups": 20,
"digest": "5a8d7de8ae87887902d16f28cb70d31014c96ab5d2eb63cbd6c8be27650fa9a1"
"digest": "c60375f48bb2765060f068dc986abc9ed6ed4489c14115b6fa544ee3beb3d3d8"
},
"go-chain-call/models/repo.go": {
"captureGroups": 10,
"digest": "ac4799aae638d528c5c7c01c8b9734fc61e995596a12b12e790dcffab97b4029"
"digest": "94b6f138a062dfd96969dc7ec3d3eddecab591cb29d16fed4ea91da20ff0363b"
},
"go-chain-call/models/user.go": {
"captureGroups": 10,
"digest": "5c31199e148340ee32dd48e32a00003e094be963b1287b9b40c6b4effca12175"
"digest": "4193f6356f75505415e36d4b090dc9f5265937efe72b4251c33e5b5f06aaa99d"
},
"go-child-extends-parent/models/child.go": {
"captureGroups": 4,
@ -69,7 +69,7 @@
},
"go-child-extends-parent/models/parent.go": {
"captureGroups": 8,
"digest": "454a724f571a5aede89d7657f76ed8e3c9b19bfc18d524141d1b646005f98e49"
"digest": "19a2b1696d45ea66cd00df562b65ea0b2e9fdbe60233cf31587a334221420d6e"
},
"go-child-extends-parent/services/app.go": {
"captureGroups": 10,
@ -77,7 +77,7 @@
},
"go-cmd-helper/cmd/server/internal/config/config.go": {
"captureGroups": 5,
"digest": "b10874198d380b0a186fb1e1ee8cedac644eb3b6b624398110a660183e59a0b5"
"digest": "129af13eba5f2b52ea2fd2d24de49331ba542aa3dfab014e72baa0239685a0db"
},
"go-cmd-helper/cmd/server/main.go": {
"captureGroups": 7,
@ -89,11 +89,11 @@
},
"go-constructor-type-inference/models/repo.go": {
"captureGroups": 8,
"digest": "1b3acbf48751105d056253488f34159266bdb0b229248c85065e927b72bf4801"
"digest": "63cb96d06478f4b2039d6eaeab1468a8af3c503ec45e5785ff3e4de5aabe9240"
},
"go-constructor-type-inference/models/user.go": {
"captureGroups": 8,
"digest": "457c76ebf1c86efac7a7d9f384a8e49acd5649c006e6cc8aa29a59c36f0b102a"
"digest": "fcee44ed373eda2ff00778ff951fc2fa1503c5187b9e7d43dea3ee8b63656907"
},
"go-deep-field-chain/cmd/main.go": {
"captureGroups": 13,
@ -101,7 +101,7 @@
},
"go-deep-field-chain/models/models.go": {
"captureGroups": 33,
"digest": "0ad1df946f58e446a8dbd8ef13041b6e0177f53293946b955acf3c46684e4295"
"digest": "3941943919b0066138d8a759cceadd95784c8a89978e7a345ea056ef1ca78c9c"
},
"go-field-types/cmd/main.go": {
"captureGroups": 9,
@ -109,7 +109,7 @@
},
"go-field-types/models/models.go": {
"captureGroups": 22,
"digest": "fbdbf74d927c0ed07f4820c190fc6f2039b74ddead8abb45fc238812dfa4d4ef"
"digest": "78de0f686b33993bd361a1d5366982afbe5378f4a62464c4248e3c9b65aaa9c3"
},
"go-for-call-expr/cmd/main.go": {
"captureGroups": 27,
@ -117,11 +117,11 @@
},
"go-for-call-expr/models/repo.go": {
"captureGroups": 14,
"digest": "312e59c1402cb83a4ce51c27866fde6c597f7121098b57ff1d3bd4dc6363834a"
"digest": "33889573461301951f51786e94a60a1d20c87bd3d2cad12f8619e951b94ad7ed"
},
"go-for-call-expr/models/user.go": {
"captureGroups": 14,
"digest": "c442a26c4051c2b7426850a381137507a147c21d631d6535d71ef1035b1506fb"
"digest": "964cf1bd181cc7196726c3261a683884ac9c6fc3aee6170b47bd54427a6cb947"
},
"go-inc-dec-write-access/main.go": {
"captureGroups": 21,
@ -141,7 +141,7 @@
},
"go-make-builtin/models.go": {
"captureGroups": 15,
"digest": "4d5628f66471f1ad1180a47b797195506d190ddc84f61f6c57f2e22afd754c1e"
"digest": "2af382fcd2e59cf01d9439bdb8709cba150bb239fae71b6f0d09c1fa7248fc5b"
},
"go-map-range/main.go": {
"captureGroups": 10,
@ -157,23 +157,23 @@
},
"go-member-calls/cmd/main.go": {
"captureGroups": 11,
"digest": "e46f6d1dff39943f8f89c05e0d28f61f8471cdc729c91f01ca693a38a244b8ba"
"digest": "b65bbfd200e4be9a991403a44bc68fb0a0abfe0bbf764b2acbd472d3eaf6742e"
},
"go-member-calls/models/user.go": {
"captureGroups": 8,
"digest": "457c76ebf1c86efac7a7d9f384a8e49acd5649c006e6cc8aa29a59c36f0b102a"
"digest": "fcee44ed373eda2ff00778ff951fc2fa1503c5187b9e7d43dea3ee8b63656907"
},
"go-method-chain-binding/cmd/main.go": {
"captureGroups": 21,
"digest": "0fff0df5dd77e13e0b9e62dd7f38efc038d33ffd5d891f684289eccbc46ff583"
"digest": "884589d31ad9c4463d48323b7aa5f80c15a4d3eb1097210abd88c3d013d2fbef"
},
"go-method-chain-binding/models/user.go": {
"captureGroups": 24,
"digest": "504ea598176dd8e01d759cc54e012735c746f14ec3a660af2c362fc356326f65"
"digest": "d72468ce0567ce35393b570e7e3e86f0c004e37a9614dc5ee698aff6d3a4348a"
},
"go-method-enrichment/animal.go": {
"captureGroups": 15,
"digest": "ac0933f59d4a88a25629d02f308c66847fd34ceba5831660fc8bff07109b7708"
"captureGroups": 16,
"digest": "7ef52b78d7c920a34961f3275ceb4a34d0172067459ab594287b7e4b7ec695a6"
},
"go-method-enrichment/app.go": {
"captureGroups": 15,
@ -185,7 +185,7 @@
},
"go-mixed-chain/models/models.go": {
"captureGroups": 41,
"digest": "dc2cedaefcd73faf13a9f704a4be8f0bd4140c608cd86480e1f4400fec49dbd1"
"digest": "afb1eaf2869f534033c5cd844ca111a93b63df1c098cec9c9518c4480a47ddb0"
},
"go-multi-assign/app.go": {
"captureGroups": 16,
@ -193,19 +193,19 @@
},
"go-multi-assign/models.go": {
"captureGroups": 19,
"digest": "a4d43cf2cd2f7bdbc750a7ee1f5e9c7611e5d1e25f1a46386d0eeb9f73bb7846"
"digest": "e071ebdd59b5bbebb42785ebf850a9c0ee7c2e3a8cc13fb0b17233480efa26ed"
},
"go-multi-return-inference/cmd/main.go": {
"captureGroups": 36,
"digest": "6229b69cfd15bf1465d770d97486522faac5a91cf8828c424c8c8c989b310224"
"digest": "179fca92694ce3e1f2a9b38cde0c283e4bab00f240316a6116931115e079057a"
},
"go-multi-return-inference/models/repo.go": {
"captureGroups": 10,
"digest": "ac4799aae638d528c5c7c01c8b9734fc61e995596a12b12e790dcffab97b4029"
"digest": "94b6f138a062dfd96969dc7ec3d3eddecab591cb29d16fed4ea91da20ff0363b"
},
"go-multi-return-inference/models/user.go": {
"captureGroups": 10,
"digest": "5c31199e148340ee32dd48e32a00003e094be963b1287b9b40c6b4effca12175"
"digest": "4193f6356f75505415e36d4b090dc9f5265937efe72b4251c33e5b5f06aaa99d"
},
"go-new-builtin/main.go": {
"captureGroups": 12,
@ -213,27 +213,27 @@
},
"go-new-builtin/models.go": {
"captureGroups": 17,
"digest": "40c9f89942406caf2610f0d1954b66e905c39dd5d617d7359e572445d586c15d"
"digest": "d5ce8ba6ff38c6c78bb87e746a5382822745d0c9841a8092495ff386c7c4ee24"
},
"go-nullable-receiver/cmd/main.go": {
"captureGroups": 27,
"digest": "7a93b339f8c68ed3882802da21343c1d6b5231ae5cd8dd13eb4c05e2e7716320"
"digest": "7430c0c8f4877d1b41e62aee9017eadc66d2a8f01b70acf2836584a84f254335"
},
"go-nullable-receiver/models/repo.go": {
"captureGroups": 8,
"digest": "1b3acbf48751105d056253488f34159266bdb0b229248c85065e927b72bf4801"
"digest": "63cb96d06478f4b2039d6eaeab1468a8af3c503ec45e5785ff3e4de5aabe9240"
},
"go-nullable-receiver/models/user.go": {
"captureGroups": 8,
"digest": "457c76ebf1c86efac7a7d9f384a8e49acd5649c006e6cc8aa29a59c36f0b102a"
"digest": "fcee44ed373eda2ff00778ff951fc2fa1503c5187b9e7d43dea3ee8b63656907"
},
"go-parent-resolution/models/base.go": {
"captureGroups": 8,
"digest": "2afaeb50d544a55fe437ef20e2c0de92152d2ba62f2693c329255787bb3d0a02"
"digest": "a5138789f8e35111dced225841a4d0e209eabad582973a55f5d47f882cf777e8"
},
"go-parent-resolution/models/user.go": {
"captureGroups": 9,
"digest": "bc39ef8b54dcfcf975155ff69721bf2075ca170fb81bb081a1052dda72f64ecd"
"digest": "e1b57b0c5865d84b80518fe52e3807f813ae5589b4b558d0037e6ea136843f85"
},
"go-pkg/cmd/main.go": {
"captureGroups": 14,
@ -241,19 +241,19 @@
},
"go-pkg/internal/auth/service.go": {
"captureGroups": 17,
"digest": "2204643b50f486423ee7a5877b2bab7d6334cbe62b14b4435fe4ba8a6465ce92"
"digest": "dc481fe7319f4e69d6f53d04a10029bb36b5f964b5ecd5a367d6e081c0158fec"
},
"go-pkg/internal/models/admin.go": {
"captureGroups": 14,
"digest": "85b2e848f29885082671f00d015bcce3b14ebd372a88acbc225e6ade1ee5449c"
"digest": "6c307b22f7ddd3c2c34a525a61e4ed36602e22b5845fc6533be15515ef7d78fb"
},
"go-pkg/internal/models/repository.go": {
"captureGroups": 3,
"digest": "7de6e11a3cf9c89afa9d89fe37dab85b208699f20105fbade223e4f78a63b15c"
"captureGroups": 5,
"digest": "73a5c9bfcb59011f36c47e1b806edba89bad1ed32f81d46acbefdb8abdba67c7"
},
"go-pkg/internal/models/user.go": {
"captureGroups": 13,
"digest": "e56fcea1c473866ed06fc0262702e54c72016a9556cc6337330c03cc1f638fe1"
"digest": "a9eeed64d83ab471847db27f8ce5c4ba65b6ac2664f372da67ee4fc974016224"
},
"go-pointer-constructor-inference/cmd/main.go": {
"captureGroups": 15,
@ -261,19 +261,19 @@
},
"go-pointer-constructor-inference/models/repo.go": {
"captureGroups": 10,
"digest": "ac4799aae638d528c5c7c01c8b9734fc61e995596a12b12e790dcffab97b4029"
"digest": "94b6f138a062dfd96969dc7ec3d3eddecab591cb29d16fed4ea91da20ff0363b"
},
"go-pointer-constructor-inference/models/user.go": {
"captureGroups": 10,
"digest": "5c31199e148340ee32dd48e32a00003e094be963b1287b9b40c6b4effca12175"
"digest": "4193f6356f75505415e36d4b090dc9f5265937efe72b4251c33e5b5f06aaa99d"
},
"go-qualified-base/base/base.go": {
"captureGroups": 16,
"digest": "4c50e40140094556e8bfc540f0ea6627c55823a1e4fcc91ca5d74912df67d499"
"captureGroups": 17,
"digest": "c2f2241a4e31ad1b003c1649d7511437f3884815412aeac65754cf3cfb9b6dc1"
},
"go-qualified-base/consumers/local.go": {
"captureGroups": 19,
"digest": "9097247f77ab93b742c715ff9d656038fc8ea0558e3fe7e4df64e6b1c6fcb64c"
"captureGroups": 20,
"digest": "923a41030b83b854118c12cb6f99ca02a8347b1db72ec8304904d3a99284f5f4"
},
"go-qualified-base/consumers/qualified.go": {
"captureGroups": 14,
@ -281,7 +281,7 @@
},
"go-receiver-method-free-call/example.go": {
"captureGroups": 8,
"digest": "2a3c26672d3b997bdc39644361c550f8cf0749489f0945d210e2fb7f3bca9383"
"digest": "0b773eaede711e43ed8b8ae6aacf76c398dc362f54429b30a4f5eb7c71f863da"
},
"go-receiver-method-free-call/util.go": {
"captureGroups": 4,
@ -293,35 +293,35 @@
},
"go-receiver-resolution/models/repo.go": {
"captureGroups": 8,
"digest": "1b3acbf48751105d056253488f34159266bdb0b229248c85065e927b72bf4801"
"digest": "63cb96d06478f4b2039d6eaeab1468a8af3c503ec45e5785ff3e4de5aabe9240"
},
"go-receiver-resolution/models/user.go": {
"captureGroups": 8,
"digest": "457c76ebf1c86efac7a7d9f384a8e49acd5649c006e6cc8aa29a59c36f0b102a"
"digest": "fcee44ed373eda2ff00778ff951fc2fa1503c5187b9e7d43dea3ee8b63656907"
},
"go-return-type-inference/cmd/main.go": {
"captureGroups": 39,
"digest": "ac8ca3dc1fb7fb4d7a1f947a77e93db890f04d328135dc514f36ba5cd04bc3e1"
"digest": "71b935bc70b194c2a8136f053319aaba86a4ba80a5d171ef39bc6e8d5c144865"
},
"go-return-type-inference/models/repo.go": {
"captureGroups": 16,
"digest": "8c500db82093b4622acbca734f3040e6176000a9d9a4843e2a81e3f2b3daee7c"
"digest": "0dde6336b070f5e9919189de576fdad96d69eb993f9b29a7a48875bc044f229b"
},
"go-return-type-inference/models/user.go": {
"captureGroups": 16,
"digest": "a70b19b9c46a02003f26d0b70fa5368983a791583ec3776e983c733a9283eefa"
"digest": "c133a3db65e1952277b7c986f0870db86d4bb762ff0043d2160ed772317e03ca"
},
"go-same-package-factory/main.go": {
"captureGroups": 14,
"digest": "505d4d279615f3c99457d8cb0f29fdf946b547c6b1fda6d43aa6d4302dd61809"
"digest": "4638aceb638f4cf9c11d9f51f43f50edf6e5358da69cde78d4bffee1f3dce3cc"
},
"go-same-package-factory/repo.go": {
"captureGroups": 8,
"digest": "f8bb213588f517e166b421f19d72cde981153464cc3f3168499769e778c4a22e"
"digest": "a5488a5ebe483c88d9f5bb2ccf0c283e397b3345798509c0e8e5ab65272be7fd"
},
"go-same-package-factory/user.go": {
"captureGroups": 8,
"digest": "4daaad60f35d4519a24cd067baf4cd4bdc421e3dd80b9110a8dfaa8a9e75d9eb"
"digest": "067c51c54c15b71f3ec45b8da94ea693e6b440d30fa26122d0a90a3b81f42bce"
},
"go-split-method-owner/main.go": {
"captureGroups": 9,
@ -329,11 +329,11 @@
},
"go-split-method-owner/repo.go": {
"captureGroups": 8,
"digest": "f8bb213588f517e166b421f19d72cde981153464cc3f3168499769e778c4a22e"
"digest": "a5488a5ebe483c88d9f5bb2ccf0c283e397b3345798509c0e8e5ab65272be7fd"
},
"go-split-method-owner/save.go": {
"captureGroups": 6,
"digest": "6e0e1b5521a2fad1cd00252e771926d5caa7930440278968f7f8607821ad339c"
"digest": "5d3d7a31d42336c964302822a959c6519e31f09bc4c6ee744197205d7591f707"
},
"go-split-method-owner/user.go": {
"captureGroups": 3,
@ -345,15 +345,43 @@
},
"go-struct-literals/user.go": {
"captureGroups": 10,
"digest": "89b791a9d150fe341924d54d3173d9f35f72be899d0e7ac03a35d32119b94f8f"
"digest": "16f0d425b25bdc7eaaf331bd47ce1ece6d476ae0e24e72110ce96c1f315bd314"
},
"go-structural-interface-cross-package/api/repository.go": {
"captureGroups": 12,
"digest": "b5ef588c11391fa93237260baae4c4bb67ee4dfc381651444a9925c3f52adec3"
},
"go-structural-interface-cross-package/cmd/main.go": {
"captureGroups": 27,
"digest": "b44adcfaa2b965479abe17b406c6c74ddd51bb54e7d129e8a99a26a2a3f2fc16"
},
"go-structural-interface-cross-package/contracts/read_closer.go": {
"captureGroups": 6,
"digest": "0813deb9e72bfb1e07d0e9399c4d4537dc40181f0c5c43cff5580b91d4ed3605"
},
"go-structural-interface-cross-package/impl/file.go": {
"captureGroups": 20,
"digest": "950002bd5dfc126d13355f317944a509253fe6dd81f12638af1d5f0fe37d04db"
},
"go-structural-interface-cross-package/other/user.go": {
"captureGroups": 5,
"digest": "4d6298ea05addadfa17f6dd8a2eb0fa2ea63b73de69355015594504ae4e6c1b1"
},
"go-structural-interface-cross-package/store/repository.go": {
"captureGroups": 33,
"digest": "9a497524610994e9a47555cd5874074b278d3e12850e3ee62a1afadb25fcabe8"
},
"go-structural-interface-dispatch/repository.go": {
"captureGroups": 129,
"digest": "f31bc522991e59527834bb7dcd45b8738490c622c3d25181a8148cc85d27844b"
},
"go-type-assertion/main.go": {
"captureGroups": 11,
"digest": "b8bd327d3965531802a93bc01e3c24969a027540397a1635119ef0b7da46c348"
},
"go-type-assertion/models.go": {
"captureGroups": 17,
"digest": "3780f8f7c145a15f849ae6958e492db0044e825b81aa8dfd21b9873759fad65e"
"captureGroups": 18,
"digest": "018658521c83d757464357f9963cd5952e040be01a02d9f1d41304f3748fd1a5"
},
"go-variadic-resolution/cmd/main.go": {
"captureGroups": 6,
@ -369,6 +397,6 @@
},
"synthetic:dao-20": {
"captureGroups": 481,
"digest": "1698b5dd78c8094f251b10ab8cacebfbf453f38eddf7233fbc7964e32f04ceeb"
"digest": "611e1f8147dff51e8857e493e06089aadf17a042cee6a522520433025b2bc0da"
}
}

View file

@ -0,0 +1,14 @@
package api
type User struct {
ID string
}
type Saver interface {
Load(id string) User
Save(user User) error
}
type Reader interface {
Read() error
}

View file

@ -0,0 +1,20 @@
package main
import (
"github.com/example/gostruct/api"
"github.com/example/gostruct/contracts"
"github.com/example/gostruct/store"
)
func precise(user api.User) {
var saver api.Saver = store.GoodStore{}
saver.Save(user)
}
func fallback(saver api.Saver, user api.User) {
saver.Save(user)
}
func fallbackReadCloser(rc contracts.ReadCloser) {
rc.Close()
}

View file

@ -0,0 +1,8 @@
package contracts
import apix "github.com/example/gostruct/api"
type ReadCloser interface {
apix.Reader
Close() error
}

View file

@ -0,0 +1,3 @@
module github.com/example/gostruct
go 1.21

View file

@ -0,0 +1,17 @@
package impl
type File struct{}
func (f File) Read() error {
return nil
}
func (f File) Close() error {
return nil
}
type CloseOnly struct{}
func (c CloseOnly) Close() error {
return nil
}

View file

@ -0,0 +1,5 @@
package other
type User struct {
ID string
}

View file

@ -0,0 +1,26 @@
package store
import (
apix "github.com/example/gostruct/api"
"github.com/example/gostruct/other"
)
type GoodStore struct{}
func (s GoodStore) Load(id string) apix.User {
return apix.User{ID: id}
}
func (s GoodStore) Save(user apix.User) error {
return nil
}
type WrongStore struct{}
func (s WrongStore) Load(id string) other.User {
return other.User{ID: id}
}
func (s WrongStore) Save(user other.User) error {
return nil
}

View file

@ -0,0 +1,102 @@
package main
type User struct {
Name string
}
type Repository interface {
Find(id string) User
Save(user User) error
}
type SqlRepository struct{}
func (s SqlRepository) Find(id string) User {
return User{Name: id}
}
func (s SqlRepository) Save(user User) error {
return nil
}
type MemoryRepository struct{}
func (m MemoryRepository) Find(id string) User {
return User{Name: id}
}
func (m MemoryRepository) Save(user User) error {
return nil
}
type BadRepository struct{}
func (b BadRepository) Find(id string) User {
return User{Name: id}
}
func (b BadRepository) Save(id string) error {
return nil
}
func precise(user User) {
var repo Repository = SqlRepository{}
repo.Save(user)
}
func fallback(repo Repository, user User) {
repo.Save(user)
}
type Reader interface {
Read() error
}
type ReadCloser interface {
Reader
Close() error
}
type FileBase struct{}
func (f FileBase) Read() error {
return nil
}
type File struct {
FileBase
}
func (f File) Close() error {
return nil
}
type ShadowReadFile struct {
FileBase
}
func (s ShadowReadFile) Read(path string) error {
return nil
}
func (s ShadowReadFile) Close() error {
return nil
}
type CloseOnly struct{}
func (c CloseOnly) Close() error {
return nil
}
func fallbackReadCloser(rc ReadCloser) {
rc.Close()
}
type PointerOnly interface {
Touch()
}
type PointerOnlyThing struct{}
func (p *PointerOnlyThing) Touch() {}

View file

@ -32,8 +32,12 @@ import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import type { ParsedFile, SymbolDefinition } from 'gitnexus-shared';
import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js';
import { emitGoScopeCaptures } from '../../src/core/ingestion/languages/go/index.js';
import {
emitGoScopeCaptures,
detectGoInterfaceImplementations,
} from '../../src/core/ingestion/languages/go/index.js';
const BENCH_ENABLED = process.env.GITNEXUS_BENCH === '1';
@ -454,3 +458,264 @@ describe('Go scope-capture O(n^2) regression tripwire', () => {
expect(elapsedMs).toBeLessThan(BUDGET_MS);
}, 30_000);
});
// ---------------------------------------------------------------------------
// Go structural interface detection benchmarks
// ---------------------------------------------------------------------------
/**
* Build synthetic ParsedFile data for detectGoInterfaceImplementations.
*
* Creates `interfaceCount` interfaces (each with Find + Save methods) and
* `structCount` structs that implement all of them (matching signatures).
* Also generates a few BadStruct entries with mismatched signatures to
* verify they are correctly excluded (non-vacuous guard).
*/
function generateSyntheticInterfaceData(interfaceCount: number, structCount: number): ParsedFile[] {
const defs: SymbolDefinition[] = [];
const ifaceIds: string[] = [];
const structIds: string[] = [];
// Interfaces
for (let i = 0; i < interfaceCount; i++) {
const ifaceId = `iface:Repo${i}`;
ifaceIds.push(ifaceId);
defs.push({
nodeId: ifaceId,
filePath: 'repo.go',
type: 'Interface',
qualifiedName: `Repo${i}`,
});
// Find(id string) User
defs.push({
nodeId: `iface:Repo${i}.Find`,
filePath: 'repo.go',
type: 'Method',
qualifiedName: `Repo${i}.Find`,
ownerId: ifaceId,
parameterCount: 1,
requiredParameterCount: 1,
parameterTypes: ['string'],
returnType: 'User',
});
// Save(user User) error
defs.push({
nodeId: `iface:Repo${i}.Save`,
filePath: 'repo.go',
type: 'Method',
qualifiedName: `Repo${i}.Save`,
ownerId: ifaceId,
parameterCount: 1,
requiredParameterCount: 1,
parameterTypes: ['User'],
returnType: 'error',
});
}
// Structs — each implements all interfaces
for (let s = 0; s < structCount; s++) {
const structId = `struct:Impl${s}`;
structIds.push(structId);
defs.push({
nodeId: structId,
filePath: 'repo.go',
type: 'Struct',
qualifiedName: `Impl${s}`,
});
for (let i = 0; i < interfaceCount; i++) {
defs.push({
nodeId: `struct:Impl${s}.Repo${i}.Find`,
filePath: 'repo.go',
type: 'Method',
qualifiedName: `Impl${s}.Find`,
ownerId: structId,
parameterCount: 1,
requiredParameterCount: 1,
parameterTypes: ['string'],
returnType: 'User',
});
defs.push({
nodeId: `struct:Impl${s}.Repo${i}.Save`,
filePath: 'repo.go',
type: 'Method',
qualifiedName: `Impl${s}.Save`,
ownerId: structId,
parameterCount: 1,
requiredParameterCount: 1,
parameterTypes: ['User'],
returnType: 'error',
});
}
}
// BadStructs — wrong Save signature (string instead of User), should NOT match
for (let b = 0; b < Math.min(5, structCount); b++) {
const badId = `struct:Bad${b}`;
defs.push({
nodeId: badId,
filePath: 'repo.go',
type: 'Struct',
qualifiedName: `Bad${b}`,
});
for (let i = 0; i < interfaceCount; i++) {
defs.push({
nodeId: `struct:Bad${b}.Repo${i}.Find`,
filePath: 'repo.go',
type: 'Method',
qualifiedName: `Bad${b}.Find`,
ownerId: badId,
parameterCount: 1,
requiredParameterCount: 1,
parameterTypes: ['string'],
returnType: 'User',
});
// Mismatched Save: string param instead of User
defs.push({
nodeId: `struct:Bad${b}.Repo${i}.Save`,
filePath: 'repo.go',
type: 'Method',
qualifiedName: `Bad${b}.Save`,
ownerId: badId,
parameterCount: 1,
requiredParameterCount: 1,
parameterTypes: ['string'],
returnType: 'error',
});
}
}
return [
{
filePath: 'repo.go',
language: 'go',
scopes: [],
imports: [],
localDefs: defs,
referenceSites: [],
},
] as ParsedFile[];
}
/**
* Ungated tripwire: runs in normal CI. Calls detectGoInterfaceImplementations
* directly on synthetic data to catch O(n²) regressions. The current indexed
* path (structIdsByMethodName intersection) keeps this well under budget.
*/
describe('Go structural interface detection O(n²) regression tripwire', () => {
it('detects implementations for 50 interfaces × 50 structs within budget', () => {
const IFACE_COUNT = 50;
const STRUCT_COUNT = 50;
const BUDGET_MS = 5_000;
const parsed = generateSyntheticInterfaceData(IFACE_COUNT, STRUCT_COUNT);
const emptyIndexes = {} as any;
const emptyModel = {} as any;
// Warm up
detectGoInterfaceImplementations(
generateSyntheticInterfaceData(5, 5),
emptyIndexes,
emptyModel,
);
const start = Date.now();
const result = detectGoInterfaceImplementations(parsed, emptyIndexes, emptyModel);
const elapsedMs = Date.now() - start;
// Sanity: each interface should be implemented by all STRUCT_COUNT structs
expect(result.size).toBe(IFACE_COUNT);
for (const [, impls] of result) {
expect(impls).toHaveLength(STRUCT_COUNT);
}
// Regression guard
expect(elapsedMs).toBeLessThan(BUDGET_MS);
console.log(
` interface-detection tripwire: ${IFACE_COUNT}×${STRUCT_COUNT} = ${IFACE_COUNT * STRUCT_COUNT} pairs, ${elapsedMs}ms`,
);
}, 30_000);
});
/**
* Gated scaling benchmark: measures how detectGoInterfaceImplementations
* scales as interface and struct counts grow proportionally.
*
* Run: GITNEXUS_BENCH=1 npx vitest run test/integration/go-pipeline-benchmark.test.ts
*/
describe.skipIf(!BENCH_ENABLED)('Go structural interface detection benchmark', () => {
const scales = [50, 200, 800];
const REPS = 3;
it('scales linearly with interface × struct count', () => {
interface ScaleResult {
ifaceCount: number;
structCount: number;
totalPairs: number;
elapsedMs: number;
implEdges: number;
}
const results: ScaleResult[] = [];
const emptyIndexes = {} as any;
const emptyModel = {} as any;
for (const n of scales) {
// Warm up with small data once
detectGoInterfaceImplementations(
generateSyntheticInterfaceData(5, 5),
emptyIndexes,
emptyModel,
);
let bestMs = Infinity;
let implEdges = 0;
const parsed = generateSyntheticInterfaceData(n, n);
for (let r = 0; r < REPS; r++) {
const start = Date.now();
const result = detectGoInterfaceImplementations(parsed, emptyIndexes, emptyModel);
const elapsed = Date.now() - start;
if (elapsed < bestMs) {
bestMs = elapsed;
implEdges = 0;
for (const [, impls] of result) implEdges += impls.length;
}
}
results.push({
ifaceCount: n,
structCount: n,
totalPairs: n * n,
elapsedMs: bestMs,
implEdges,
});
console.log(` ${n}×${n} = ${n * n} pairs: ${bestMs}ms (${implEdges} IMPLEMENTS edges)`);
}
// Print scaling table
console.log('\nGo Interface Detection — Scaling');
console.log('┌──────────┬──────────┬───────────┬───────────┐');
console.log('│ Iface×St │ Pairs │ Time (ms) │ IMPL edges│');
console.log('├──────────┼──────────┼───────────┼───────────┤');
for (const r of results) {
console.log(
`│ ${String(`${r.ifaceCount}×${r.structCount}`).padStart(8)} │ ${String(r.totalPairs).padStart(8)} │ ${String(r.elapsedMs).padStart(9)} │ ${String(r.implEdges).padStart(9)} │`,
);
}
console.log('└──────────┴──────────┴───────────┴───────────┘');
// Assert linear scaling
if (results.length >= 2) {
console.log('\nScaling ratios (time_ratio / size_ratio):');
for (let i = 1; i < results.length; i++) {
const sizeRatio = results[i].totalPairs / results[i - 1].totalPairs;
const timeRatio = results[i].elapsedMs / results[i - 1].elapsedMs;
const scaling = timeRatio / sizeRatio;
console.log(
` ${results[i - 1].totalPairs} → ${results[i].totalPairs}: ${scaling.toFixed(2)}x (${scaling < 1.5 ? 'linear' : scaling < 3 ? 'superlinear' : 'WARNING: quadratic'})`,
);
expect(scaling).toBeLessThan(1.5);
}
}
}, 300_000);
});

View file

@ -340,6 +340,156 @@ describe('Go receiver-constrained resolution', () => {
});
});
describe('Go structural interface dispatch', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'go-structural-interface-dispatch'),
() => {},
);
}, 60000);
function owningTypeName(methodId: string): string {
for (const rel of result.graph.iterRelationshipsByType('HAS_METHOD')) {
if (rel.targetId !== methodId) continue;
const owner = result.graph.getNode(rel.sourceId);
return (owner?.properties.name ?? rel.sourceId) as string;
}
return '';
}
it('emits signature-checked structural IMPLEMENTS edges only for valid implementors', () => {
const implementsEdges = getRelationships(result, 'IMPLEMENTS').filter(
(edge) => edge.rel.reason === 'go-structural-implements',
);
expect(edgeSet(implementsEdges)).toEqual([
'File → ReadCloser',
'File → Reader',
'FileBase → Reader',
'MemoryRepository → Repository',
'SqlRepository → Repository',
]);
expect(implementsEdges.every((edge) => edge.rel.confidence === 0.85)).toBe(true);
});
it('feeds structural IMPLEMENTS into METHOD_IMPLEMENTS edges', () => {
const methodEdges = getRelationships(result, 'METHOD_IMPLEMENTS').filter(
(edge) => edge.target === 'Save',
);
const sourceOwners = methodEdges.map((edge) => owningTypeName(edge.rel.sourceId)).sort();
expect(sourceOwners).toEqual(['MemoryRepository', 'SqlRepository']);
});
it('prefers the concrete local assignment over interface fan-out', () => {
const saveCalls = getRelationships(result, 'CALLS').filter(
(edge) => edge.source === 'precise' && edge.target === 'Save',
);
const targetOwners = saveCalls.map((edge) => owningTypeName(edge.rel.targetId));
expect(targetOwners).toEqual(['SqlRepository']);
});
it('fans out interface-typed receiver calls to all known implementors', () => {
const saveCalls = getRelationships(result, 'CALLS').filter(
(edge) => edge.source === 'fallback' && edge.target === 'Save',
);
const dispatchTargets = saveCalls
.filter((edge) => edge.rel.reason === 'interface-dispatch')
.map((edge) => owningTypeName(edge.rel.targetId))
.sort();
expect(dispatchTargets).toEqual(['MemoryRepository', 'SqlRepository']);
});
it('includes embedded interface methods before emitting structural IMPLEMENTS edges', () => {
const implementsEdges = getRelationships(result, 'IMPLEMENTS');
expect(edgeSet(implementsEdges)).toContain('File → ReadCloser');
expect(edgeSet(implementsEdges)).not.toContain('CloseOnly → ReadCloser');
});
it('includes promoted embedded struct methods before emitting structural IMPLEMENTS edges', () => {
const implementsEdges = getRelationships(result, 'IMPLEMENTS');
expect(edgeSet(implementsEdges)).toContain('File → Reader');
expect(edgeSet(implementsEdges)).toContain('File → ReadCloser');
expect(edgeSet(implementsEdges)).not.toContain('ShadowReadFile → Reader');
expect(edgeSet(implementsEdges)).not.toContain('ShadowReadFile → ReadCloser');
});
it('does not emit value-type IMPLEMENTS for pointer-receiver-only methods', () => {
const implementsEdges = getRelationships(result, 'IMPLEMENTS');
expect(edgeSet(implementsEdges)).not.toContain('PointerOnlyThing → PointerOnly');
});
it('fans out embedded-interface receivers only to complete implementors', () => {
const closeCalls = getRelationships(result, 'CALLS').filter(
(edge) => edge.source === 'fallbackReadCloser' && edge.target === 'Close',
);
const dispatchTargets = closeCalls
.filter((edge) => edge.rel.reason === 'interface-dispatch')
.map((edge) => owningTypeName(edge.rel.targetId))
.sort();
expect(dispatchTargets).toEqual(['File']);
});
});
describe('Go cross-package structural interface dispatch', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'go-structural-interface-cross-package'),
() => {},
);
}, 60000);
function owningTypeName(methodId: string): string {
for (const rel of result.graph.iterRelationshipsByType('HAS_METHOD')) {
if (rel.targetId !== methodId) continue;
const owner = result.graph.getNode(rel.sourceId);
return (owner?.properties.name ?? rel.sourceId) as string;
}
return '';
}
it('matches local interface types against package-qualified implementation signatures', () => {
const implementsEdges = getRelationships(result, 'IMPLEMENTS').filter(
(edge) => edge.rel.reason === 'go-structural-implements',
);
expect(edgeSet(implementsEdges)).toEqual([
'File → ReadCloser',
'File → Reader',
'GoodStore → Saver',
]);
});
it('merges methods from package-qualified embedded interfaces before matching implementors', () => {
const implementsEdges = getRelationships(result, 'IMPLEMENTS');
expect(edgeSet(implementsEdges)).toContain('File → ReadCloser');
expect(edgeSet(implementsEdges)).not.toContain('CloseOnly → ReadCloser');
});
it('fans out cross-package interface receivers only to valid implementors', () => {
const saveCalls = getRelationships(result, 'CALLS').filter(
(edge) => edge.source === 'fallback' && edge.target === 'Save',
);
const dispatchTargets = saveCalls
.filter((edge) => edge.rel.reason === 'interface-dispatch')
.map((edge) => owningTypeName(edge.rel.targetId))
.sort();
expect(dispatchTargets).toEqual(['GoodStore']);
});
it('dispatches package-qualified embedded-interface receivers only to complete implementors', () => {
const closeCalls = getRelationships(result, 'CALLS').filter(
(edge) => edge.source === 'fallbackReadCloser' && edge.target === 'Close',
);
const dispatchTargets = closeCalls
.filter((edge) => edge.rel.reason === 'interface-dispatch')
.map((edge) => owningTypeName(edge.rel.targetId))
.sort();
expect(dispatchTargets).toEqual(['File']);
});
});
// ---------------------------------------------------------------------------
// Variadic resolution: ...interface{} doesn't get filtered by arity
// ---------------------------------------------------------------------------

View file

@ -43,6 +43,20 @@ const LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES: Readonly<Record<string, Readonly
// fixture). This requires scope-based cross-file package-sibling resolution
// which is only available in the registry-primary path.
'resolves user.Save() to the method whose receiver type is declared in another package file',
// Go structural interface implementation inference is a registry-primary
// scope-resolution feature. The legacy DAG does not synthesize structural
// IMPLEMENTS / METHOD_IMPLEMENTS edges or feed them into interface dispatch.
'emits signature-checked structural IMPLEMENTS edges only for valid implementors',
'feeds structural IMPLEMENTS into METHOD_IMPLEMENTS edges',
'prefers the concrete local assignment over interface fan-out',
'fans out interface-typed receiver calls to all known implementors',
'includes embedded interface methods before emitting structural IMPLEMENTS edges',
'includes promoted embedded struct methods before emitting structural IMPLEMENTS edges',
'fans out embedded-interface receivers only to complete implementors',
'matches local interface types against package-qualified implementation signatures',
'merges methods from package-qualified embedded interfaces before matching implementors',
'fans out cross-package interface receivers only to valid implementors',
'dispatches package-qualified embedded-interface receivers only to complete implementors',
]),
java: new Set([
// Duplicate-FQN same-module path-affinity ordering is implemented in the

View file

@ -86,11 +86,11 @@ func main() {
const matches = emitGoScopeCaptures(src, 'main.go');
// The closure is still captured as a @scope.function...
expect(matches.some((m) => m['@scope.function']?.text.startsWith('func()'))).toBe(true);
// ...and the method's receiver self-binding is synthesized (name + pointer-stripped type)...
// ...and the method's receiver self-binding is synthesized with its raw pointer shape...
const selves = matches.filter((m) => m['@type-binding.self'] !== undefined);
expect(selves).toHaveLength(1); // exactly one — from the method, not the closure
expect(selves[0]!['@type-binding.name']?.text).toBe('u');
expect(selves[0]!['@type-binding.type']?.text).toBe('User');
expect(selves[0]!['@type-binding.type']?.text).toBe('*User');
});
it('does not drop a var-form type assertion binding', () => {
@ -162,4 +162,47 @@ func Map[T any](x T) T { return x }
expect(decl).toBeDefined();
expect(decl!['@declaration.name']?.text).toBe('Map');
});
it('expands grouped named return types into separate return values', () => {
const src = `
package main
type Pairer interface {
Pair() (a, b int)
}
`;
const matches = emitGoScopeCaptures(src, 'main.go');
const pairDecl = matches.find(
(m) => m['@declaration.method'] !== undefined && m['@declaration.name']?.text === 'Pair',
);
expect(pairDecl?.['@declaration.return-type']?.text).toBe('(int, int)');
});
it('captures interface method return signatures for structural matching', () => {
const src = `
package main
type Shapes interface {
Touch()
Close() error
Pair() (int, error)
NamedPair() (a, b int)
}
`;
const methods = emitGoScopeCaptures(src, 'main.go')
.filter((m) => m['@declaration.method'] !== undefined)
.map((m) => ({
name: m['@declaration.name']?.text,
returns: m['@declaration.return-type']?.text,
}))
.sort((a, b) => String(a.name).localeCompare(String(b.name)));
expect(methods).toEqual([
{ name: 'Close', returns: 'error' },
{ name: 'NamedPair', returns: '(int, int)' },
{ name: 'Pair', returns: '(int, error)' },
{ name: 'Touch', returns: undefined },
]);
});
});

File diff suppressed because it is too large Load diff

View file

@ -16,7 +16,9 @@ describe('Go receiver binding', () => {
const result = synthesizeGoReceiverBinding(methodNode as any)!;
expect(result['@type-binding.self']).toBeDefined();
expect(result['@type-binding.name']!.text).toBe('u');
expect(result['@type-binding.type']!.text).toBe('User');
expect(result['@type-binding.type']!.text).toBe('*User');
const parsed = interpretGoTypeBinding(result);
expect(parsed?.rawTypeName).toBe('*User');
});
it('returns null for free function', () => {
@ -67,6 +69,25 @@ describe('Go type binding synthesis — 7 patterns', () => {
expect(qMatch).toBeDefined();
});
it('prefers concrete RHS composite literal type for explicit interface var declarations', () => {
const src = `package main
type Repository interface{ Save(User) error }
type User struct{}
type SqlRepository struct{}
func main() {
var repo Repository = SqlRepository{}
_ = repo
}`;
const matches = emitGoScopeCaptures(src, 'main.go');
const concreteMatch = matches.find(
(m) =>
m['@type-binding.constructor'] !== undefined &&
m['@type-binding.name']?.text === 'repo' &&
m['@type-binding.type']?.text === 'SqlRepository',
);
expect(concreteMatch).toBeDefined();
});
it('keeps multi-assignment constructor bindings aligned with RHS positions', () => {
const src = 'package main\nfunc main() {\n a, b := 42, X{}\n}';
const bindings = emitGoScopeCaptures(src, 'main.go')