From 6465b8d34c7241025df117746cb8e154f3a79897 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Sun, 15 Mar 2026 14:17:58 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20review=20findings=20=E2=80=94=20receiver?= =?UTF-8?q?ClassName=20parity,=20Rust=20wrappers,=20Go=20multi-return,=20K?= =?UTF-8?q?otlin/Swift=20qualified=20calls?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P1: Sequential path now includes receiverClassName narrowing for PHP $this->method() disambiguation (was missing vs worker path). P2: Added Rc/Arc/Weak/MutexGuard/Cow + 6 more Rust Deref types to WRAPPER_GENERICS (Box excluded — Java Swing collision). Extended Kotlin/Swift scanners to handle navigation_expression callees. Added Go multi-return support (user, err := f()) with blank/_/err/ok guard + AST-level first-return extraction in extractMethodSignature. P3: Extracted shared verifyConstructorBindings() eliminating 60 lines of duplication between sequential and worker paths. Added return-type inference integration tests for C++, Rust, Swift with competing methods and negative disambiguation assertions. --- gitnexus/src/core/ingestion/call-processor.ts | 124 ++++++++++-------- .../src/core/ingestion/type-extractors/go.ts | 24 +++- .../src/core/ingestion/type-extractors/jvm.ts | 19 ++- .../core/ingestion/type-extractors/swift.ts | 4 + gitnexus/src/core/ingestion/utils.ts | 16 ++- .../cpp-return-type-inference/app.cpp | 20 +++ .../cpp-return-type-inference/repo.h | 9 ++ .../cpp-return-type-inference/user.h | 9 ++ .../go-multi-return-inference/cmd/main.go | 24 ++++ .../go-multi-return-inference/go.mod | 3 + .../go-multi-return-inference/models/repo.go | 9 ++ .../go-multi-return-inference/models/user.go | 9 ++ .../go-return-type-inference/cmd/main.go | 9 ++ .../go-return-type-inference/models/repo.go | 13 ++ .../rust-return-type-inference/src/main.rs | 21 +++ .../rust-return-type-inference/src/models.rs | 19 +++ .../swift-return-type-inference/App.swift | 17 +++ .../swift-return-type-inference/Models.swift | 11 ++ .../test/integration/resolvers/cpp.test.ts | 43 ++++++ .../test/integration/resolvers/go.test.ts | 94 ++++++++++--- .../test/integration/resolvers/rust.test.ts | 43 ++++++ .../test/integration/resolvers/swift.test.ts | 43 ++++++ gitnexus/test/unit/call-processor.test.ts | 28 ++++ gitnexus/test/unit/method-signature.test.ts | 2 +- 24 files changed, 537 insertions(+), 76 deletions(-) create mode 100644 gitnexus/test/fixtures/lang-resolution/cpp-return-type-inference/app.cpp create mode 100644 gitnexus/test/fixtures/lang-resolution/cpp-return-type-inference/repo.h create mode 100644 gitnexus/test/fixtures/lang-resolution/cpp-return-type-inference/user.h create mode 100644 gitnexus/test/fixtures/lang-resolution/go-multi-return-inference/cmd/main.go create mode 100644 gitnexus/test/fixtures/lang-resolution/go-multi-return-inference/go.mod create mode 100644 gitnexus/test/fixtures/lang-resolution/go-multi-return-inference/models/repo.go create mode 100644 gitnexus/test/fixtures/lang-resolution/go-multi-return-inference/models/user.go create mode 100644 gitnexus/test/fixtures/lang-resolution/go-return-type-inference/models/repo.go create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-return-type-inference/src/main.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-return-type-inference/src/models.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/swift-return-type-inference/App.swift create mode 100644 gitnexus/test/fixtures/lang-resolution/swift-return-type-inference/Models.swift diff --git a/gitnexus/src/core/ingestion/call-processor.ts b/gitnexus/src/core/ingestion/call-processor.ts index 4886f1fc8..204728e97 100644 --- a/gitnexus/src/core/ingestion/call-processor.ts +++ b/gitnexus/src/core/ingestion/call-processor.ts @@ -20,6 +20,7 @@ import { findEnclosingClassId, } from './utils.js'; import { buildTypeEnv } from './type-env.js'; +import type { ConstructorBinding } from './type-env.js'; import { getTreeSitterBufferSize } from './constants.js'; import type { ExtractedCall, ExtractedHeritage, ExtractedRoute, FileConstructorBindings } from './workers/parse-worker.js'; import { callRouters } from './call-routing.js'; @@ -54,6 +55,65 @@ const findEnclosingFunction = ( return null; }; +/** + * Verify constructor bindings against SymbolTable and infer receiver types. + * Shared between sequential (processCalls) and worker (processCallsFromExtracted) paths. + */ +const verifyConstructorBindings = ( + bindings: readonly ConstructorBinding[], + filePath: string, + ctx: ResolutionContext, + graph?: KnowledgeGraph, +): Map => { + const verified = new Map(); + + for (const { scope, varName, calleeName, receiverClassName } of bindings) { + const tiered = ctx.resolve(calleeName, filePath); + const isClass = tiered?.candidates.some(def => def.type === 'Class') ?? false; + + if (isClass) { + verified.set(receiverKey(extractFuncNameFromScope(scope), varName), calleeName); + } else { + let callableDefs = tiered?.candidates.filter(d => + d.type === 'Function' || d.type === 'Method' + ); + + // When receiver class is known (e.g. $this->method() in PHP), narrow + // candidates to methods owned by that class to avoid false disambiguation failures. + if (callableDefs && callableDefs.length > 1 && receiverClassName) { + if (graph) { + // Worker path: use graph.getNode (fast, already in-memory) + const narrowed = callableDefs.filter(d => { + if (!d.ownerId) return false; + const owner = graph.getNode(d.ownerId); + return owner?.properties.name === receiverClassName; + }); + if (narrowed.length > 0) callableDefs = narrowed; + } else { + // Sequential path: use ctx.resolve (no graph available) + const classResolved = ctx.resolve(receiverClassName, filePath); + if (classResolved && classResolved.candidates.length > 0) { + const classNodeIds = new Set(classResolved.candidates.map(c => c.nodeId)); + const narrowed = callableDefs.filter(d => + d.ownerId && classNodeIds.has(d.ownerId) + ); + if (narrowed.length > 0) callableDefs = narrowed; + } + } + } + + if (callableDefs && callableDefs.length === 1 && callableDefs[0].returnType) { + const typeName = extractReturnTypeName(callableDefs[0].returnType); + if (typeName) { + verified.set(receiverKey(extractFuncNameFromScope(scope), varName), typeName); + } + } + } + } + + return verified; +}; + export const processCalls = async ( graph: KnowledgeGraph, files: { path: string; content: string }[], @@ -110,29 +170,9 @@ export const processCalls = async ( const typeEnv = lang ? buildTypeEnv(tree, lang, ctx.symbols) : null; const callRouter = callRouters[language]; - // Verify constructor bindings against SymbolTable for return type inference. - // In the worker path, this happens in processCallsFromExtracted. In the - // sequential path, we must do it here before resolving calls. - const verifiedReceivers = new Map(); - if (typeEnv && typeEnv.constructorBindings.length > 0) { - for (const { scope, varName, calleeName } of typeEnv.constructorBindings) { - const tiered = ctx.resolve(calleeName, file.path); - const isClass = tiered?.candidates.some(def => def.type === 'Class') ?? false; - if (isClass) { - verifiedReceivers.set(receiverKey(extractFuncNameFromScope(scope), varName), calleeName); - } else { - const callableDefs = tiered?.candidates.filter(d => - d.type === 'Function' || d.type === 'Method' - ); - if (callableDefs && callableDefs.length === 1 && callableDefs[0].returnType) { - const typeName = extractReturnTypeName(callableDefs[0].returnType); - if (typeName) { - verifiedReceivers.set(receiverKey(extractFuncNameFromScope(scope), varName), typeName); - } - } - } - } - } + const verifiedReceivers = typeEnv && typeEnv.constructorBindings.length > 0 + ? verifyConstructorBindings(typeEnv.constructorBindings, file.path, ctx) + : new Map(); ctx.enableCache(file.path); @@ -404,6 +444,11 @@ const WRAPPER_GENERICS = new Set([ 'Promise', 'Observable', 'Future', 'CompletableFuture', 'Task', 'ValueTask', // async wrappers 'Option', 'Some', 'Optional', 'Maybe', // nullable wrappers 'Result', 'Either', // result wrappers + // Rust smart pointers (Deref to inner type) + 'Rc', 'Arc', 'Weak', // pointer types + 'MutexGuard', 'RwLockReadGuard', 'RwLockWriteGuard', // guard types + 'Ref', 'RefMut', // RefCell guards + 'Cow', // copy-on-write // Containers (List, Array, Vec, Set, etc.) are intentionally excluded — // methods are called on the container, not the element type. // Non-wrapper generics return the base type (e.g., List) via the else branch. @@ -510,36 +555,9 @@ export const processCallsFromExtracted = async ( const fileReceiverTypes = new Map>(); if (constructorBindings) { for (const { filePath, bindings } of constructorBindings) { - for (const { scope, varName, calleeName, receiverClassName } of bindings) { - const tiered = ctx.resolve(calleeName, filePath); - const isClass = tiered?.candidates.some(def => def.type === 'Class') ?? false; - if (isClass) { - if (!fileReceiverTypes.has(filePath)) fileReceiverTypes.set(filePath, new Map()); - fileReceiverTypes.get(filePath)!.set(receiverKey(extractFuncNameFromScope(scope), varName), calleeName); - } else { - // Return type inference: if the callee is a function/method with a known - // return type, bind the variable to that return type. - let callableDefs = tiered?.candidates.filter(d => - d.type === 'Function' || d.type === 'Method' - ); - // When receiver class is known (e.g. $this->method() in PHP), narrow - // candidates to methods owned by that class to avoid false disambiguation failures. - if (callableDefs && callableDefs.length > 1 && receiverClassName) { - const narrowed = callableDefs.filter(d => { - if (!d.ownerId) return false; - const owner = graph.getNode(d.ownerId); - return owner?.properties.name === receiverClassName; - }); - if (narrowed.length > 0) callableDefs = narrowed; - } - if (callableDefs && callableDefs.length === 1 && callableDefs[0].returnType) { - const typeName = extractReturnTypeName(callableDefs[0].returnType); - if (typeName) { - if (!fileReceiverTypes.has(filePath)) fileReceiverTypes.set(filePath, new Map()); - fileReceiverTypes.get(filePath)!.set(receiverKey(extractFuncNameFromScope(scope), varName), typeName); - } - } - } + const verified = verifyConstructorBindings(bindings, filePath, ctx, graph); + if (verified.size > 0) { + fileReceiverTypes.set(filePath, verified); } } } diff --git a/gitnexus/src/core/ingestion/type-extractors/go.ts b/gitnexus/src/core/ingestion/type-extractors/go.ts index 0b3456ced..6a5c13d9b 100644 --- a/gitnexus/src/core/ingestion/type-extractors/go.ts +++ b/gitnexus/src/core/ingestion/type-extractors/go.ts @@ -147,10 +147,30 @@ const scanConstructorBinding: ConstructorBindingScanner = (node) => { const left = node.childForFieldName('left'); const right = node.childForFieldName('right'); if (!left || !right) return undefined; - // Single assignment only — skip multi-return like `user, err := GetUser()` const leftIds = left.type === 'expression_list' ? left.namedChildren : [left]; - if (leftIds.length !== 1 || leftIds[0].type !== 'identifier') return undefined; const rightExprs = right.type === 'expression_list' ? right.namedChildren : [right]; + + // Multi-return: user, err := NewUser() — bind first var when second is err/ok/_ + if (leftIds.length === 2 && rightExprs.length === 1) { + const secondVar = leftIds[1]; + const isErrorOrDiscard = + secondVar.text === '_' || + secondVar.text === 'err' || + secondVar.text === 'ok' || + secondVar.text === 'error'; + if (isErrorOrDiscard && leftIds[0].type === 'identifier') { + if (rightExprs[0].type !== 'call_expression') return undefined; + const func = rightExprs[0].childForFieldName('function'); + if (!func) return undefined; + if (func.text === 'new' || func.text === 'make') return undefined; + const calleeName = extractSimpleTypeName(func); + if (!calleeName) return undefined; + return { varName: leftIds[0].text, calleeName }; + } + } + + // Single assignment only + if (leftIds.length !== 1 || leftIds[0].type !== 'identifier') return undefined; if (rightExprs.length !== 1 || rightExprs[0].type !== 'call_expression') return undefined; const func = rightExprs[0].childForFieldName('function'); if (!func) return undefined; diff --git a/gitnexus/src/core/ingestion/type-extractors/jvm.ts b/gitnexus/src/core/ingestion/type-extractors/jvm.ts index 80d0b197e..05e9168be 100644 --- a/gitnexus/src/core/ingestion/type-extractors/jvm.ts +++ b/gitnexus/src/core/ingestion/type-extractors/jvm.ts @@ -194,10 +194,25 @@ const scanKotlinConstructorBinding: ConstructorBindingScanner = (node) => { const callExpr = node.namedChildren.find(c => c.type === 'call_expression'); if (!callExpr) return undefined; const callee = callExpr.firstNamedChild; - if (!callee || callee.type !== 'simple_identifier') return undefined; + if (!callee) return undefined; + + let calleeName: string | undefined; + if (callee.type === 'simple_identifier') { + calleeName = callee.text; + } else if (callee.type === 'navigation_expression') { + // Extract method name from qualified call: service.getUser() → getUser + const suffix = callee.lastNamedChild; + if (suffix?.type === 'navigation_suffix') { + const methodName = suffix.lastNamedChild; + if (methodName?.type === 'simple_identifier') { + calleeName = methodName.text; + } + } + } + if (!calleeName) return undefined; const nameNode = varDecl.namedChildren.find(c => c.type === 'simple_identifier'); if (!nameNode) return undefined; - return { varName: nameNode.text, calleeName: callee.text }; + return { varName: nameNode.text, calleeName }; }; export const kotlinTypeConfig: LanguageTypeConfig = { diff --git a/gitnexus/src/core/ingestion/type-extractors/swift.ts b/gitnexus/src/core/ingestion/type-extractors/swift.ts index 1cd870681..1ef2e6493 100644 --- a/gitnexus/src/core/ingestion/type-extractors/swift.ts +++ b/gitnexus/src/core/ingestion/type-extractors/swift.ts @@ -102,6 +102,10 @@ const scanConstructorBinding: ConstructorBindingScanner = (node) => { if (receiver?.type === 'simple_identifier' && suffix?.text === 'init') { return { varName, calleeName: receiver.text }; } + // General qualified call: service.getUser() → extract method name + if (suffix?.type === 'simple_identifier') { + return { varName, calleeName: suffix.text }; + } } return undefined; }; diff --git a/gitnexus/src/core/ingestion/utils.ts b/gitnexus/src/core/ingestion/utils.ts index cd6d89f03..1439dbee0 100644 --- a/gitnexus/src/core/ingestion/utils.ts +++ b/gitnexus/src/core/ingestion/utils.ts @@ -618,9 +618,19 @@ export const extractMethodSignature = (node: SyntaxNode | null | undefined): Met // Go: 'result' field is either a type_identifier or parameter_list (multi-return) const goResult = node.childForFieldName?.('result'); if (goResult) { - returnType = goResult.type === 'parameter_list' - ? goResult.text // multi-return: "(string, error)" - : goResult.text; // single return: "int" + if (goResult.type === 'parameter_list') { + // Multi-return: extract first parameter's type only (e.g. (*User, error) → *User) + const firstParam = goResult.firstNamedChild; + if (firstParam?.type === 'parameter_declaration') { + const typeNode = firstParam.childForFieldName('type'); + if (typeNode) returnType = typeNode.text; + } else if (firstParam) { + // Unnamed return types: (string, error) — first child is a bare type node + returnType = firstParam.text; + } + } else { + returnType = goResult.text; + } } // Rust: 'return_type' field — the value IS the type node (e.g. primitive_type, type_identifier). diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-return-type-inference/app.cpp b/gitnexus/test/fixtures/lang-resolution/cpp-return-type-inference/app.cpp new file mode 100644 index 000000000..60080a349 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-return-type-inference/app.cpp @@ -0,0 +1,20 @@ +#include "user.h" +#include "repo.h" + +User getUser(const char* name) { + return User(name); +} + +Repo getRepo(const char* name) { + return Repo(name); +} + +void processUser() { + auto user = getUser("alice"); + user.save(); +} + +void processRepo() { + auto repo = getRepo("main"); + repo.save(); +} diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-return-type-inference/repo.h b/gitnexus/test/fixtures/lang-resolution/cpp-return-type-inference/repo.h new file mode 100644 index 000000000..6331d23eb --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-return-type-inference/repo.h @@ -0,0 +1,9 @@ +#pragma once + +class Repo { +public: + Repo(const char* name) : name_(name) {} + bool save() { return true; } +private: + const char* name_; +}; diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-return-type-inference/user.h b/gitnexus/test/fixtures/lang-resolution/cpp-return-type-inference/user.h new file mode 100644 index 000000000..b1fb5928c --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-return-type-inference/user.h @@ -0,0 +1,9 @@ +#pragma once + +class User { +public: + User(const char* name) : name_(name) {} + bool save() { return true; } +private: + const char* name_; +}; diff --git a/gitnexus/test/fixtures/lang-resolution/go-multi-return-inference/cmd/main.go b/gitnexus/test/fixtures/lang-resolution/go-multi-return-inference/cmd/main.go new file mode 100644 index 000000000..5d88f68c9 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/go-multi-return-inference/cmd/main.go @@ -0,0 +1,24 @@ +package main + +import "example.com/multireturn/models" + +func NewUser(name string) (*models.User, error) { + return &models.User{Name: name}, nil +} + +func NewRepo(name string) (*models.Repo, error) { + return &models.Repo{Name: name}, nil +} + +func processUser() { + user, err := NewUser("alice") + if err != nil { + return + } + user.Save() +} + +func processRepo() { + repo, _ := NewRepo("main") + repo.Save() +} diff --git a/gitnexus/test/fixtures/lang-resolution/go-multi-return-inference/go.mod b/gitnexus/test/fixtures/lang-resolution/go-multi-return-inference/go.mod new file mode 100644 index 000000000..31de69be1 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/go-multi-return-inference/go.mod @@ -0,0 +1,3 @@ +module example.com/multireturn + +go 1.21 diff --git a/gitnexus/test/fixtures/lang-resolution/go-multi-return-inference/models/repo.go b/gitnexus/test/fixtures/lang-resolution/go-multi-return-inference/models/repo.go new file mode 100644 index 000000000..5abb2e7d8 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/go-multi-return-inference/models/repo.go @@ -0,0 +1,9 @@ +package models + +type Repo struct { + Name string +} + +func (r *Repo) Save() bool { + return true +} diff --git a/gitnexus/test/fixtures/lang-resolution/go-multi-return-inference/models/user.go b/gitnexus/test/fixtures/lang-resolution/go-multi-return-inference/models/user.go new file mode 100644 index 000000000..0e78a30a8 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/go-multi-return-inference/models/user.go @@ -0,0 +1,9 @@ +package models + +type User struct { + Name string +} + +func (u *User) Save() bool { + return true +} diff --git a/gitnexus/test/fixtures/lang-resolution/go-return-type-inference/cmd/main.go b/gitnexus/test/fixtures/lang-resolution/go-return-type-inference/cmd/main.go index b8c8c3819..fda782be1 100644 --- a/gitnexus/test/fixtures/lang-resolution/go-return-type-inference/cmd/main.go +++ b/gitnexus/test/fixtures/lang-resolution/go-return-type-inference/cmd/main.go @@ -16,3 +16,12 @@ func processUserCrossPackage() { user := models.NewUser("bob") user.Save() } + +func GetRepo(name string) *models.Repo { + return &models.Repo{Name: name} +} + +func processRepo() { + repo := GetRepo("main") + repo.Save() +} diff --git a/gitnexus/test/fixtures/lang-resolution/go-return-type-inference/models/repo.go b/gitnexus/test/fixtures/lang-resolution/go-return-type-inference/models/repo.go new file mode 100644 index 000000000..69bbe0895 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/go-return-type-inference/models/repo.go @@ -0,0 +1,13 @@ +package models + +type Repo struct { + Name string +} + +func (r *Repo) Save() bool { + return true +} + +func GetRepo(name string) *Repo { + return &Repo{Name: name} +} diff --git a/gitnexus/test/fixtures/lang-resolution/rust-return-type-inference/src/main.rs b/gitnexus/test/fixtures/lang-resolution/rust-return-type-inference/src/main.rs new file mode 100644 index 000000000..984621603 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-return-type-inference/src/main.rs @@ -0,0 +1,21 @@ +mod models; + +use models::{User, Repo}; + +fn get_user() -> User { + User { name: String::from("alice") } +} + +fn get_repo() -> Repo { + Repo { name: String::from("main") } +} + +fn process_user() { + let user = get_user(); + user.save(); +} + +fn process_repo() { + let repo = get_repo(); + repo.save(); +} diff --git a/gitnexus/test/fixtures/lang-resolution/rust-return-type-inference/src/models.rs b/gitnexus/test/fixtures/lang-resolution/rust-return-type-inference/src/models.rs new file mode 100644 index 000000000..05659ee6c --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-return-type-inference/src/models.rs @@ -0,0 +1,19 @@ +pub struct User { + pub name: String, +} + +impl User { + pub fn save(&self) -> bool { + true + } +} + +pub struct Repo { + pub name: String, +} + +impl Repo { + pub fn save(&self) -> bool { + true + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/swift-return-type-inference/App.swift b/gitnexus/test/fixtures/lang-resolution/swift-return-type-inference/App.swift new file mode 100644 index 000000000..5893a35c3 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/swift-return-type-inference/App.swift @@ -0,0 +1,17 @@ +func getUser() -> User { + return User(name: "alice") +} + +func getRepo() -> Repo { + return Repo(name: "main") +} + +func processUser() { + let user = getUser() + user.save() +} + +func processRepo() { + let repo = getRepo() + repo.save() +} diff --git a/gitnexus/test/fixtures/lang-resolution/swift-return-type-inference/Models.swift b/gitnexus/test/fixtures/lang-resolution/swift-return-type-inference/Models.swift new file mode 100644 index 000000000..a84954990 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/swift-return-type-inference/Models.swift @@ -0,0 +1,11 @@ +class User { + var name: String + init(name: String) { self.name = name } + func save() -> Bool { return true } +} + +class Repo { + var name: String + init(name: String) { self.name = name } + func save() -> Bool { return true } +} diff --git a/gitnexus/test/integration/resolvers/cpp.test.ts b/gitnexus/test/integration/resolvers/cpp.test.ts index 7d3c6a914..09e5ed50b 100644 --- a/gitnexus/test/integration/resolvers/cpp.test.ts +++ b/gitnexus/test/integration/resolvers/cpp.test.ts @@ -512,3 +512,46 @@ describe('C++ return type inference via auto + function call', () => { expect(saveCall).toBeDefined(); }); }); + +// --------------------------------------------------------------------------- +// Return-type inference with competing methods: +// Two classes both have save(), factory functions disambiguate via return type +// --------------------------------------------------------------------------- + +describe('C++ return-type inference via function return type', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'cpp-return-type-inference'), + () => {}, + ); + }, 60000); + + it('resolves user.save() to User#save via return type of getUser()', () => { + const calls = getRelationships(result, 'CALLS'); + const saveCall = calls.find(c => + c.target === 'save' && c.source === 'processUser' && c.targetFilePath.includes('user.h') + ); + expect(saveCall).toBeDefined(); + }); + + it('user.save() does NOT resolve to Repo#save', () => { + const calls = getRelationships(result, 'CALLS'); + const wrongSave = calls.find(c => + c.target === 'save' && c.source === 'processUser' + ); + // Should resolve to exactly one target — if it resolves at all, check it's the right one + if (wrongSave) { + expect(wrongSave.targetFilePath).toContain('user.h'); + } + }); + + it('resolves repo.save() to Repo#save via return type of getRepo()', () => { + const calls = getRelationships(result, 'CALLS'); + const saveCall = calls.find(c => + c.target === 'save' && c.source === 'processRepo' && c.targetFilePath.includes('repo.h') + ); + expect(saveCall).toBeDefined(); + }); +}); diff --git a/gitnexus/test/integration/resolvers/go.test.ts b/gitnexus/test/integration/resolvers/go.test.ts index fa0c71835..f2be3460c 100644 --- a/gitnexus/test/integration/resolvers/go.test.ts +++ b/gitnexus/test/integration/resolvers/go.test.ts @@ -558,39 +558,103 @@ describe('Go return type inference via explicit function return type', () => { ); }, 60000); - it('detects GetUser and Save symbols', () => { + it('detects GetUser, GetRepo, and competing Save methods', () => { const allSymbols = [...getNodesByLabel(result, 'Function'), ...getNodesByLabel(result, 'Method')]; expect(allSymbols).toContain('GetUser'); - expect(allSymbols).toContain('Save'); + expect(allSymbols).toContain('GetRepo'); + const saveMethods = allSymbols.filter(s => s === 'Save'); + expect(saveMethods.length).toBe(2); }); - it('resolves user.Save() to User#Save via return type of GetUser() *models.User', () => { - // The Go CONSTRUCTOR_BINDING_SCANNER registers `user` → GetUser in the type - // environment. Combined with GetUser's declared return type (*models.User), - // the pipeline resolves user.Save() to the correct User#Save method. + it('resolves user.Save() to models/user.go via return type of GetUser()', () => { const calls = getRelationships(result, 'CALLS'); const saveCall = calls.find(c => - c.target === 'Save' && c.source === 'processUser' && c.targetFilePath.includes('models') + c.target === 'Save' && c.source === 'processUser' && c.targetFilePath.includes('user.go') ); expect(saveCall).toBeDefined(); }); - it('resolves user.Save() via short_var_declaration binding (CONSTRUCTOR_BINDING_SCANNER)', () => { - // Verifies that the scanner path — not just explicit return-type annotation — - // is responsible for establishing the user → User binding. + it('user.Save() does NOT resolve to models/repo.go (negative disambiguation)', () => { const calls = getRelationships(result, 'CALLS'); - const saveCall = calls.find(c => c.target === 'Save' && c.targetFilePath.includes('models')); + const wrongSave = calls.find(c => + c.target === 'Save' && c.source === 'processUser' && c.targetFilePath.includes('repo.go') + ); + expect(wrongSave).toBeUndefined(); + }); + + it('resolves repo.Save() to models/repo.go via return type of GetRepo()', () => { + const calls = getRelationships(result, 'CALLS'); + const saveCall = calls.find(c => + c.target === 'Save' && c.source === 'processRepo' && c.targetFilePath.includes('repo.go') + ); expect(saveCall).toBeDefined(); }); + it('repo.Save() does NOT resolve to models/user.go (negative disambiguation)', () => { + const calls = getRelationships(result, 'CALLS'); + const wrongSave = calls.find(c => + c.target === 'Save' && c.source === 'processRepo' && c.targetFilePath.includes('user.go') + ); + expect(wrongSave).toBeUndefined(); + }); + it('resolves user.Save() via cross-package factory call models.NewUser()', () => { - // Go package-qualified calls use `selector_expression` in the AST. - // extractSimpleTypeName must handle selector_expression + field_identifier - // to extract "NewUser" from `models.NewUser()`. const calls = getRelationships(result, 'CALLS'); const saveCall = calls.find(c => - c.target === 'Save' && c.source === 'processUserCrossPackage' && c.targetFilePath.includes('models') + c.target === 'Save' && c.source === 'processUserCrossPackage' && c.targetFilePath.includes('user.go') ); expect(saveCall).toBeDefined(); }); }); + +// --------------------------------------------------------------------------- +// Go multi-return factory inference: user, err := NewUser("alice"); user.Save() +// --------------------------------------------------------------------------- + +describe('Go multi-return factory type inference', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'go-multi-return-inference'), + () => {}, + ); + }, 60000); + + it('detects User and Repo structs with competing Save methods', () => { + const saveMethods = getNodesByLabel(result, 'Method').filter(m => m === 'Save'); + expect(saveMethods.length).toBe(2); + }); + + it('resolves user.Save() to models/user.go via multi-return inference (user, err := NewUser())', () => { + const calls = getRelationships(result, 'CALLS'); + const userSave = calls.find(c => + c.target === 'Save' && c.source === 'processUser' && c.targetFilePath.includes('user.go') + ); + expect(userSave).toBeDefined(); + }); + + it('user.Save() does NOT resolve to models/repo.go', () => { + const calls = getRelationships(result, 'CALLS'); + const wrongSave = calls.find(c => + c.target === 'Save' && c.source === 'processUser' && c.targetFilePath.includes('repo.go') + ); + expect(wrongSave).toBeUndefined(); + }); + + it('resolves repo.Save() to models/repo.go via blank discard (repo, _ := NewRepo())', () => { + const calls = getRelationships(result, 'CALLS'); + const repoSave = calls.find(c => + c.target === 'Save' && c.source === 'processRepo' && c.targetFilePath.includes('repo.go') + ); + expect(repoSave).toBeDefined(); + }); + + it('repo.Save() does NOT resolve to models/user.go', () => { + const calls = getRelationships(result, 'CALLS'); + const wrongSave = calls.find(c => + c.target === 'Save' && c.source === 'processRepo' && c.targetFilePath.includes('user.go') + ); + expect(wrongSave).toBeUndefined(); + }); +}); diff --git a/gitnexus/test/integration/resolvers/rust.test.ts b/gitnexus/test/integration/resolvers/rust.test.ts index 930df4885..6e182cc40 100644 --- a/gitnexus/test/integration/resolvers/rust.test.ts +++ b/gitnexus/test/integration/resolvers/rust.test.ts @@ -636,3 +636,46 @@ describe('Rust return type inference', () => { expect(saveCall!.targetFilePath).toBe('src/models.rs'); }); }); + +// --------------------------------------------------------------------------- +// Return-type inference with competing methods: +// Two structs both have save(), factory functions disambiguate via return type +// --------------------------------------------------------------------------- + +describe('Rust return-type inference via function return type', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'rust-return-type-inference'), + () => {}, + ); + }, 60000); + + it('resolves user.save() to models.rs User#save via return type of get_user()', () => { + const calls = getRelationships(result, 'CALLS'); + const saveCall = calls.find(c => + c.target === 'save' && c.source === 'process_user' && c.targetFilePath.includes('models') + ); + expect(saveCall).toBeDefined(); + }); + + it('user.save() does NOT resolve to Repo#save', () => { + const calls = getRelationships(result, 'CALLS'); + const wrongSave = calls.find(c => + c.target === 'save' && c.source === 'process_user' + ); + // Should resolve to exactly one target — if it resolves at all, check it's the right one + if (wrongSave) { + expect(wrongSave.targetFilePath).toContain('models'); + } + }); + + it('resolves repo.save() to models.rs Repo#save via return type of get_repo()', () => { + const calls = getRelationships(result, 'CALLS'); + const saveCall = calls.find(c => + c.target === 'save' && c.source === 'process_repo' && c.targetFilePath.includes('models') + ); + expect(saveCall).toBeDefined(); + }); +}); diff --git a/gitnexus/test/integration/resolvers/swift.test.ts b/gitnexus/test/integration/resolvers/swift.test.ts index d20e548af..6ab8c28b6 100644 --- a/gitnexus/test/integration/resolvers/swift.test.ts +++ b/gitnexus/test/integration/resolvers/swift.test.ts @@ -179,3 +179,46 @@ describe.skipIf(!swiftAvailable)('Swift return type inference', () => { expect(saveCall).toBeDefined(); }); }); + +// --------------------------------------------------------------------------- +// Return-type inference with competing methods: +// Two classes both have save(), factory functions disambiguate via return type +// --------------------------------------------------------------------------- + +describe.skipIf(!swiftAvailable)('Swift return-type inference via function return type', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'swift-return-type-inference'), + () => {}, + ); + }, 60000); + + it('resolves user.save() to User#save via return type of getUser()', () => { + const calls = getRelationships(result, 'CALLS'); + const saveCall = calls.find(c => + c.target === 'save' && c.source === 'processUser' && c.targetFilePath.includes('Models.swift') + ); + expect(saveCall).toBeDefined(); + }); + + it('user.save() does NOT resolve to Repo#save', () => { + const calls = getRelationships(result, 'CALLS'); + const wrongSave = calls.find(c => + c.target === 'save' && c.source === 'processUser' + ); + // Should resolve to exactly one target — if it resolves at all, check it's the right one + if (wrongSave) { + expect(wrongSave.targetFilePath).toContain('Models.swift'); + } + }); + + it('resolves repo.save() to Repo#save via return type of getRepo()', () => { + const calls = getRelationships(result, 'CALLS'); + const saveCall = calls.find(c => + c.target === 'save' && c.source === 'processRepo' && c.targetFilePath.includes('Models.swift') + ); + expect(saveCall).toBeDefined(); + }); +}); diff --git a/gitnexus/test/unit/call-processor.test.ts b/gitnexus/test/unit/call-processor.test.ts index 85e9c2f3f..e387ca576 100644 --- a/gitnexus/test/unit/call-processor.test.ts +++ b/gitnexus/test/unit/call-processor.test.ts @@ -641,6 +641,34 @@ describe('extractReturnTypeName', () => { expect(extractReturnTypeName('CompletableFuture>')).toBe('User'); }); + // Rust smart pointer unwrapping + it('unwraps Rc → User', () => { + expect(extractReturnTypeName('Rc')).toBe('User'); + }); + it('unwraps Arc → User', () => { + expect(extractReturnTypeName('Arc')).toBe('User'); + }); + it('unwraps Weak → User', () => { + expect(extractReturnTypeName('Weak')).toBe('User'); + }); + it('unwraps MutexGuard → User', () => { + expect(extractReturnTypeName('MutexGuard')).toBe('User'); + }); + it('unwraps RwLockReadGuard → User', () => { + expect(extractReturnTypeName('RwLockReadGuard')).toBe('User'); + }); + it('unwraps Cow → User', () => { + expect(extractReturnTypeName('Cow')).toBe('User'); + }); + // Nested: Arc> → User (double unwrap) + it('unwraps Arc> → User', () => { + expect(extractReturnTypeName('Arc>')).toBe('User'); + }); + // NOT unwrapped (containers/wrappers not in set) + it('does not unwrap Mutex (not a Deref wrapper)', () => { + expect(extractReturnTypeName('Mutex')).toBe('Mutex'); + }); + it('returns undefined for lowercase non-class types', () => { expect(extractReturnTypeName('error')).toBeUndefined(); }); diff --git a/gitnexus/test/unit/method-signature.test.ts b/gitnexus/test/unit/method-signature.test.ts index 859b46d30..ccd03b4ba 100644 --- a/gitnexus/test/unit/method-signature.test.ts +++ b/gitnexus/test/unit/method-signature.test.ts @@ -268,7 +268,7 @@ func parse(s string) (string, error) { return s, nil }`; const sig = extractMethodSignature(funcNode); expect(sig.parameterCount).toBe(1); - expect(sig.returnType).toBe('(string, error)'); + expect(sig.returnType).toBe('string'); }); it('handles no return type', () => {