mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-30 06:42:43 +00:00
feat(go): infer structural interface implementations (#1966)
This commit is contained in:
parent
e466ee7451
commit
eb2ba3dcad
32 changed files with 2562 additions and 169 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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(', ')})`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
14
gitnexus/test/fixtures/lang-resolution/go-structural-interface-cross-package/api/repository.go
vendored
Normal file
14
gitnexus/test/fixtures/lang-resolution/go-structural-interface-cross-package/api/repository.go
vendored
Normal 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
|
||||
}
|
||||
20
gitnexus/test/fixtures/lang-resolution/go-structural-interface-cross-package/cmd/main.go
vendored
Normal file
20
gitnexus/test/fixtures/lang-resolution/go-structural-interface-cross-package/cmd/main.go
vendored
Normal 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()
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package contracts
|
||||
|
||||
import apix "github.com/example/gostruct/api"
|
||||
|
||||
type ReadCloser interface {
|
||||
apix.Reader
|
||||
Close() error
|
||||
}
|
||||
3
gitnexus/test/fixtures/lang-resolution/go-structural-interface-cross-package/go.mod
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/go-structural-interface-cross-package/go.mod
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module github.com/example/gostruct
|
||||
|
||||
go 1.21
|
||||
17
gitnexus/test/fixtures/lang-resolution/go-structural-interface-cross-package/impl/file.go
vendored
Normal file
17
gitnexus/test/fixtures/lang-resolution/go-structural-interface-cross-package/impl/file.go
vendored
Normal 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
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/go-structural-interface-cross-package/other/user.go
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/go-structural-interface-cross-package/other/user.go
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package other
|
||||
|
||||
type User struct {
|
||||
ID string
|
||||
}
|
||||
26
gitnexus/test/fixtures/lang-resolution/go-structural-interface-cross-package/store/repository.go
vendored
Normal file
26
gitnexus/test/fixtures/lang-resolution/go-structural-interface-cross-package/store/repository.go
vendored
Normal 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
|
||||
}
|
||||
102
gitnexus/test/fixtures/lang-resolution/go-structural-interface-dispatch/repository.go
vendored
Normal file
102
gitnexus/test/fixtures/lang-resolution/go-structural-interface-dispatch/repository.go
vendored
Normal 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() {}
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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')
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue