diff --git a/gitnexus/src/core/ingestion/languages/go/captures.ts b/gitnexus/src/core/ingestion/languages/go/captures.ts index 1946b58ff..93fcbc51a 100644 --- a/gitnexus/src/core/ingestion/languages/go/captures.ts +++ b/gitnexus/src/core/ingestion/languages/go/captures.ts @@ -1,5 +1,10 @@ import type { Capture, CaptureMatch } from 'gitnexus-shared'; -import { findNodeAtRange, nodeToCapture, syntheticCapture } from '../../utils/ast-helpers.js'; +import { + findNodeAtRange, + nodeToCapture, + syntheticCapture, + type SyntaxNode, +} from '../../utils/ast-helpers.js'; import { getGoParser, getGoScopeQuery } from './query.js'; import { recordGoCacheHit, recordGoCacheMiss } from './cache-stats.js'; import { computeGoCallArity, computeGoDeclarationArity } from './arity-metadata.js'; @@ -8,37 +13,6 @@ import { synthesizeGoReceiverBinding } from './receiver-binding.js'; import { synthesizeGoTypeBindings } from './type-binding.js'; import { getTreeSitterBufferSize } from '../../constants.js'; -/** Go builtin types that must not be qualified with a package prefix. */ -const GO_BUILTIN_TYPES = new Set([ - 'bool', - 'byte', - 'comparable', - 'complex128', - 'complex64', - 'error', - 'float32', - 'float64', - 'int', - 'int16', - 'int32', - 'int64', - 'int8', - 'rune', - 'string', - 'uint', - 'uint16', - 'uint32', - 'uint64', - 'uint8', - 'uintptr', - 'any', -]); - -function inferPackageName(sourceText: string): string | null { - const match = sourceText.match(/^\s*package\s+([A-Za-z_][A-Za-z0-9_]*)/m); - return match?.[1] ?? null; -} - export function emitGoScopeCaptures( sourceText: string, _filePath: string, @@ -56,7 +30,6 @@ export function emitGoScopeCaptures( const rawMatches = getGoScopeQuery().matches(tree.rootNode); const out: CaptureMatch[] = []; - const pkgName = inferPackageName(sourceText); for (const m of rawMatches) { const grouped: Record = {}; @@ -89,6 +62,8 @@ export function emitGoScopeCaptures( } } + if (isRawMultiAssignTypeBinding(tree.rootNode, grouped)) continue; + const declAnchor = grouped['@declaration.function'] ?? grouped['@declaration.method']; if (declAnchor !== undefined) { const fnNode = @@ -167,38 +142,27 @@ export function emitGoScopeCaptures( }); } - // Qualify same-package return-type captures: strip wrapper types - // (`*`, `[]`, etc.) first, then prepend `pkg.` so the rawName - // matches the qualified name we stamped on declarations. - // Cross-package types (`*models.User`) already carry a dot and are - // left as-is. - if (pkgName !== null) { - for (let i = 0; i < out.length; i++) { - const match = out[i]!; - if (match['@type-binding.return'] === undefined) continue; - const typeCap = match['@type-binding.type']; - if (typeCap === undefined || typeCap.text.includes('.')) continue; - let raw = typeCap.text.trim(); - while (raw.startsWith('*')) raw = raw.slice(1).trim(); - if (raw.startsWith('[]')) raw = raw.slice(2).trim(); - // Strip chan prefix so element type gets qualified: - // chan Event → pkg.Event (correct for cross-receiver dispatch) - // chan int → excluded by GO_BUILTIN_TYPES below (no phantom pkg.int) - if (raw.startsWith('chan ')) raw = raw.slice(5).trim(); - if (raw.includes('.') || raw.startsWith('func(') || raw.startsWith('map[')) continue; - if (GO_BUILTIN_TYPES.has(raw)) continue; - const idx = raw.indexOf('['); - if (idx !== -1) raw = raw.slice(0, idx); - out[i] = { - ...match, - '@type-binding.type': { - name: '@type-binding.type', - text: pkgName + '.' + raw, - range: { ...typeCap.range }, - }, - }; - } - } - return out; } + +function isRawMultiAssignTypeBinding( + rootNode: SyntaxNode, + grouped: Record, +): boolean { + const anchor = + grouped['@type-binding.constructor'] ?? + grouped['@type-binding.call-return'] ?? + grouped['@type-binding.assertion']; + if (anchor === undefined) return false; + + const node = findNodeAtRange(rootNode, anchor.range, 'short_var_declaration'); + if (node === null) return false; + const lhs = node.childForFieldName('left'); + const rhs = node.childForFieldName('right'); + if (lhs === null) return false; + if (rhs === null) return false; + return ( + lhs.namedChildren.filter((c) => c.type === 'identifier').length >= 2 && + rhs.namedChildren.length >= 2 + ); +} diff --git a/gitnexus/src/core/ingestion/languages/go/interpret.ts b/gitnexus/src/core/ingestion/languages/go/interpret.ts index d27cbdf8c..c0bd71f48 100644 --- a/gitnexus/src/core/ingestion/languages/go/interpret.ts +++ b/gitnexus/src/core/ingestion/languages/go/interpret.ts @@ -10,7 +10,7 @@ export function interpretGoImport(captures: CaptureMatch): ParsedImport | null { if (kind === 'dot') return { kind: 'wildcard', targetRaw: source }; if (kind === 'alias') { if (alias === undefined || name === undefined) return null; - return { kind: 'alias', localName: alias, importedName: name, alias, targetRaw: source }; + return { kind: 'namespace', localName: alias, importedName: name, targetRaw: source }; } if (kind === 'namespace') { if (name === undefined) return null; diff --git a/gitnexus/src/core/ingestion/languages/go/method-owners.ts b/gitnexus/src/core/ingestion/languages/go/method-owners.ts index 1754b289f..457f9f60a 100644 --- a/gitnexus/src/core/ingestion/languages/go/method-owners.ts +++ b/gitnexus/src/core/ingestion/languages/go/method-owners.ts @@ -19,16 +19,38 @@ export function populateGoOwners(parsed: ParsedFile): void { // struct/interface declarations, this handles struct field ownership. populateClassOwnedMembers(parsed); - const moduleScope = parsed.scopes.find((s) => s.kind === 'Module'); - if (moduleScope === undefined) return; + populateGoOwnersInPackage([parsed]); +} +export function populateGoWorkspaceOwners( + parsedFiles: readonly ParsedFile[], + ctx: { readonly fileContents: ReadonlyMap }, +): void { + const filesByPackage = new Map(); + for (const parsed of parsedFiles) { + const pkgName = inferPackageName(ctx.fileContents.get(parsed.filePath) ?? ''); + if (pkgName === null) continue; + const key = `${packageDir(parsed.filePath)}\0${pkgName}`; + const bucket = filesByPackage.get(key) ?? []; + bucket.push(parsed); + filesByPackage.set(key, bucket); + } + + for (const bucket of filesByPackage.values()) { + populateGoOwnersInPackage(bucket); + } +} + +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(); // qname → nodeId - for (const scope of parsed.scopes) { - for (const def of scope.ownedDefs) { - if (isClassLike(def.type) && def.qualifiedName) { - structByQualifiedName.set(def.qualifiedName, def.nodeId); + 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); + } } } } @@ -37,37 +59,50 @@ export function populateGoOwners(parsed: ParsedFile): void { // scope whose typeBindings carry the self entry (kept there by // goBindingScopeFor). Match the self rawName against struct defs. if (structByQualifiedName.size > 0) { - for (const scope of parsed.scopes) { - if (scope.kind !== 'Function') continue; - const methodDefs = scope.ownedDefs.filter( - (d) => d.type === 'Method' && d.ownerId === undefined, - ); - if (methodDefs.length === 0) continue; + for (const parsed of parsedFiles) { + for (const scope of parsed.scopes) { + if (scope.kind !== 'Function') continue; + const methodDefs = scope.ownedDefs.filter( + (d) => d.type === 'Method' && d.ownerId === undefined, + ); + if (methodDefs.length === 0) continue; - // Find the self typeBinding in this Function scope. - let receiverType: string | undefined; - for (const [, tb] of scope.typeBindings) { - if (tb.source === 'self') { - receiverType = tb.rawName; - break; - } - } - if (receiverType === undefined) continue; - - let ownerId = structByQualifiedName.get(receiverType); - if (ownerId === undefined) { - for (const [qname, nodeId] of structByQualifiedName) { - if (qname.endsWith('.' + receiverType)) { - ownerId = nodeId; + // Find the self typeBinding in this Function scope. + let receiverType: string | undefined; + for (const [, tb] of scope.typeBindings) { + if (tb.source === 'self') { + receiverType = tb.rawName; break; } } - } - if (ownerId !== undefined) { - for (const def of methodDefs) { - (def as { ownerId?: string }).ownerId = ownerId; + if (receiverType === undefined) continue; + + let ownerId = structByQualifiedName.get(receiverType); + if (ownerId === undefined) { + for (const [qname, nodeId] of structByQualifiedName) { + if (qname.endsWith('.' + receiverType)) { + ownerId = nodeId; + break; + } + } + } + if (ownerId !== undefined) { + for (const def of methodDefs) { + (def as { ownerId?: string }).ownerId = ownerId; + } } } } } } + +function inferPackageName(sourceText: string): string | null { + const match = sourceText.match(/^\s*package\s+([A-Za-z_][A-Za-z0-9_]*)/m); + return match?.[1] ?? null; +} + +function packageDir(filePath: string): string { + const normalized = filePath.replace(/\\/g, '/'); + const idx = normalized.lastIndexOf('/'); + return idx === -1 ? '' : normalized.slice(0, idx); +} diff --git a/gitnexus/src/core/ingestion/languages/go/query.ts b/gitnexus/src/core/ingestion/languages/go/query.ts index 1e728f925..21aae19ad 100644 --- a/gitnexus/src/core/ingestion/languages/go/query.ts +++ b/gitnexus/src/core/ingestion/languages/go/query.ts @@ -59,7 +59,6 @@ const GO_SCOPE_QUERY = ` left: (expression_list (identifier) @declaration.name)) @declaration.variable ;; Imports -(import_declaration) @import.statement (import_spec) @import.statement ;; Type bindings — parameter annotations diff --git a/gitnexus/src/core/ingestion/languages/go/scope-resolver.ts b/gitnexus/src/core/ingestion/languages/go/scope-resolver.ts index 464fa6e24..32e834c80 100644 --- a/gitnexus/src/core/ingestion/languages/go/scope-resolver.ts +++ b/gitnexus/src/core/ingestion/languages/go/scope-resolver.ts @@ -1,7 +1,7 @@ import type { ParsedFile } from 'gitnexus-shared'; import { SupportedLanguages } from 'gitnexus-shared'; import { buildMro, defaultLinearize } from '../../scope-resolution/passes/mro.js'; -import { populateGoOwners } from './method-owners.js'; +import { populateGoOwners, populateGoWorkspaceOwners } from './method-owners.js'; import type { ScopeResolver } from '../../scope-resolution/contract/scope-resolver.js'; import { loadGoModulePath } from '../../language-config.js'; import { goProvider } from '../go.js'; @@ -36,6 +36,7 @@ export const goScopeResolver: ScopeResolver = { buildMro(graph, parsedFiles, nodeLookup, defaultLinearize), populateOwners: (parsed: ParsedFile) => populateGoOwners(parsed), + populateWorkspaceOwners: (parsedFiles, ctx) => populateGoWorkspaceOwners(parsedFiles, ctx), isSuperReceiver: () => false, diff --git a/gitnexus/src/core/ingestion/languages/go/type-binding.ts b/gitnexus/src/core/ingestion/languages/go/type-binding.ts index b66bed8a8..60868b70b 100644 --- a/gitnexus/src/core/ingestion/languages/go/type-binding.ts +++ b/gitnexus/src/core/ingestion/languages/go/type-binding.ts @@ -11,18 +11,18 @@ export function synthesizeGoTypeBindings(rootNode: SyntaxNode): CaptureMatch[] { if (lhs === null) continue; // Multi-assignment: pair LHS identifiers positionally with RHS - // composite_literals or call_expressions. The tree-sitter query - // produces all combinations; these correct pairings overwrite them. + // expressions. The tree-sitter query produces all LHS/RHS + // combinations; emit only the positions whose RHS carries an + // inferable type. const lhsIds = lhs.namedChildren.filter((c) => c.type === 'identifier'); - const rhsExprs = right.namedChildren.filter( - (c) => c.type === 'composite_literal' || c.type === 'call_expression', - ); + const rhsExprs = right.namedChildren; if (lhsIds.length >= 2 && rhsExprs.length >= 2) { for (let i = 0; i < Math.min(lhsIds.length, rhsExprs.length); i++) { const lhsId = lhsIds[i]!; const rhsExpr = rhsExprs[i]!; const typeNode = extractTypeNode(rhsExpr); - const typeName = typeNode ? extractSimpleTypeNameText(typeNode) : rhsExpr.text; + if (typeNode === null) continue; + const typeName = extractSimpleTypeNameText(typeNode); out.push({ '@type-binding.multi-assign': syntheticCapture( '@type-binding.multi-assign', @@ -239,12 +239,46 @@ function extractTypeNode(expr: SyntaxNode): SyntaxNode | null { null ); } + if (expr.type === 'unary_expression') { + const operand = expr.childForFieldName('operand'); + return operand === null ? null : extractTypeNode(operand); + } if (expr.type === 'call_expression') { const fn = expr.childForFieldName('function'); + if (fn?.type === 'identifier' && fn.text === 'new') { + const args = expr.childForFieldName('arguments'); + return ( + args?.namedChildren.find((c) => + ['type_identifier', 'qualified_type', 'pointer_type'].includes(c.type), + ) ?? null + ); + } + if (fn?.type === 'identifier' && fn.text === 'make') { + const args = expr.childForFieldName('arguments'); + const container = args?.namedChildren.find((c) => + ['slice_type', 'map_type'].includes(c.type), + ); + if (container?.type === 'slice_type') { + return ( + container.namedChildren.find((c) => + ['type_identifier', 'qualified_type'].includes(c.type), + ) ?? null + ); + } + if (container?.type === 'map_type') { + const typeChildren = container.namedChildren.filter((c) => + ['type_identifier', 'qualified_type'].includes(c.type), + ); + return typeChildren[1] ?? typeChildren[0] ?? null; + } + } if (fn?.type === 'identifier') return fn; if (fn?.type === 'selector_expression') { return fn.childForFieldName('field') ?? fn; } } + if (expr.type === 'type_assertion_expression') { + return expr.childForFieldName('type'); + } return null; } diff --git a/gitnexus/src/core/ingestion/scope-resolution/contract/scope-resolver.ts b/gitnexus/src/core/ingestion/scope-resolution/contract/scope-resolver.ts index f7c034f9d..2dfff58c9 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/contract/scope-resolver.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/contract/scope-resolver.ts @@ -395,6 +395,18 @@ export interface ScopeResolver { */ populateOwners(parsed: ParsedFile): void; + /** + * Optional workspace-wide ownership reconciliation for languages whose + * member owner can be declared in a different file from the owner type. + * Runs after every file has had `populateOwners(parsed)` applied, but + * still before `reconcileOwnership`, so stamped ownerIds are copied into + * the semantic model registries. + */ + readonly populateWorkspaceOwners?: ( + parsedFiles: readonly ParsedFile[], + ctx: { readonly fileContents: ReadonlyMap }, + ) => void; + /** * Recognize a `super(...)`-style receiver text. Python returns * `/^super\s*\(/.test(t)`. Java returns `t === 'super'`. C++ may diff --git a/gitnexus/src/core/ingestion/scope-resolution/pipeline/run.ts b/gitnexus/src/core/ingestion/scope-resolution/pipeline/run.ts index 4edde9638..45b783049 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/pipeline/run.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/pipeline/run.ts @@ -90,6 +90,14 @@ export function runScopeResolution( const onWarn = input.onWarn ?? (() => {}); const PROF = process.env.PROF_SCOPE_RESOLUTION === '1'; const tStart = PROF ? process.hrtime.bigint() : 0n; + let fileContents: Map | undefined; + const getFileContents = (): Map => { + if (fileContents === undefined) { + fileContents = new Map(); + for (const f of files) fileContents.set(f.path, f.content); + } + return fileContents; + }; // ── Phase 1: extract each file → ParsedFile ──────────────────────────── const parsedFiles: ParsedFile[] = []; @@ -111,6 +119,7 @@ export function runScopeResolution( provider.populateOwners(parsed); parsedFiles.push(parsed); } + provider.populateWorkspaceOwners?.(parsedFiles, { fileContents: getFileContents() }); // Reconcile scope-resolution's ownership view into the SemanticModel. // See `reconcile-ownership.ts` for the full rationale (Contract @@ -179,15 +188,6 @@ export function runScopeResolution( // class bindings when chasing return-type chains across files. // The hook writes to `bindingAugmentations` only; finalized // `indexes.bindings` remains immutable post-finalize (I8). - let fileContents: Map | undefined; - const getFileContents = (): Map => { - if (fileContents === undefined) { - fileContents = new Map(); - for (const f of files) fileContents.set(f.path, f.content); - } - return fileContents; - }; - if (provider.populateNamespaceSiblings !== undefined) { provider.populateNamespaceSiblings(parsedFiles, indexes, { fileContents: getFileContents(), diff --git a/gitnexus/test/fixtures/lang-resolution/go-aliased-package-import/go.mod b/gitnexus/test/fixtures/lang-resolution/go-aliased-package-import/go.mod new file mode 100644 index 000000000..02d98452c --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/go-aliased-package-import/go.mod @@ -0,0 +1,3 @@ +module example.com/aliasimport + +go 1.21 diff --git a/gitnexus/test/fixtures/lang-resolution/go-aliased-package-import/internal/util/log.go b/gitnexus/test/fixtures/lang-resolution/go-aliased-package-import/internal/util/log.go new file mode 100644 index 000000000..c9920227e --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/go-aliased-package-import/internal/util/log.go @@ -0,0 +1,3 @@ +package util + +func Log() {} diff --git a/gitnexus/test/fixtures/lang-resolution/go-aliased-package-import/main.go b/gitnexus/test/fixtures/lang-resolution/go-aliased-package-import/main.go new file mode 100644 index 000000000..d3a46469d --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/go-aliased-package-import/main.go @@ -0,0 +1,7 @@ +package main + +import util "example.com/aliasimport/internal/util" + +func main() { + util.Log() +} diff --git a/gitnexus/test/fixtures/lang-resolution/go-same-package-factory/go.mod b/gitnexus/test/fixtures/lang-resolution/go-same-package-factory/go.mod new file mode 100644 index 000000000..70b6fc53d --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/go-same-package-factory/go.mod @@ -0,0 +1,3 @@ +module example.com/samefactory + +go 1.21 diff --git a/gitnexus/test/fixtures/lang-resolution/go-same-package-factory/main.go b/gitnexus/test/fixtures/lang-resolution/go-same-package-factory/main.go new file mode 100644 index 000000000..d3878c76e --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/go-same-package-factory/main.go @@ -0,0 +1,10 @@ +package main + +func NewUser() *User { + return &User{} +} + +func processUser() { + user := NewUser() + user.Save() +} diff --git a/gitnexus/test/fixtures/lang-resolution/go-same-package-factory/repo.go b/gitnexus/test/fixtures/lang-resolution/go-same-package-factory/repo.go new file mode 100644 index 000000000..e760c880d --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/go-same-package-factory/repo.go @@ -0,0 +1,7 @@ +package main + +type Repo struct{} + +func (r *Repo) Save() bool { + return true +} diff --git a/gitnexus/test/fixtures/lang-resolution/go-same-package-factory/user.go b/gitnexus/test/fixtures/lang-resolution/go-same-package-factory/user.go new file mode 100644 index 000000000..10a724c78 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/go-same-package-factory/user.go @@ -0,0 +1,7 @@ +package main + +type User struct{} + +func (u *User) Save() bool { + return true +} diff --git a/gitnexus/test/fixtures/lang-resolution/go-split-method-owner/go.mod b/gitnexus/test/fixtures/lang-resolution/go-split-method-owner/go.mod new file mode 100644 index 000000000..d7c722bd7 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/go-split-method-owner/go.mod @@ -0,0 +1,3 @@ +module example.com/splitowner + +go 1.21 diff --git a/gitnexus/test/fixtures/lang-resolution/go-split-method-owner/main.go b/gitnexus/test/fixtures/lang-resolution/go-split-method-owner/main.go new file mode 100644 index 000000000..c96ec3516 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/go-split-method-owner/main.go @@ -0,0 +1,6 @@ +package main + +func process() { + user := User{} + user.Save() +} diff --git a/gitnexus/test/fixtures/lang-resolution/go-split-method-owner/repo.go b/gitnexus/test/fixtures/lang-resolution/go-split-method-owner/repo.go new file mode 100644 index 000000000..e760c880d --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/go-split-method-owner/repo.go @@ -0,0 +1,7 @@ +package main + +type Repo struct{} + +func (r *Repo) Save() bool { + return true +} diff --git a/gitnexus/test/fixtures/lang-resolution/go-split-method-owner/save.go b/gitnexus/test/fixtures/lang-resolution/go-split-method-owner/save.go new file mode 100644 index 000000000..c6f550ef8 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/go-split-method-owner/save.go @@ -0,0 +1,5 @@ +package main + +func (u *User) Save() bool { + return true +} diff --git a/gitnexus/test/fixtures/lang-resolution/go-split-method-owner/user.go b/gitnexus/test/fixtures/lang-resolution/go-split-method-owner/user.go new file mode 100644 index 000000000..150e04c86 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/go-split-method-owner/user.go @@ -0,0 +1,3 @@ +package main + +type User struct{} diff --git a/gitnexus/test/integration/resolvers/go.test.ts b/gitnexus/test/integration/resolvers/go.test.ts index f9a57d660..28afbb3f0 100644 --- a/gitnexus/test/integration/resolvers/go.test.ts +++ b/gitnexus/test/integration/resolvers/go.test.ts @@ -610,6 +610,30 @@ describe('Go return type inference via explicit function return type', () => { }); }); +describe('Go same-package factory return type inference', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo(path.join(FIXTURES, 'go-same-package-factory'), () => {}); + }, 60000); + + it('resolves user.Save() through same-package NewUser() return type', () => { + const calls = getRelationships(result, 'CALLS'); + const userSave = calls.find( + (c) => c.target === 'Save' && c.source === 'processUser' && c.targetFilePath === 'user.go', + ); + expect(userSave).toBeDefined(); + }); + + it('does not resolve user.Save() to Repo.Save', () => { + const calls = getRelationships(result, 'CALLS'); + const repoSave = calls.find( + (c) => c.target === 'Save' && c.source === 'processUser' && c.targetFilePath === 'repo.go', + ); + expect(repoSave).toBeUndefined(); + }); +}); + // --------------------------------------------------------------------------- // Go multi-return factory inference: user, err := NewUser("alice"); user.Save() // --------------------------------------------------------------------------- @@ -1261,6 +1285,47 @@ describe('Go cross-file binding propagation', () => { }); }); +describe('Go aliased package selector resolution', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo(path.join(FIXTURES, 'go-aliased-package-import'), () => {}); + }, 60000); + + it('resolves util.Log() through an aliased package import', () => { + const calls = getRelationships(result, 'CALLS'); + const logCall = calls.find( + (c) => + c.target === 'Log' && c.source === 'main' && c.targetFilePath === 'internal/util/log.go', + ); + expect(logCall).toBeDefined(); + }); +}); + +describe('Go method owner resolution across package files', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo(path.join(FIXTURES, 'go-split-method-owner'), () => {}); + }, 60000); + + it('resolves user.Save() to the method whose receiver type is declared in another package file', () => { + const calls = getRelationships(result, 'CALLS'); + const userSave = calls.find( + (c) => c.target === 'Save' && c.source === 'process' && c.targetFilePath === 'save.go', + ); + expect(userSave).toBeDefined(); + }); + + it('does not resolve user.Save() to Repo.Save', () => { + const calls = getRelationships(result, 'CALLS'); + const repoSave = calls.find( + (c) => c.target === 'Save' && c.source === 'process' && c.targetFilePath === 'repo.go', + ); + expect(repoSave).toBeUndefined(); + }); +}); + // --------------------------------------------------------------------------- // Go cmd/ helper files should NOT get entry-point multiplier (P0-1 fix) // Only main.go files should get the 3.0 entry-point boost, not arbitrary diff --git a/gitnexus/test/unit/scope-resolution/go/go-captures-smoke.test.ts b/gitnexus/test/unit/scope-resolution/go/go-captures-smoke.test.ts index a7e97b4b6..b921bddc4 100644 --- a/gitnexus/test/unit/scope-resolution/go/go-captures-smoke.test.ts +++ b/gitnexus/test/unit/scope-resolution/go/go-captures-smoke.test.ts @@ -5,6 +5,23 @@ const tagNames = (matches: readonly Record[]) => matches.flatMap((m) => Object.keys(m)); describe('Go scope captures — smoke', () => { + it('emits grouped imports once per import spec', () => { + const src = ` +package main + +import ( + "fmt" + "os" +) +`; + const matches = emitGoScopeCaptures(src, 'main.go'); + const imports = matches + .filter((m) => m['@import.source'] !== undefined) + .map((m) => m['@import.source']!.text); + + expect(imports).toEqual(['fmt', 'os']); + }); + it('emits module, struct, interface, function, method, import, call, read, write captures', () => { const src = ` package main diff --git a/gitnexus/test/unit/scope-resolution/go/go-imports.test.ts b/gitnexus/test/unit/scope-resolution/go/go-imports.test.ts index e96d73ee7..1e1e715b3 100644 --- a/gitnexus/test/unit/scope-resolution/go/go-imports.test.ts +++ b/gitnexus/test/unit/scope-resolution/go/go-imports.test.ts @@ -82,10 +82,9 @@ describe('Go import interpretation', () => { '@import.source': capt('@import.source', 'example.com/pkg/util'), }); expect(result).toEqual({ - kind: 'alias', + kind: 'namespace', localName: 'util', importedName: 'util', - alias: 'util', targetRaw: 'example.com/pkg/util', }); }); diff --git a/gitnexus/test/unit/scope-resolution/go/go-type-binding.test.ts b/gitnexus/test/unit/scope-resolution/go/go-type-binding.test.ts index ceb876342..9ea213128 100644 --- a/gitnexus/test/unit/scope-resolution/go/go-type-binding.test.ts +++ b/gitnexus/test/unit/scope-resolution/go/go-type-binding.test.ts @@ -67,6 +67,19 @@ describe('Go type binding synthesis — 7 patterns', () => { expect(qMatch).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') + .filter((m) => m['@type-binding.name'] !== undefined) + .map((m) => ({ + name: m['@type-binding.name']!.text, + type: m['@type-binding.type']!.text, + })); + + expect(bindings).toContainEqual({ name: 'b', type: 'X' }); + expect(bindings).not.toContainEqual({ name: 'a', type: 'X' }); + }); + it('interprets assertion type binding', () => { const result = interpretGoTypeBinding({ '@type-binding.assertion': {