gitnexus 1.6.6-rc.115 → 1.6.6-rc.116

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -98,6 +98,15 @@ const C_SCOPE_QUERY = `
98
98
  declarator: (init_declarator
99
99
  declarator: (identifier) @declaration.name)) @declaration.variable
100
100
 
101
+ ;; Declarations — variables (without initializer), including non-leading
102
+ ;; declarators in mixed declaration lists.
103
+ (declaration
104
+ declarator: (identifier) @declaration.name) @declaration.variable
105
+
106
+ (declaration
107
+ declarator: (pointer_declarator
108
+ declarator: (identifier) @declaration.name)) @declaration.variable
109
+
101
110
  ;; Declarations — macro definitions
102
111
  (preproc_def
103
112
  name: (identifier) @declaration.name) @declaration.macro
@@ -272,6 +272,15 @@ const CPP_SCOPE_QUERY = `
272
272
  declarator: (init_declarator
273
273
  declarator: (identifier) @declaration.name)) @declaration.variable
274
274
 
275
+ ;; ─── Declarations — variables (without initializer) ─────────────────
276
+ ;; Covers non-leading declarators in mixed declaration lists.
277
+ (declaration
278
+ declarator: (identifier) @declaration.name) @declaration.variable
279
+
280
+ (declaration
281
+ declarator: (pointer_declarator
282
+ declarator: (identifier) @declaration.name)) @declaration.variable
283
+
275
284
  ;; ─── Declarations — macro definitions ───────────────────────────────
276
285
  (preproc_def
277
286
  name: (identifier) @declaration.name) @declaration.macro
@@ -3,6 +3,7 @@ export interface GoArityMetadata {
3
3
  readonly parameterCount?: number;
4
4
  readonly requiredParameterCount?: number;
5
5
  readonly parameterTypes?: readonly string[];
6
+ readonly returnType?: string;
6
7
  }
7
8
  export declare function computeGoDeclarationArity(node: SyntaxNode): GoArityMetadata;
8
9
  export declare function computeGoCallArity(callNode: SyntaxNode): number;
@@ -1,7 +1,14 @@
1
1
  export function computeGoDeclarationArity(node) {
2
2
  const params = node.childForFieldName('parameters');
3
+ const returnType = extractGoReturnType(node);
3
4
  if (params === null)
4
- return {};
5
+ return returnType === undefined ? {} : { returnType };
6
+ return {
7
+ ...computeGoParameterMetadata(params),
8
+ ...(returnType === undefined ? {} : { returnType }),
9
+ };
10
+ }
11
+ function computeGoParameterMetadata(params) {
5
12
  let count = 0;
6
13
  let required = 0;
7
14
  const types = [];
@@ -35,3 +42,26 @@ export function computeGoCallArity(callNode) {
35
42
  return 0;
36
43
  return args.namedChildCount;
37
44
  }
45
+ function extractGoReturnType(node) {
46
+ const result = node.childForFieldName('result');
47
+ if (result === null)
48
+ return undefined;
49
+ if (result.type !== 'parameter_list')
50
+ return result.text;
51
+ const returnTypes = [];
52
+ for (const child of result.namedChildren) {
53
+ if (child.type !== 'parameter_declaration')
54
+ continue;
55
+ const typeNode = child.childForFieldName('type');
56
+ if (typeNode === null)
57
+ continue;
58
+ const names = child.namedChildren.filter((c) => c.type === 'identifier');
59
+ const n = Math.max(1, names.length);
60
+ for (let i = 0; i < n; i++) {
61
+ returnTypes.push(typeNode.text);
62
+ }
63
+ }
64
+ if (returnTypes.length === 0)
65
+ return undefined;
66
+ return returnTypes.length === 1 ? returnTypes[0] : `(${returnTypes.join(', ')})`;
67
+ }
@@ -74,7 +74,8 @@ export function emitGoScopeCaptures(sourceText, _filePath, cachedTree) {
74
74
  // @declaration.function / @declaration.method are captured directly on
75
75
  // the function_declaration / method_declaration node.
76
76
  const fnNode = declAnchorNode.type === 'function_declaration' ||
77
- declAnchorNode.type === 'method_declaration'
77
+ declAnchorNode.type === 'method_declaration' ||
78
+ declAnchorNode.type === 'method_elem'
78
79
  ? declAnchorNode
79
80
  : null;
80
81
  if (fnNode !== null) {
@@ -88,6 +89,9 @@ export function emitGoScopeCaptures(sourceText, _filePath, cachedTree) {
88
89
  if (arity.parameterTypes !== undefined) {
89
90
  grouped['@declaration.parameter-types'] = syntheticCapture('@declaration.parameter-types', fnNode, JSON.stringify(arity.parameterTypes));
90
91
  }
92
+ if (arity.returnType !== undefined) {
93
+ grouped['@declaration.return-type'] = syntheticCapture('@declaration.return-type', fnNode, arity.returnType);
94
+ }
91
95
  }
92
96
  out.push(grouped);
93
97
  continue;
@@ -1,72 +1,467 @@
1
+ import { simpleQualifiedName } from '../../scope-resolution/graph-bridge/ids.js';
2
+ import { resolveInheritanceBaseInScope } from '../../scope-resolution/scope/walkers.js';
1
3
  export function detectGoInterfaceImplementations(parsedFiles, _indexes, _model) {
2
- // 1. Collect interface defs → method names (from scope.ownedDefs)
3
- const interfaceMethods = new Map();
4
- const interfaceDefsById = new Map();
5
- // 2. Collect struct defs → method names
6
- const structMethods = new Map();
4
+ return detectGoInterfaceImplementationsFromIndexes(buildDetectionIndexes(parsedFiles, _indexes));
5
+ }
6
+ function buildDetectionIndexes(parsedFiles, indexes) {
7
+ const interfaces = [];
8
+ const structsById = new Map();
9
+ const methodsByOwner = new Map();
10
+ const effectiveMethodsByStructId = new Map();
11
+ const interfaceById = new Map();
12
+ const interfaceOwnMethodsById = new Map();
13
+ const embeddedSitesByInterfaceId = new Map();
14
+ const parentStructIdsByStructId = new Map();
15
+ const structIdsByMethodName = new Map();
16
+ const signatureContextByDefId = new Map();
17
+ const interfaceIdByScopeId = new Map();
18
+ const structIdByScopeId = new Map();
19
+ for (const parsed of parsedFiles) {
20
+ const signatureContext = signatureContextForFile(parsed, indexes);
21
+ for (const def of parsed.localDefs) {
22
+ signatureContextByDefId.set(def.nodeId, signatureContext);
23
+ if (def.type === 'Interface') {
24
+ interfaces.push(def);
25
+ interfaceById.set(def.nodeId, def);
26
+ continue;
27
+ }
28
+ if (def.type === 'Struct') {
29
+ structsById.set(def.nodeId, def);
30
+ continue;
31
+ }
32
+ if (def.type !== 'Method' && def.type !== 'Function')
33
+ continue;
34
+ if (def.ownerId === undefined)
35
+ continue;
36
+ if (isPointerReceiverMethod(def))
37
+ continue;
38
+ const methodName = simpleQualifiedName(def);
39
+ if (methodName === undefined || methodName.length === 0)
40
+ continue;
41
+ addMethod(methodsByOwner, def.ownerId, methodName, def);
42
+ }
43
+ }
44
+ for (const parsed of parsedFiles) {
45
+ for (const scope of parsed.scopes) {
46
+ const iface = scope.ownedDefs.find((def) => def.type === 'Interface');
47
+ if (iface !== undefined)
48
+ interfaceIdByScopeId.set(scope.id, iface.nodeId);
49
+ const struct = scope.ownedDefs.find((def) => def.type === 'Struct');
50
+ if (struct !== undefined)
51
+ structIdByScopeId.set(scope.id, struct.nodeId);
52
+ }
53
+ }
7
54
  for (const parsed of parsedFiles) {
8
- // Collect interface defs and their owned methods
55
+ const childScopesByParent = new Map();
9
56
  for (const scope of parsed.scopes) {
10
- if (scope.kind !== 'Class')
57
+ if (scope.parent === null)
11
58
  continue;
12
- // Find the type def for this scope
13
- const typeDef = scope.ownedDefs.find((d) => d.type === 'Interface' || d.type === 'Struct');
14
- if (typeDef === undefined)
59
+ const children = childScopesByParent.get(scope.parent) ?? [];
60
+ children.push(scope);
61
+ childScopesByParent.set(scope.parent, children);
62
+ }
63
+ for (const scope of parsed.scopes) {
64
+ const ifaceId = interfaceIdByScopeId.get(scope.id);
65
+ if (ifaceId === undefined)
15
66
  continue;
16
- if (typeDef.type === 'Interface') {
17
- interfaceDefsById.set(typeDef.nodeId, typeDef);
18
- const methodNames = new Set();
19
- // Methods are in child scopes (Function kind) or ownedDefs
20
- for (const childScope of parsed.scopes) {
21
- if (childScope.parent === scope.id && childScope.kind === 'Function') {
22
- for (const def of childScope.ownedDefs) {
23
- if (def.type === 'Method' || def.type === 'Function') {
24
- methodNames.add(def.qualifiedName?.split('.').pop() ?? '');
25
- }
26
- }
27
- }
28
- }
29
- // Also check if methods have ownerId pointing to this interface
30
- for (const def of parsed.localDefs) {
31
- if (def.ownerId === typeDef.nodeId &&
32
- (def.type === 'Method' || def.type === 'Function')) {
33
- methodNames.add(def.qualifiedName?.split('.').pop() ?? '');
34
- }
67
+ const methods = new Map();
68
+ for (const childScope of childScopesByParent.get(scope.id) ?? []) {
69
+ for (const def of childScope.ownedDefs) {
70
+ if (def.type !== 'Method' && def.type !== 'Function')
71
+ continue;
72
+ const methodName = simpleQualifiedName(def);
73
+ if (methodName === undefined || methodName.length === 0)
74
+ continue;
75
+ addMethodOverload(methods, methodName, def);
35
76
  }
36
- interfaceMethods.set(typeDef.nodeId, methodNames);
37
77
  }
38
- if (typeDef.type === 'Struct') {
39
- const methodNames = new Set();
40
- for (const def of parsed.localDefs) {
41
- if (def.ownerId === typeDef.nodeId &&
42
- (def.type === 'Method' || def.type === 'Function')) {
43
- methodNames.add(def.qualifiedName?.split('.').pop() ?? '');
44
- }
45
- }
46
- structMethods.set(typeDef.nodeId, methodNames);
78
+ interfaceOwnMethodsById.set(ifaceId, methods);
79
+ }
80
+ for (const site of parsed.referenceSites) {
81
+ if (site.kind !== 'inherits')
82
+ continue;
83
+ const ifaceId = interfaceIdByScopeId.get(site.inScope);
84
+ if (ifaceId !== undefined) {
85
+ const sites = embeddedSitesByInterfaceId.get(ifaceId) ?? [];
86
+ sites.push(site);
87
+ embeddedSitesByInterfaceId.set(ifaceId, sites);
88
+ continue;
47
89
  }
90
+ const structId = structIdByScopeId.get(site.inScope);
91
+ if (structId === undefined)
92
+ continue;
93
+ const parent = resolveInheritanceBaseInScope(site.inScope, site.name, indexes);
94
+ if (parent === undefined || parent.type !== 'Struct')
95
+ continue;
96
+ addParentStruct(parentStructIdsByStructId, structId, parent.nodeId);
48
97
  }
49
98
  }
50
- // 3. For each interface, find structs whose method set is a superset
51
- const impls = new Map();
52
- for (const [ifaceId, ifaceMethods] of interfaceMethods) {
53
- if (ifaceMethods.size === 0)
99
+ const structMethodSetCache = new Map();
100
+ for (const structId of structsById.keys()) {
101
+ const effective = collectStructMethodSet(structId, {
102
+ parentStructIdsByStructId,
103
+ methodsByOwner,
104
+ }, new Set(), structMethodSetCache);
105
+ if (effective === undefined)
106
+ continue;
107
+ effectiveMethodsByStructId.set(structId, effective);
108
+ for (const methodName of effective.keys()) {
109
+ addStructMethodCandidate(structIdsByMethodName, methodName, structId);
110
+ }
111
+ }
112
+ return {
113
+ interfaces,
114
+ structsById,
115
+ methodsByOwner,
116
+ effectiveMethodsByStructId,
117
+ interfaceById,
118
+ interfaceOwnMethodsById,
119
+ embeddedSitesByInterfaceId,
120
+ parentStructIdsByStructId,
121
+ structIdsByMethodName,
122
+ signatureContextByDefId,
123
+ scopeIndexes: indexes,
124
+ };
125
+ }
126
+ function detectGoInterfaceImplementationsFromIndexes(indexes) {
127
+ const implementations = new Map();
128
+ const methodSetCache = new Map();
129
+ for (const iface of indexes.interfaces) {
130
+ const required = collectInterfaceMethodSet(iface, indexes, new Set(), methodSetCache);
131
+ if (required === undefined || required.size === 0)
132
+ continue;
133
+ if (!methodSetHasVerifiableSignatures(required))
54
134
  continue;
55
135
  const implementors = [];
56
- for (const [structId, methods] of structMethods) {
57
- if (isSuperset(methods, ifaceMethods)) {
136
+ for (const structId of candidateStructIdsFor(required, indexes)) {
137
+ const actual = indexes.effectiveMethodsByStructId.get(structId);
138
+ if (actual === undefined)
139
+ continue;
140
+ if (methodSetSatisfies(actual, required, indexes.signatureContextByDefId)) {
58
141
  implementors.push(structId);
59
142
  }
60
143
  }
61
144
  if (implementors.length > 0)
62
- impls.set(ifaceId, implementors);
145
+ implementations.set(iface.nodeId, implementors);
146
+ }
147
+ return implementations;
148
+ }
149
+ function addMethod(methodsByOwner, ownerId, methodName, def) {
150
+ let methods = methodsByOwner.get(ownerId);
151
+ if (methods === undefined) {
152
+ methods = new Map();
153
+ methodsByOwner.set(ownerId, methods);
154
+ }
155
+ addMethodOverload(methods, methodName, def);
156
+ }
157
+ function addMethodOverload(methods, methodName, def) {
158
+ const overloads = methods.get(methodName) ?? [];
159
+ overloads.push(def);
160
+ methods.set(methodName, overloads);
161
+ }
162
+ function addStructMethodCandidate(structIdsByMethodName, methodName, structId) {
163
+ const structIds = structIdsByMethodName.get(methodName) ?? new Set();
164
+ structIds.add(structId);
165
+ structIdsByMethodName.set(methodName, structIds);
166
+ }
167
+ function addParentStruct(parentStructIdsByStructId, structId, parentStructId) {
168
+ const parents = parentStructIdsByStructId.get(structId) ?? [];
169
+ parents.push(parentStructId);
170
+ parentStructIdsByStructId.set(structId, parents);
171
+ }
172
+ function collectStructMethodSet(structId, indexes, visiting, cache) {
173
+ const entries = collectStructMethodEntries(structId, indexes, visiting, cache);
174
+ return entries === undefined ? undefined : methodEntriesToMethodSet(entries);
175
+ }
176
+ function collectStructMethodEntries(structId, indexes, visiting, cache) {
177
+ const cached = cache.get(structId);
178
+ if (cached !== undefined)
179
+ return cloneMethodEntries(cached);
180
+ if (visiting.has(structId))
181
+ return undefined;
182
+ visiting.add(structId);
183
+ const merged = directMethodEntries(indexes.methodsByOwner.get(structId));
184
+ for (const parentStructId of indexes.parentStructIdsByStructId.get(structId) ?? []) {
185
+ const parentEntries = collectStructMethodEntries(parentStructId, indexes, visiting, cache);
186
+ if (parentEntries === undefined) {
187
+ visiting.delete(structId);
188
+ return undefined;
189
+ }
190
+ for (const [methodName, entry] of parentEntries) {
191
+ if (entry.ambiguous)
192
+ continue;
193
+ mergePromotedMethodEntry(merged, methodName, {
194
+ overloads: entry.overloads,
195
+ depth: entry.depth + 1,
196
+ ambiguous: false,
197
+ });
198
+ }
199
+ }
200
+ visiting.delete(structId);
201
+ cache.set(structId, cloneMethodEntries(merged));
202
+ return merged;
203
+ }
204
+ function collectInterfaceMethodSet(iface, indexes, visiting, cache) {
205
+ const cached = cache.get(iface.nodeId);
206
+ if (cached !== undefined)
207
+ return cloneMethodSet(cached);
208
+ if (visiting.has(iface.nodeId))
209
+ return undefined;
210
+ visiting.add(iface.nodeId);
211
+ const ownMethods = indexes.methodsByOwner.get(iface.nodeId) ?? indexes.interfaceOwnMethodsById.get(iface.nodeId);
212
+ const merged = cloneMethodSet(ownMethods);
213
+ const embeddedInterfaces = embeddedInterfacesFor(iface, indexes);
214
+ if (embeddedInterfaces === undefined) {
215
+ visiting.delete(iface.nodeId);
216
+ return undefined;
217
+ }
218
+ for (const embeddedIface of embeddedInterfaces) {
219
+ const embeddedMethods = collectInterfaceMethodSet(embeddedIface, indexes, visiting, cache);
220
+ if (embeddedMethods === undefined) {
221
+ visiting.delete(iface.nodeId);
222
+ return undefined;
223
+ }
224
+ mergeMethodSet(merged, embeddedMethods);
225
+ }
226
+ visiting.delete(iface.nodeId);
227
+ cache.set(iface.nodeId, cloneMethodSet(merged));
228
+ return merged;
229
+ }
230
+ function embeddedInterfacesFor(iface, indexes) {
231
+ const embedded = [];
232
+ for (const site of indexes.embeddedSitesByInterfaceId.get(iface.nodeId) ?? []) {
233
+ const resolved = resolveEmbeddedInterface(site, indexes);
234
+ if (resolved === undefined)
235
+ return undefined;
236
+ embedded.push(resolved);
237
+ }
238
+ return embedded;
239
+ }
240
+ function candidateStructIdsFor(required, indexes) {
241
+ let best;
242
+ for (const name of required.keys()) {
243
+ const candidates = indexes.structIdsByMethodName.get(name);
244
+ if (candidates === undefined)
245
+ return [];
246
+ if (best === undefined || candidates.size < best.size)
247
+ best = candidates;
248
+ }
249
+ return best === undefined ? [...indexes.structsById.keys()] : [...best];
250
+ }
251
+ function resolveEmbeddedInterface(site, indexes) {
252
+ const bound = resolveInheritanceBaseInScope(site.inScope, site.name, indexes.scopeIndexes);
253
+ if (bound !== undefined)
254
+ return bound.type === 'Interface' ? bound : undefined;
255
+ const simpleName = simpleTypeName(site.name);
256
+ const matches = [];
257
+ for (const iface of indexes.interfaceById.values()) {
258
+ if (iface.qualifiedName === site.name || iface.qualifiedName === simpleName) {
259
+ matches.push(iface);
260
+ }
261
+ }
262
+ return matches.length === 1 ? matches[0] : undefined;
263
+ }
264
+ function simpleTypeName(name) {
265
+ const dot = name.lastIndexOf('.');
266
+ return dot === -1 ? name : name.slice(dot + 1);
267
+ }
268
+ function cloneMethodSet(methods) {
269
+ const clone = new Map();
270
+ if (methods === undefined)
271
+ return clone;
272
+ for (const [name, overloads] of methods) {
273
+ clone.set(name, [...overloads]);
274
+ }
275
+ return clone;
276
+ }
277
+ function directMethodEntries(methods) {
278
+ const entries = new Map();
279
+ if (methods === undefined)
280
+ return entries;
281
+ for (const [name, overloads] of methods) {
282
+ entries.set(name, { overloads: [...overloads], depth: 0, ambiguous: false });
283
+ }
284
+ return entries;
285
+ }
286
+ function cloneMethodEntries(entries) {
287
+ const clone = new Map();
288
+ for (const [name, entry] of entries) {
289
+ clone.set(name, { ...entry, overloads: [...entry.overloads] });
290
+ }
291
+ return clone;
292
+ }
293
+ function methodEntriesToMethodSet(entries) {
294
+ const methods = new Map();
295
+ for (const [name, entry] of entries) {
296
+ if (entry.ambiguous)
297
+ continue;
298
+ methods.set(name, [...entry.overloads]);
299
+ }
300
+ return methods;
301
+ }
302
+ function mergePromotedMethodEntry(target, methodName, candidate) {
303
+ const existing = target.get(methodName);
304
+ if (existing === undefined || candidate.depth < existing.depth) {
305
+ target.set(methodName, candidate);
306
+ return;
307
+ }
308
+ if (candidate.depth > existing.depth)
309
+ return;
310
+ target.set(methodName, { overloads: [], depth: candidate.depth, ambiguous: true });
311
+ }
312
+ function mergeMethodSet(target, source) {
313
+ for (const [name, overloads] of source) {
314
+ const existing = target.get(name) ?? [];
315
+ existing.push(...overloads);
316
+ target.set(name, existing);
63
317
  }
64
- return impls;
65
318
  }
66
- function isSuperset(superset, subset) {
67
- for (const item of subset) {
68
- if (!superset.has(item))
319
+ function methodSetSatisfies(actual, required, signatureContextByDefId) {
320
+ for (const [name, requiredOverloads] of required) {
321
+ const actualOverloads = actual.get(name);
322
+ if (actualOverloads === undefined)
69
323
  return false;
324
+ for (const requiredMethod of requiredOverloads) {
325
+ if (!hasCompatibleMethod(actualOverloads, requiredMethod, signatureContextByDefId)) {
326
+ return false;
327
+ }
328
+ }
70
329
  }
71
330
  return true;
72
331
  }
332
+ function hasCompatibleMethod(actualOverloads, requiredMethod, signatureContextByDefId) {
333
+ if (!hasVerifiableSignature(requiredMethod))
334
+ return false;
335
+ return actualOverloads.some((actualMethod) => signaturesCompatible(actualMethod, requiredMethod, signatureContextByDefId));
336
+ }
337
+ function methodSetHasVerifiableSignatures(methods) {
338
+ for (const overloads of methods.values()) {
339
+ if (!overloads.some(hasVerifiableSignature))
340
+ return false;
341
+ }
342
+ return true;
343
+ }
344
+ function isPointerReceiverMethod(def) {
345
+ return def.goReceiverKind === 'pointer';
346
+ }
347
+ function hasVerifiableSignature(def) {
348
+ return (def.parameterCount !== undefined ||
349
+ def.requiredParameterCount !== undefined ||
350
+ (def.parameterTypes !== undefined && def.parameterTypes.length > 0) ||
351
+ def.returnType !== undefined);
352
+ }
353
+ function signaturesCompatible(actual, required, signatureContextByDefId) {
354
+ const actualContext = signatureContextByDefId.get(actual.nodeId);
355
+ const requiredContext = signatureContextByDefId.get(required.nodeId);
356
+ return (countsCompatible(actual.parameterCount, required.parameterCount) &&
357
+ countsCompatible(actual.requiredParameterCount, required.requiredParameterCount) &&
358
+ parameterTypesCompatible(actual.parameterTypes, required.parameterTypes, actualContext, requiredContext) &&
359
+ returnTypesCompatible(actual.returnType, required.returnType, actualContext, requiredContext));
360
+ }
361
+ function countsCompatible(actual, required) {
362
+ return actual === undefined || required === undefined || actual === required;
363
+ }
364
+ function parameterTypesCompatible(actual, required, actualContext, requiredContext) {
365
+ if (actual === undefined || required === undefined)
366
+ return true;
367
+ if (actual.length !== required.length)
368
+ return false;
369
+ return actual.every((type, index) => {
370
+ const actualType = normalizeSignatureType(type, actualContext);
371
+ const requiredType = normalizeSignatureType(required[index], requiredContext);
372
+ return actualType !== undefined && requiredType !== undefined && actualType === requiredType;
373
+ });
374
+ }
375
+ function returnTypesCompatible(actual, required, actualContext, requiredContext) {
376
+ if (required === undefined)
377
+ return actual === undefined;
378
+ if (actual === undefined)
379
+ return false;
380
+ const actualType = normalizeSignatureType(actual, actualContext);
381
+ const requiredType = normalizeSignatureType(required, requiredContext);
382
+ return actualType !== undefined && requiredType !== undefined && actualType === requiredType;
383
+ }
384
+ function normalizeSignatureType(typeName, context) {
385
+ // Go type identity includes pointer/slice/map/variadic shape and package
386
+ // qualifiers. Only erase whitespace and qualify bare local type names; stripping
387
+ // `*`, `[]`, `...`, or `pkg.` would make non-identical signatures compare equal.
388
+ const compact = typeName.replace(/\s+/g, '');
389
+ if (context === undefined)
390
+ return compact;
391
+ return qualifyGoSignatureTypes(compact, context);
392
+ }
393
+ function qualifyGoSignatureTypes(typeName, context) {
394
+ let unresolvedQualifier = false;
395
+ const qualified = typeName.replace(/[A-Za-z_][A-Za-z0-9_]*/g, (token, offset, source) => {
396
+ if (GO_BUILTIN_TYPES.has(token))
397
+ return token;
398
+ if (hasPackageQualifierDot(source, offset))
399
+ return token;
400
+ if (source[offset + token.length] === '.') {
401
+ const qualifier = context.importQualifiers.get(token);
402
+ if (qualifier !== undefined)
403
+ return qualifier;
404
+ unresolvedQualifier = true;
405
+ return token;
406
+ }
407
+ if (context.packageQualifier === undefined)
408
+ return token;
409
+ return `${context.packageQualifier}.${token}`;
410
+ });
411
+ return unresolvedQualifier ? undefined : qualified;
412
+ }
413
+ function hasPackageQualifierDot(source, offset) {
414
+ return source[offset - 1] === '.' && source[offset - 2] !== '.';
415
+ }
416
+ function signatureContextForFile(parsed, indexes) {
417
+ const importQualifiers = new Map();
418
+ const importEdges = indexes.imports?.get(parsed.moduleScope) ?? [];
419
+ for (const edge of importEdges) {
420
+ if (edge.kind !== 'namespace' || edge.targetFile === null)
421
+ continue;
422
+ const qualifier = packageQualifierForFile(edge.targetFile);
423
+ if (qualifier !== undefined)
424
+ importQualifiers.set(edge.localName, qualifier);
425
+ }
426
+ return {
427
+ packageQualifier: packageQualifierForFile(parsed.filePath),
428
+ importQualifiers,
429
+ };
430
+ }
431
+ function packageQualifierForFile(filePath) {
432
+ const normalized = filePath.replace(/\\/g, '/');
433
+ const slash = normalized.lastIndexOf('/');
434
+ if (slash === -1)
435
+ return undefined;
436
+ const packageDir = normalized.slice(0, slash);
437
+ return packageDir.length === 0 ? undefined : packageDir;
438
+ }
439
+ const GO_BUILTIN_TYPES = new Set([
440
+ 'any',
441
+ 'bool',
442
+ 'byte',
443
+ 'comparable',
444
+ 'complex64',
445
+ 'complex128',
446
+ 'error',
447
+ 'float32',
448
+ 'float64',
449
+ 'func',
450
+ 'int',
451
+ 'int8',
452
+ 'int16',
453
+ 'int32',
454
+ 'int64',
455
+ 'interface',
456
+ 'map',
457
+ 'rune',
458
+ 'string',
459
+ 'struct',
460
+ 'uint',
461
+ 'uint8',
462
+ 'uint16',
463
+ 'uint32',
464
+ 'uint64',
465
+ 'uintptr',
466
+ 'chan',
467
+ ]);
@@ -28,7 +28,10 @@ export function interpretGoTypeBinding(captures) {
28
28
  let normalizedType;
29
29
  if (captures['@type-binding.self'] !== undefined) {
30
30
  source = 'self';
31
- normalizedType = normalizeGoTypeName(type);
31
+ // Preserve pointer shape on receiver self-bindings (`*T` vs `T`).
32
+ // Method-owner enrichment consumes that raw shape to model Go value and
33
+ // pointer receiver method sets conservatively.
34
+ normalizedType = type.trim();
32
35
  }
33
36
  else if (captures['@type-binding.constructor'] !== undefined) {
34
37
  source = 'constructor-inferred';
@@ -36,12 +36,15 @@ export function populateGoWorkspaceOwners(parsedFiles, ctx) {
36
36
  function populateGoOwnersInPackage(parsedFiles) {
37
37
  // Build struct name → def map from ALL scopes' ownedDefs (struct defs
38
38
  // live in Class scopes now, not Module scope).
39
- const structByQualifiedName = new Map(); // qname → nodeId
39
+ const structByQualifiedName = new Map();
40
40
  for (const parsed of parsedFiles) {
41
41
  for (const scope of parsed.scopes) {
42
42
  for (const def of scope.ownedDefs) {
43
43
  if (isClassLike(def.type) && def.qualifiedName) {
44
- structByQualifiedName.set(def.qualifiedName, def.nodeId);
44
+ structByQualifiedName.set(def.qualifiedName, {
45
+ nodeId: def.nodeId,
46
+ qualifiedName: def.qualifiedName,
47
+ });
45
48
  }
46
49
  }
47
50
  }
@@ -59,26 +62,35 @@ function populateGoOwnersInPackage(parsedFiles) {
59
62
  continue;
60
63
  // Find the self typeBinding in this Function scope.
61
64
  let receiverType;
65
+ let receiverKind = 'value';
62
66
  for (const [, tb] of scope.typeBindings) {
63
67
  if (tb.source === 'self') {
64
68
  receiverType = tb.rawName;
69
+ receiverKind = tb.rawName.trim().startsWith('*') ? 'pointer' : 'value';
65
70
  break;
66
71
  }
67
72
  }
68
73
  if (receiverType === undefined)
69
74
  continue;
70
- let ownerId = structByQualifiedName.get(receiverType);
71
- if (ownerId === undefined) {
72
- for (const [qname, nodeId] of structByQualifiedName) {
75
+ receiverType = receiverType.replace(/^\*+/, '').trim();
76
+ let owner = structByQualifiedName.get(receiverType);
77
+ if (owner === undefined) {
78
+ for (const [qname, candidate] of structByQualifiedName) {
73
79
  if (qname.endsWith('.' + receiverType)) {
74
- ownerId = nodeId;
80
+ owner = candidate;
75
81
  break;
76
82
  }
77
83
  }
78
84
  }
79
- if (ownerId !== undefined) {
85
+ if (owner !== undefined) {
80
86
  for (const def of methodDefs) {
81
- def.ownerId = ownerId;
87
+ def.ownerId = owner.nodeId;
88
+ def.goReceiverKind = receiverKind;
89
+ const simpleName = def.qualifiedName?.split('.').pop() ?? def.qualifiedName;
90
+ if (simpleName !== undefined && !def.qualifiedName?.includes('.')) {
91
+ def.qualifiedName =
92
+ `${owner.qualifiedName}.${simpleName}`;
93
+ }
82
94
  }
83
95
  }
84
96
  }
@@ -38,6 +38,10 @@ const GO_SCOPE_QUERY = `
38
38
  (method_declaration
39
39
  name: (field_identifier) @declaration.name) @declaration.method
40
40
 
41
+ ;; Declarations — interface methods
42
+ (method_elem
43
+ name: (field_identifier) @declaration.name) @declaration.method
44
+
41
45
  ;; Declarations — struct fields
42
46
  (struct_type
43
47
  (field_declaration_list
@@ -54,9 +54,10 @@ export function populateGoRangeBindings(parsedFiles, _indexes, ctx) {
54
54
  continue;
55
55
  // Resolve range expression type
56
56
  let elementType = null;
57
+ const functionScope = findEnclosingFunctionScope(rangeNode, scopeMap);
57
58
  if (rangeExpr.type === 'identifier') {
58
- // Look up the identifier's type in scope typeBindings (V1: module scope only)
59
- const binding = moduleScope.typeBindings.get(rangeExpr.text);
59
+ const binding = functionScope?.typeBindings.get(rangeExpr.text) ??
60
+ moduleScope.typeBindings.get(rangeExpr.text);
60
61
  if (binding !== null && binding !== undefined) {
61
62
  elementType = extractElementType(binding);
62
63
  }
@@ -77,7 +78,6 @@ export function populateGoRangeBindings(parsedFiles, _indexes, ctx) {
77
78
  }
78
79
  if (elementType !== null && valueVar !== null) {
79
80
  // Inject type binding for the range variable onto the enclosing function scope
80
- const functionScope = findEnclosingFunctionScope(rangeNode, scopeMap);
81
81
  const targetScope = functionScope ?? moduleScope;
82
82
  const mutable = targetScope.typeBindings;
83
83
  mutable.set(valueVar, {
@@ -111,7 +111,7 @@ function findEnclosingFunctionScope(node, scopeMap) {
111
111
  // same line/column as the tree-sitter node.
112
112
  for (const scope of scopeMap.values()) {
113
113
  if (scope.kind === 'Function' &&
114
- scope.range.startLine === current.startPosition.row &&
114
+ scope.range.startLine === current.startPosition.row + 1 &&
115
115
  scope.range.startCol === current.startPosition.column) {
116
116
  return scope;
117
117
  }
@@ -12,7 +12,7 @@ export function synthesizeGoReceiverBinding(fnNode) {
12
12
  const typeNode = param.childForFieldName('type');
13
13
  if (nameNode === null || typeNode === null)
14
14
  return null;
15
- const typeName = typeNode.text.replace(/^\*/, '');
15
+ const typeName = typeNode.text;
16
16
  return {
17
17
  '@type-binding.self': syntheticCapture('@type-binding.self', fnNode, nameNode.text),
18
18
  '@type-binding.name': syntheticCapture('@type-binding.name', nameNode, nameNode.text),
@@ -26,8 +26,8 @@ export const goScopeResolver = {
26
26
  allowGlobalFreeCallFallback: true,
27
27
  populateNamespaceSiblings: populateGoPackageSiblings,
28
28
  mirrorNamespaceTypeBindings: mirrorGoNamespaceTypeBindings,
29
- // Staged/V2: registered but not yet wired in run.ts — method-name-only
30
- // matching produces false IMPLEMENTS edges; awaits signature-level comparison.
29
+ // Go has structural interfaces: implementations are inferred by signature,
30
+ // then fed into generic MRO/interface-dispatch.
31
31
  detectInterfaceImplementations: detectGoInterfaceImplementations,
32
32
  populateRangeBindings: populateGoRangeBindings,
33
33
  };
@@ -1,8 +1,9 @@
1
1
  export function goBindingScopeFor(decl, innermost, _tree) {
2
- // Keep self typeBindings in the method's Function scope (prevent
3
- // auto-hoist to Module) so populateGoOwners can match Method defs
4
- // to their receiver types by inspecting each Function scope.
5
- if (decl['@type-binding.self'] !== undefined) {
2
+ // Keep receiver and parameter typeBindings in the function scope
3
+ // (prevent auto-hoist to Module). Parameters are local variables:
4
+ // hoisting `repo Repository` from one function to module scope can
5
+ // pollute another function's local receiver resolution.
6
+ if (decl['@type-binding.self'] !== undefined || decl['@type-binding.parameter'] !== undefined) {
6
7
  return innermost.id;
7
8
  }
8
9
  return null; // default auto-hoist for other bindings
@@ -15,7 +16,11 @@ export function goReceiverBinding(functionScope) {
15
16
  return null;
16
17
  for (const binding of functionScope.typeBindings.values()) {
17
18
  if (binding.source === 'self')
18
- return binding;
19
+ return normalizeGoSelfTypeRef(binding);
19
20
  }
20
21
  return null;
21
22
  }
23
+ function normalizeGoSelfTypeRef(binding) {
24
+ const rawName = binding.rawName.replace(/^\*+/, '').trim();
25
+ return rawName === binding.rawName ? binding : { ...binding, rawName };
26
+ }
@@ -1,6 +1,23 @@
1
1
  import { syntheticCapture } from '../../utils/ast-helpers.js';
2
2
  export function synthesizeGoTypeBindings(rootNode) {
3
3
  const out = [];
4
+ for (const node of rootNode.descendantsOfType('var_spec')) {
5
+ const name = node.childForFieldName('name');
6
+ const value = node.childForFieldName('value');
7
+ if (name?.type !== 'identifier' || value === null)
8
+ continue;
9
+ const rhsExpr = value.namedChildren[0];
10
+ if (rhsExpr === undefined)
11
+ continue;
12
+ const typeNode = extractCompositeLiteralTypeNode(rhsExpr);
13
+ if (typeNode === null)
14
+ continue;
15
+ out.push({
16
+ '@type-binding.constructor': syntheticCapture('@type-binding.constructor', node, node.text),
17
+ '@type-binding.name': syntheticCapture('@type-binding.name', name, name.text),
18
+ '@type-binding.type': syntheticCapture('@type-binding.type', typeNode, extractSimpleTypeNameText(typeNode)),
19
+ });
20
+ }
4
21
  for (const node of rootNode.descendantsOfType('short_var_declaration')) {
5
22
  const right = node.childForFieldName('right');
6
23
  if (right === null)
@@ -197,6 +214,18 @@ function extractSimpleTypeNameText(node) {
197
214
  return node.text;
198
215
  }
199
216
  /** Extract the type/signature node from a RHS expression. */
217
+ function extractCompositeLiteralTypeNode(expr) {
218
+ if (expr.type === 'composite_literal') {
219
+ return (expr.childForFieldName('type') ??
220
+ expr.namedChildren.find((c) => ['type_identifier', 'qualified_type'].includes(c.type)) ??
221
+ null);
222
+ }
223
+ if (expr.type === 'unary_expression') {
224
+ const operand = expr.childForFieldName('operand');
225
+ return operand === null ? null : extractCompositeLiteralTypeNode(operand);
226
+ }
227
+ return null;
228
+ }
200
229
  function extractTypeNode(expr) {
201
230
  if (expr.type === 'composite_literal') {
202
231
  return (expr.childForFieldName('type') ??
@@ -106,7 +106,7 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
106
106
  const graphIdToClassDef = new Map();
107
107
  for (const parsed of parsedFiles) {
108
108
  for (const def of parsed.localDefs) {
109
- if (def.type !== 'Class' && def.type !== 'Interface')
109
+ if (def.type !== 'Class' && def.type !== 'Struct' && def.type !== 'Interface')
110
110
  continue;
111
111
  const graphId = resolveDefGraphId(parsed.filePath, def, nodeLookup);
112
112
  if (graphId !== undefined)
@@ -22,6 +22,7 @@
22
22
  *
23
23
  * Plan: `docs/plans/2026-04-20-001-refactor-emit-pipeline-generalization-plan.md`.
24
24
  */
25
+ import { generateId } from '../../../../lib/utils.js';
25
26
  import { lookupOwnedMembersByOwner } from '../../model/owned-members-lookup.js';
26
27
  import { reconcileOwnership, validateOwnershipParity } from './reconcile-ownership.js';
27
28
  import { validateBindingsImmutability } from './validate-bindings-immutability.js';
@@ -36,7 +37,7 @@ import { emitReceiverBoundCalls } from '../passes/receiver-bound-calls.js';
36
37
  import { emitFreeCallFallback } from '../passes/free-call-fallback.js';
37
38
  import { emitReferencesViaLookup } from '../graph-bridge/references-to-edges.js';
38
39
  import { emitImportEdges } from '../graph-bridge/imports-to-edges.js';
39
- import { findClassBindingInScope, findEnclosingClassDef, resolveAmbiguousInheritanceBaseViaImports, } from '../scope/walkers.js';
40
+ import { findEnclosingClassDef, resolveInheritanceBaseInScope } from '../scope/walkers.js';
40
41
  import { buildWorkspaceResolutionIndex } from '../workspace-index.js';
41
42
  import { logger } from '../../../logger.js';
42
43
  /**
@@ -108,13 +109,7 @@ function preEmitInheritanceEdges(graph, scopes, nodeLookup) {
108
109
  // suppression keeps `buildMro` and the final graph class-owned.
109
110
  handledSites.add(siteKey);
110
111
  }
111
- const targetDef = findClassBindingInScope(site.inScope, site.name, scopes) ??
112
- // Import-aware disambiguation fallback (#1951). Only engages when the
113
- // scope-chain + single-match lookups above returned undefined because
114
- // the simple name is ambiguous (multiple same-named class-like defs).
115
- // Picks the candidate whose defining file is imported/included by the
116
- // referencing file. Never changes behavior for single-match cases.
117
- resolveAmbiguousInheritanceBaseViaImports(site.inScope, site.name, scopes);
112
+ const targetDef = resolveInheritanceBaseInScope(site.inScope, site.name, scopes);
118
113
  if (targetDef === undefined)
119
114
  continue;
120
115
  const callerClass = findEnclosingClassDef(site.inScope, scopes);
@@ -138,6 +133,62 @@ function preEmitInheritanceEdges(graph, scopes, nodeLookup) {
138
133
  }
139
134
  return handledSites;
140
135
  }
136
+ /**
137
+ * Emit language-inferred structural interface implementations before MRO and
138
+ * interface dispatch are built. Languages such as Go do not declare
139
+ * `implements` explicitly, so their resolver can infer defId-level interface
140
+ * satisfaction from parsed files and this bridge converts those defIds to
141
+ * graph node ids.
142
+ *
143
+ * Existing explicit IMPLEMENTS edges win: the local `existing` set prevents
144
+ * duplicate structural edges and keeps this hook language-neutral. The reason
145
+ * string carries the provider language (`go-structural-implements`) so callers
146
+ * can distinguish inferred edges from source-declared heritage.
147
+ */
148
+ function emitDetectedInterfaceImplementations(graph, parsedFiles, nodeLookup, provider, indexes, model) {
149
+ if (provider.detectInterfaceImplementations === undefined)
150
+ return 0;
151
+ const graphIdByDefId = new Map();
152
+ for (const parsed of parsedFiles) {
153
+ for (const def of parsed.localDefs) {
154
+ if (def.type !== 'Class' && def.type !== 'Struct' && def.type !== 'Interface')
155
+ continue;
156
+ const graphId = resolveDefGraphId(parsed.filePath, def, nodeLookup);
157
+ if (graphId !== undefined)
158
+ graphIdByDefId.set(def.nodeId, graphId);
159
+ }
160
+ }
161
+ const existing = new Set();
162
+ for (const rel of graph.iterRelationshipsByType('IMPLEMENTS')) {
163
+ existing.add(`${rel.sourceId}->${rel.targetId}`);
164
+ }
165
+ let emitted = 0;
166
+ const detected = provider.detectInterfaceImplementations(parsedFiles, indexes, model);
167
+ for (const [interfaceDefId, implementorDefIds] of detected) {
168
+ const targetId = graphIdByDefId.get(interfaceDefId);
169
+ if (targetId === undefined)
170
+ continue;
171
+ for (const implementorDefId of implementorDefIds) {
172
+ const sourceId = graphIdByDefId.get(implementorDefId);
173
+ if (sourceId === undefined)
174
+ continue;
175
+ const edgeKey = `${sourceId}->${targetId}`;
176
+ if (existing.has(edgeKey))
177
+ continue;
178
+ existing.add(edgeKey);
179
+ graph.addRelationship({
180
+ id: generateId('IMPLEMENTS', edgeKey),
181
+ sourceId,
182
+ targetId,
183
+ type: 'IMPLEMENTS',
184
+ confidence: 0.85,
185
+ reason: `${provider.language}-structural-implements`,
186
+ });
187
+ emitted++;
188
+ }
189
+ }
190
+ return emitted;
191
+ }
141
192
  export function runScopeResolution(input, provider) {
142
193
  const { graph, files } = input;
143
194
  const onWarn = input.onWarn ?? (() => { });
@@ -251,6 +302,7 @@ export function runScopeResolution(input, provider) {
251
302
  // `nodeLookup`). Without this rebuild, Property nodes added by the
252
303
  // heritage hook are invisible and ACCESSES edges silently fail to emit.
253
304
  const postHeritageNodeLookup = provider.emitHeritageEdges !== undefined ? buildGraphNodeLookup(graph) : nodeLookup;
305
+ emitDetectedInterfaceImplementations(graph, parsedFiles, postHeritageNodeLookup, provider, finalized, readonlyModel);
254
306
  const mroByClassDefId = provider.buildMro(graph, parsedFiles, postHeritageNodeLookup);
255
307
  const extendsOnlyMroByClassDefId = provider.buildExtendsOnlyMro?.(graph, parsedFiles, postHeritageNodeLookup);
256
308
  // Replace the empty MethodDispatchIndex that finalizeScopeModel
@@ -114,6 +114,12 @@ export declare function moduleScopeIdOf(startScope: ScopeId, scopes: ScopeResolu
114
114
  * Without (2) we'd miss every cross-file class-receiver call.
115
115
  */
116
116
  export declare function findClassBindingInScope(startScope: ScopeId, receiverName: string, scopes: ScopeResolutionIndexes): SymbolDefinition | undefined;
117
+ /**
118
+ * Resolve a class-like inheritance target using the shared inheritance
119
+ * resolution chain. Keeps pre-emitted heritage edges and language-specific
120
+ * consumers of `inherits` sites aligned.
121
+ */
122
+ export declare function resolveInheritanceBaseInScope(startScope: ScopeId, baseName: string, scopes: ScopeResolutionIndexes): SymbolDefinition | undefined;
117
123
  /**
118
124
  * Import/include-aware disambiguation for an *ambiguous* class-like base
119
125
  * name. Engages ONLY as a fallback after `findClassBindingInScope` has
@@ -286,6 +286,15 @@ export function findClassBindingInScope(startScope, receiverName, scopes) {
286
286
  }
287
287
  return undefined;
288
288
  }
289
+ /**
290
+ * Resolve a class-like inheritance target using the shared inheritance
291
+ * resolution chain. Keeps pre-emitted heritage edges and language-specific
292
+ * consumers of `inherits` sites aligned.
293
+ */
294
+ export function resolveInheritanceBaseInScope(startScope, baseName, scopes) {
295
+ return (findClassBindingInScope(startScope, baseName, scopes) ??
296
+ resolveAmbiguousInheritanceBaseViaImports(startScope, baseName, scopes));
297
+ }
289
298
  /**
290
299
  * Import/include-aware disambiguation for an *ambiguous* class-like base
291
300
  * name. Engages ONLY as a fallback after `findClassBindingInScope` has
@@ -891,6 +891,7 @@ export const GO_QUERIES = `
891
891
  ; Functions & Methods
892
892
  (function_declaration name: (identifier) @name) @definition.function
893
893
  (method_declaration name: (field_identifier) @name) @definition.method
894
+ (method_elem name: (field_identifier) @name) @definition.method
894
895
 
895
896
  ; Types
896
897
  (type_declaration (type_spec name: (type_identifier) @name type: (struct_type))) @definition.struct
@@ -148,6 +148,9 @@ export const CLASS_CONTAINER_TYPES = new Set([
148
148
  // Kotlin
149
149
  'object_declaration',
150
150
  'companion_object',
151
+ // Go
152
+ 'struct_type',
153
+ 'interface_type',
151
154
  ]);
152
155
  export const CONTAINER_TYPE_TO_LABEL = {
153
156
  class_declaration: 'Class',
@@ -179,6 +182,8 @@ export const CONTAINER_TYPE_TO_LABEL = {
179
182
  singleton_class: 'Class', // Ruby: class << self inherits enclosing class name
180
183
  object_declaration: 'Class',
181
184
  companion_object: 'Class',
185
+ struct_type: 'Struct',
186
+ interface_type: 'Interface',
182
187
  };
183
188
  /**
184
189
  * Pre-order walk over a node and all its named descendants, invoking `cb` on
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.6-rc.115",
3
+ "version": "1.6.6-rc.116",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",