mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-09 03:17:54 +00:00
fix: review findings — receiverClassName parity, Rust wrappers, Go multi-return, Kotlin/Swift qualified calls
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.
This commit is contained in:
parent
25483b28cd
commit
6465b8d34c
24 changed files with 537 additions and 76 deletions
|
|
@ -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<string, string> => {
|
||||
const verified = new Map<string, string>();
|
||||
|
||||
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<string, string>();
|
||||
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<string, string>();
|
||||
|
||||
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<string, Map<string, string>>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
20
gitnexus/test/fixtures/lang-resolution/cpp-return-type-inference/app.cpp
vendored
Normal file
20
gitnexus/test/fixtures/lang-resolution/cpp-return-type-inference/app.cpp
vendored
Normal file
|
|
@ -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();
|
||||
}
|
||||
9
gitnexus/test/fixtures/lang-resolution/cpp-return-type-inference/repo.h
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/cpp-return-type-inference/repo.h
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
#pragma once
|
||||
|
||||
class Repo {
|
||||
public:
|
||||
Repo(const char* name) : name_(name) {}
|
||||
bool save() { return true; }
|
||||
private:
|
||||
const char* name_;
|
||||
};
|
||||
9
gitnexus/test/fixtures/lang-resolution/cpp-return-type-inference/user.h
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/cpp-return-type-inference/user.h
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
#pragma once
|
||||
|
||||
class User {
|
||||
public:
|
||||
User(const char* name) : name_(name) {}
|
||||
bool save() { return true; }
|
||||
private:
|
||||
const char* name_;
|
||||
};
|
||||
24
gitnexus/test/fixtures/lang-resolution/go-multi-return-inference/cmd/main.go
vendored
Normal file
24
gitnexus/test/fixtures/lang-resolution/go-multi-return-inference/cmd/main.go
vendored
Normal file
|
|
@ -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()
|
||||
}
|
||||
3
gitnexus/test/fixtures/lang-resolution/go-multi-return-inference/go.mod
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/go-multi-return-inference/go.mod
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module example.com/multireturn
|
||||
|
||||
go 1.21
|
||||
9
gitnexus/test/fixtures/lang-resolution/go-multi-return-inference/models/repo.go
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/go-multi-return-inference/models/repo.go
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package models
|
||||
|
||||
type Repo struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (r *Repo) Save() bool {
|
||||
return true
|
||||
}
|
||||
9
gitnexus/test/fixtures/lang-resolution/go-multi-return-inference/models/user.go
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/go-multi-return-inference/models/user.go
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package models
|
||||
|
||||
type User struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (u *User) Save() bool {
|
||||
return true
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
13
gitnexus/test/fixtures/lang-resolution/go-return-type-inference/models/repo.go
vendored
Normal file
13
gitnexus/test/fixtures/lang-resolution/go-return-type-inference/models/repo.go
vendored
Normal file
|
|
@ -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}
|
||||
}
|
||||
21
gitnexus/test/fixtures/lang-resolution/rust-return-type-inference/src/main.rs
vendored
Normal file
21
gitnexus/test/fixtures/lang-resolution/rust-return-type-inference/src/main.rs
vendored
Normal file
|
|
@ -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();
|
||||
}
|
||||
19
gitnexus/test/fixtures/lang-resolution/rust-return-type-inference/src/models.rs
vendored
Normal file
19
gitnexus/test/fixtures/lang-resolution/rust-return-type-inference/src/models.rs
vendored
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
17
gitnexus/test/fixtures/lang-resolution/swift-return-type-inference/App.swift
vendored
Normal file
17
gitnexus/test/fixtures/lang-resolution/swift-return-type-inference/App.swift
vendored
Normal file
|
|
@ -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()
|
||||
}
|
||||
11
gitnexus/test/fixtures/lang-resolution/swift-return-type-inference/Models.swift
vendored
Normal file
11
gitnexus/test/fixtures/lang-resolution/swift-return-type-inference/Models.swift
vendored
Normal file
|
|
@ -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 }
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -641,6 +641,34 @@ describe('extractReturnTypeName', () => {
|
|||
expect(extractReturnTypeName('CompletableFuture<Optional<User>>')).toBe('User');
|
||||
});
|
||||
|
||||
// Rust smart pointer unwrapping
|
||||
it('unwraps Rc<User> → User', () => {
|
||||
expect(extractReturnTypeName('Rc<User>')).toBe('User');
|
||||
});
|
||||
it('unwraps Arc<User> → User', () => {
|
||||
expect(extractReturnTypeName('Arc<User>')).toBe('User');
|
||||
});
|
||||
it('unwraps Weak<User> → User', () => {
|
||||
expect(extractReturnTypeName('Weak<User>')).toBe('User');
|
||||
});
|
||||
it('unwraps MutexGuard<User> → User', () => {
|
||||
expect(extractReturnTypeName('MutexGuard<User>')).toBe('User');
|
||||
});
|
||||
it('unwraps RwLockReadGuard<User> → User', () => {
|
||||
expect(extractReturnTypeName('RwLockReadGuard<User>')).toBe('User');
|
||||
});
|
||||
it('unwraps Cow<User> → User', () => {
|
||||
expect(extractReturnTypeName('Cow<User>')).toBe('User');
|
||||
});
|
||||
// Nested: Arc<Option<User>> → User (double unwrap)
|
||||
it('unwraps Arc<Option<User>> → User', () => {
|
||||
expect(extractReturnTypeName('Arc<Option<User>>')).toBe('User');
|
||||
});
|
||||
// NOT unwrapped (containers/wrappers not in set)
|
||||
it('does not unwrap Mutex<User> (not a Deref wrapper)', () => {
|
||||
expect(extractReturnTypeName('Mutex<User>')).toBe('Mutex');
|
||||
});
|
||||
|
||||
it('returns undefined for lowercase non-class types', () => {
|
||||
expect(extractReturnTypeName('error')).toBeUndefined();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue