feat: Phase 4 — CONSTRUCTOR_BINDING_SCANNERS for all languages + return type inference tests

Add CONSTRUCTOR_BINDING_SCANNERS for 6 missing languages, completing
return type inference coverage across all 11 supported languages:

- TypeScript/JS: variable_declarator with call_expression, unwraps await
- Go: short_var_declaration single-assignment (skips multi-return, new/make)
- Java: local_variable_declaration with `var` type + method_invocation
- C#: variable_declaration with implicit_type (var) + invocation_expression
- Rust: let_declaration without type annotation, handles mut_pattern
- PHP: assignment_expression with function_call_expression

Also adds property_identifier to extractSimpleTypeName for qualified
member calls (repo.getUser → getUser), fixing namespaced constructor
inference that was previously a known limitation.

Integration tests added for all 11 languages with correct label
assertions (Function vs Method per language's tree-sitter queries).
This commit is contained in:
Gergo Magyar 2026-03-15 09:21:03 +00:00
parent 55c13fc0e8
commit dbf37cd572
28 changed files with 661 additions and 18 deletions

7
.gitignore vendored
View file

@ -62,4 +62,9 @@ docs/plans/
gitnexus/test/fixtures/mini-repo/*.md
gitnexus/test/fixtures/mini-repo/.claude
gitnexus/test/fixtures/mini-repo/.gitignore
gitnexus/test/fixtures/mini-repo/.gitignore
# Ignore csharp generated obj and bin folders
gitnexus/test/fixtures/lang-resolution/**/obj
gitnexus/test/fixtures/lang-resolution/**/bin
GitNexus.sln

View file

@ -420,8 +420,42 @@ const extractCppConstructorBinding = (node: SyntaxNode): { varName: string; call
return { varName, calleeName: func.text };
};
/**
* TypeScript/JavaScript: const user = getUser() — variable_declarator with call_expression value.
* Only matches unannotated declarators; annotated ones are handled by extractDeclaration.
* await is unwrapped: const user = await fetchUser() → callee = 'fetchUser'.
*/
const extractTsJsConstructorBinding = (node: SyntaxNode): { varName: string; calleeName: string } | undefined => {
if (node.type !== 'variable_declarator') return undefined;
// Skip if has an explicit type annotation — extractDeclaration handles those
if (node.childForFieldName('type')) return undefined;
for (const child of node.children) {
if (child.type === 'type_annotation') return undefined;
}
const nameNode = node.childForFieldName('name');
if (!nameNode || nameNode.type !== 'identifier') return undefined;
let value = node.childForFieldName('value');
if (!value) return undefined;
// Unwrap await expressions: const user = await fetchUser()
if (value.type === 'await_expression') {
value = value.firstNamedChild;
if (!value) return undefined;
}
if (value.type !== 'call_expression') return undefined;
// Skip new_expression — extractInitializer handles constructor calls
const func = value.childForFieldName('function');
if (!func) return undefined;
const calleeName = extractSimpleTypeName(func);
if (!calleeName) return undefined;
return { varName: nameNode.text, calleeName };
};
/** Language-specific constructor-binding scanners. */
const CONSTRUCTOR_BINDING_SCANNERS: Partial<Record<SupportedLanguages, (node: SyntaxNode) => { varName: string; calleeName: string } | undefined>> = {
// TypeScript/JavaScript share the same variable_declarator scanner
[SupportedLanguages.TypeScript]: extractTsJsConstructorBinding,
[SupportedLanguages.JavaScript]: extractTsJsConstructorBinding,
// Kotlin: val x = User(...) — property_declaration with call_expression
[SupportedLanguages.Kotlin]: (node) => {
if (node.type !== 'property_declaration') return undefined;
@ -509,5 +543,141 @@ const CONSTRUCTOR_BINDING_SCANNERS: Partial<Record<SupportedLanguages, (node: Sy
// Ruby: user = User.new — uses shared helper that also handles Models::User.new
[SupportedLanguages.Ruby]: extractRubyConstructorAssignment,
// Rust: let user = get_user("alice") — let_declaration with call_expression value, no type annotation.
// Skips `let user: User = ...` (explicit type annotation — handled by extractDeclaration).
// Skips `let user = User::new()` (scoped_identifier callee named "new" — handled by extractInitializer).
// Unwraps `let mut user = get_user()` by looking inside mut_pattern for the inner identifier.
[SupportedLanguages.Rust]: (node) => {
if (node.type !== 'let_declaration') return undefined;
// Skip if has explicit type annotation — extractDeclaration handles those
if (node.childForFieldName('type')) return undefined;
for (const child of node.children) {
if (child.type === 'type_annotation') return undefined;
}
let patternNode = node.childForFieldName('pattern');
if (!patternNode) return undefined;
// Unwrap mut: `let mut user` → mut_pattern > identifier
if (patternNode.type === 'mut_pattern') {
patternNode = patternNode.firstNamedChild;
if (!patternNode) return undefined;
}
if (patternNode.type !== 'identifier') return undefined;
const value = node.childForFieldName('value');
if (!value || value.type !== 'call_expression') return undefined;
const func = value.childForFieldName('function');
if (!func) return undefined;
// Skip Struct::new() patterns — handled by extractInitializer in rust.ts
if (func.type === 'scoped_identifier') {
const methodName = func.lastNamedChild;
if (methodName?.text === 'new') return undefined;
}
const calleeName = extractSimpleTypeName(func);
if (!calleeName) return undefined;
return { varName: patternNode.text, calleeName };
},
// PHP: $user = getUser() — assignment_expression with variable_name left and function_call_expression right
// object_creation_expression ($user = new User()) is handled by extractInitializer.
// Explicit typed properties (private UserRepo $repo) are handled by extractDeclaration.
// PHP variable names include the $ sigil — kept as-is to match what extractVarName stores in the env.
[SupportedLanguages.PHP]: (node) => {
if (node.type !== 'assignment_expression') return undefined;
const left = node.childForFieldName('left');
const right = node.childForFieldName('right');
if (!left || !right) return undefined;
if (left.type !== 'variable_name') return undefined;
// Skip object_creation_expression (new User()) — handled by extractInitializer
if (right.type === 'object_creation_expression') return undefined;
if (right.type !== 'function_call_expression') return undefined;
const func = right.childForFieldName('function') ?? right.firstNamedChild;
if (!func) return undefined;
const calleeName = extractSimpleTypeName(func);
if (!calleeName) return undefined;
// Keep the $ sigil — PHP env keys are stored with $ (e.g. "$user") by extractVarName
const varName = left.text;
if (!varName) return undefined;
return { varName, calleeName };
},
// Java: var user = getUser() — local_variable_declaration with `var` type and method_invocation value
// Explicit types (User user = getUser()) are handled by extractDeclaration.
// object_creation_expression (new User()) is handled by extractJavaInitializer.
[SupportedLanguages.Java]: (node) => {
if (node.type !== 'local_variable_declaration') return undefined;
const typeNode = node.childForFieldName('type');
if (!typeNode) return undefined;
// Only handle `var` — explicit types are handled by extractDeclaration
if (typeNode.text !== 'var') return undefined;
const declarator = node.namedChildren.find((c: any) => c.type === 'variable_declarator');
if (!declarator) return undefined;
const nameNode = declarator.childForFieldName('name');
const value = declarator.childForFieldName('value');
if (!nameNode || !value) return undefined;
// Skip object_creation_expression (new User()) — handled by extractInitializer
if (value.type === 'object_creation_expression') return undefined;
if (value.type !== 'method_invocation') return undefined;
const methodName = value.childForFieldName('name');
if (!methodName) return undefined;
return { varName: nameNode.text, calleeName: methodName.text };
},
// C#: var user = GetUser() — variable_declaration with implicit_type and invocation_expression value.
// Explicit types (User user = GetUser()) are handled by extractDeclaration.
// object_creation_expression (new User()) is handled by extractInitializer.
[SupportedLanguages.CSharp]: (node) => {
if (node.type !== 'variable_declaration') return undefined;
const typeNode = node.childForFieldName('type');
// Only handle implicit_type (var) — explicit types handled by extractDeclaration
if (!typeNode || typeNode.type !== 'implicit_type') return undefined;
// Find first variable_declarator child
let declarator: any = null;
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (child?.type === 'variable_declarator') { declarator = child; break; }
}
if (!declarator) return undefined;
const nameNode = declarator.childForFieldName('name') ?? declarator.firstNamedChild;
if (!nameNode || nameNode.type !== 'identifier') return undefined;
// Find equals_value_clause
let eqClause: any = null;
for (let i = 0; i < declarator.namedChildCount; i++) {
const child = declarator.namedChild(i);
if (child?.type === 'equals_value_clause') { eqClause = child; break; }
}
if (!eqClause) return undefined;
const value = eqClause.firstNamedChild;
if (!value) return undefined;
// Skip object_creation_expression (new User()) — handled by extractInitializer
if (value.type === 'object_creation_expression') return undefined;
if (value.type !== 'invocation_expression') return undefined;
const func = value.firstNamedChild;
if (!func) return undefined;
const calleeName = extractSimpleTypeName(func);
if (!calleeName) return undefined;
return { varName: nameNode.text, calleeName };
},
// Go: user := GetUser("alice") — short_var_declaration with single call_expression on the right.
// Multi-return (`user, err := GetUser()`) is intentionally skipped.
// new() and make() are already handled by extractDeclaration in go.ts.
[SupportedLanguages.Go]: (node) => {
if (node.type !== 'short_var_declaration') return undefined;
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];
if (rightExprs.length !== 1 || rightExprs[0].type !== 'call_expression') return undefined;
const func = rightExprs[0].childForFieldName('function');
if (!func) return undefined;
// Skip new() and make() — already handled by extractDeclaration
if (func.text === 'new' || func.text === 'make') return undefined;
const calleeName = extractSimpleTypeName(func);
if (!calleeName) return undefined;
return { varName: leftIds[0].text, calleeName };
},
};

View file

@ -22,7 +22,7 @@ export const extractSimpleTypeName = (typeNode: SyntaxNode): string | undefined
const last = typeNode.lastNamedChild;
if (last && (last.type === 'type_identifier' || last.type === 'identifier'
|| last.type === 'simple_identifier' || last.type === 'name'
|| last.type === 'constant')) {
|| last.type === 'constant' || last.type === 'property_identifier')) {
return last.text;
}
}

View file

@ -0,0 +1,10 @@
#include "user.h"
User getUser(const char* name) {
return User(name);
}
void processUser() {
auto user = getUser("alice");
user.save();
}

View file

@ -0,0 +1,11 @@
#pragma once
class User {
public:
User(const char* name) : name_(name) {}
void save() {}
private:
const char* name_;
};
User getUser(const char* name);

View file

@ -0,0 +1,24 @@
namespace Models;
public class User
{
private string _name;
public User(string name)
{
_name = name;
}
public bool Save()
{
return true;
}
}
public class UserService
{
public User GetUser(string name)
{
return new User(name);
}
}

View file

@ -0,0 +1,13 @@
using Models;
namespace Services;
public class App
{
public void Run()
{
var svc = new UserService();
var user = svc.GetUser("alice");
user.Save();
}
}

View file

@ -0,0 +1,9 @@
package models
class User(val name: String) {
fun save() {}
}
fun getUser(name: String): User {
return User(name)
}

View file

@ -0,0 +1,8 @@
package services
import models.getUser
fun processUser() {
val user = getUser("alice")
user.save()
}

View file

@ -0,0 +1,15 @@
<?php
namespace App\Models;
class User {
private string $name;
public function __construct(string $name) {
$this->name = $name;
}
public function save(): bool {
return true;
}
}

View file

@ -0,0 +1,16 @@
<?php
namespace App\Services;
use App\Models\User;
class UserService {
public function getUser(string $name): User {
return new User($name);
}
public function processUser(): void {
$user = $this->getUser("alice");
$user->save();
}
}

View file

@ -0,0 +1,6 @@
require_relative 'models'
def process_user
user = get_user('alice')
user.save
end

View file

@ -0,0 +1,13 @@
class User
def initialize(name)
@name = name
end
def save
true
end
end
def get_user(name)
User.new(name)
end

View file

@ -0,0 +1,7 @@
mod models;
use crate::models::get_user;
fn main() {
let user = get_user("alice");
user.save();
}

View file

@ -0,0 +1,11 @@
pub struct User {
pub name: String,
}
impl User {
pub fn save(&self) {}
}
pub fn get_user(name: &str) -> User {
User { name: name.to_string() }
}

View file

@ -0,0 +1,4 @@
func processUser() {
let user = getUser(name: "alice")
user.save()
}

View file

@ -0,0 +1,13 @@
class User {
let name: String
init(name: String) {
self.name = name
}
func save() {}
}
func getUser(name: String) -> User {
return User(name: name)
}

View file

@ -477,3 +477,38 @@ describe('C++ range-for explicit type resolution', () => {
expect(edge).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// Return type inference: auto user = getUser("alice"); user.save()
// C++'s CONSTRUCTOR_BINDING_SCANNER captures auto declarations with
// call_expression values, enabling return type inference from function results.
// ---------------------------------------------------------------------------
describe('C++ return type inference via auto + function call', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'cpp-return-type'),
() => {},
);
}, 60000);
it('detects User class and getUser function', () => {
expect(getNodesByLabel(result, 'Class')).toContain('User');
expect(getNodesByLabel(result, 'Function')).toContain('getUser');
});
it('detects save method on User', () => {
const methods = getNodesByLabel(result, 'Method');
expect(methods).toContain('save');
});
it('resolves user.save() to User#save via return type of getUser(): User', () => {
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find(c =>
c.target === 'save' && c.source === 'processUser' && c.targetFilePath.includes('user.h'),
);
expect(saveCall).toBeDefined();
});
});

View file

@ -558,3 +558,42 @@ describe('C# is pattern matching resolution', () => {
expect(catExtends!.target).toBe('Animal');
});
});
// ---------------------------------------------------------------------------
// Return type inference: var user = svc.GetUser("alice"); user.Save()
// C#'s CONSTRUCTOR_BINDING_SCANNER handles `var` declarations with
// invocation_expression values, enabling end-to-end return type inference.
// ---------------------------------------------------------------------------
describe('C# return type inference via var + invocation', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'csharp-return-type'),
() => {},
);
}, 60000);
it('detects User and UserService classes', () => {
expect(getNodesByLabel(result, 'Class')).toContain('User');
expect(getNodesByLabel(result, 'Class')).toContain('UserService');
});
it('detects Save and GetUser methods', () => {
const methods = getNodesByLabel(result, 'Method');
expect(methods).toContain('Save');
expect(methods).toContain('GetUser');
});
it('resolves user.Save() to User#Save via return type of GetUser(): User', () => {
// C#'s CONSTRUCTOR_BINDING_SCANNER binds `var user = svc.GetUser()` to the
// return type of GetUser (User), so the subsequent user.Save() call resolves
// to User#Save rather than an unresolved target.
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find(c =>
c.target === 'Save' && c.source === 'Run' && c.targetFilePath.includes('Models'),
);
expect(saveCall).toBeDefined();
});
});

View file

@ -544,8 +544,8 @@ describe('Go type assertion type inference', () => {
// ---------------------------------------------------------------------------
// Return type inference: user := GetUser("alice"); user.Save()
// Go has no CONSTRUCTOR_BINDING_SCANNER for short variable declarations yet,
// so return type inference does NOT work end-to-end for `user := GetUser()`.
// Go now has a CONSTRUCTOR_BINDING_SCANNER for short_var_declaration, so
// return type inference works end-to-end for `user := GetUser()`.
// ---------------------------------------------------------------------------
describe('Go return type inference via explicit function return type', () => {
@ -565,12 +565,21 @@ describe('Go return type inference via explicit function return type', () => {
});
it('resolves user.Save() to User#Save via return type of GetUser() *models.User', () => {
// Go's extractMethodSignature captures *models.User as the return type.
// extractReturnTypeName strips the pointer prefix and qualified name → 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.
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find(c =>
c.target === 'Save' && c.source === 'processUser' && c.targetFilePath.includes('models')
);
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.
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find(c => c.target === 'Save' && c.targetFilePath.includes('models'));
expect(saveCall).toBeDefined();
});
});

View file

@ -529,8 +529,8 @@ describe('Java generic parent super resolution', () => {
// ---------------------------------------------------------------------------
// Return type inference: var user = svc.getUser("alice"); user.save()
// Java has no CONSTRUCTOR_BINDING_SCANNER for `var` declarations yet,
// so return type inference does NOT work end-to-end.
// Java's CONSTRUCTOR_BINDING_SCANNER handles `var` declarations with
// method_invocation values, enabling end-to-end return type inference.
// ---------------------------------------------------------------------------
describe('Java return type inference via explicit method return type', () => {
@ -555,9 +555,9 @@ describe('Java return type inference via explicit method return type', () => {
});
it('resolves user.save() to User#save via return type of getUser(): User', () => {
// Java's type extractor handles `var user = svc.getUser()` through
// the local_variable_declaration path. The return type of getUser is User,
// enabling save() to resolve to User#save.
// Java's CONSTRUCTOR_BINDING_SCANNER binds `var user = svc.getUser()` to the
// return type of getUser (User), so the subsequent user.save() call resolves
// to User#save rather than an unresolved target.
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find(c =>
c.target === 'save' && c.source === 'processUser' && c.targetFilePath.includes('models')

View file

@ -458,6 +458,40 @@ describe('Kotlin this resolution', () => {
});
});
// ---------------------------------------------------------------------------
// Return type inference: val user = getUser("alice"); user.save()
// Kotlin's CONSTRUCTOR_BINDING_SCANNER captures property_declaration with
// call_expression values, enabling return type inference from function results.
// ---------------------------------------------------------------------------
describe('Kotlin return type inference', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'kotlin-return-type'),
() => {},
);
}, 60000);
it('detects User class and getUser function', () => {
expect(getNodesByLabel(result, 'Class')).toContain('User');
expect(getNodesByLabel(result, 'Function')).toContain('getUser');
});
it('detects save function on User (Kotlin class methods are Function nodes)', () => {
expect(getNodesByLabel(result, 'Function')).toContain('save');
});
it('resolves user.save() to User#save via return type of getUser(): User', () => {
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find(c =>
c.target === 'save' && c.source === 'processUser' && c.targetFilePath.includes('User.kt'),
);
expect(saveCall).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// Parent class resolution: EXTENDS + IMPLEMENTS edges
// ---------------------------------------------------------------------------

View file

@ -695,3 +695,39 @@ describe('PHP typed class property resolution', () => {
expect(saveCall!.targetFilePath).toBe('app/Models/UserRepo.php');
});
});
// ---------------------------------------------------------------------------
// Return type inference: $user = $this->getUser("alice"); $user->save()
// PHP's CONSTRUCTOR_BINDING_SCANNER captures assignment_expression with
// function_call_expression values, enabling return type inference.
// ---------------------------------------------------------------------------
describe('PHP return type inference via function call', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'php-return-type'),
() => {},
);
}, 60000);
it('detects User and UserService classes', () => {
expect(getNodesByLabel(result, 'Class')).toContain('User');
expect(getNodesByLabel(result, 'Class')).toContain('UserService');
});
it('detects save and getUser methods', () => {
const methods = getNodesByLabel(result, 'Method');
expect(methods).toContain('save');
expect(methods).toContain('getUser');
});
it('resolves $user->save() to User#save via return type of getUser(): User', () => {
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find(c =>
c.target === 'save' && c.source === 'processUser' && c.targetFilePath.includes('User.php'),
);
expect(saveCall).toBeDefined();
});
});

View file

@ -587,3 +587,39 @@ describe('Ruby namespaced constructor resolution (Models::UserService.new)', ()
expect(validateCall).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// Return type inference: user = get_user('alice'); user.save
// Ruby's CONSTRUCTOR_BINDING_SCANNER captures assignment nodes with
// User.new calls. For plain function calls like get_user(), inference
// works via the scanner + SymbolTable return type lookup.
// ---------------------------------------------------------------------------
describe('Ruby return type inference via function call', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'ruby-return-type'),
() => {},
);
}, 60000);
it('detects User class and get_user method', () => {
expect(getNodesByLabel(result, 'Class')).toContain('User');
// Ruby `def` is always captured as definition.method (Method label)
expect(getNodesByLabel(result, 'Method')).toContain('get_user');
});
it('detects save method on User', () => {
expect(getNodesByLabel(result, 'Method')).toContain('save');
});
it('resolves user.save to 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.rb'),
);
expect(saveCall).toBeDefined();
});
});

View file

@ -600,3 +600,39 @@ describe('Rust if-let captured_pattern type resolution', () => {
expect(validateCall!.targetFilePath).toBe('models.rs');
});
});
// ---------------------------------------------------------------------------
// Return type inference: let user = get_user("alice"); user.save()
// Plain function call (no ::new) with no type annotation
// ---------------------------------------------------------------------------
describe('Rust return type inference', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'rust-return-type'),
() => {},
);
}, 60000);
it('detects User struct and get_user + save functions', () => {
expect(getNodesByLabel(result, 'Struct')).toContain('User');
expect(getNodesByLabel(result, 'Function')).toContain('get_user');
expect(getNodesByLabel(result, 'Function')).toContain('save');
});
it('resolves main → get_user as a CALLS edge to src/models.rs', () => {
const calls = getRelationships(result, 'CALLS');
const getUserCall = calls.find(c => c.target === 'get_user' && c.source === 'main');
expect(getUserCall).toBeDefined();
expect(getUserCall!.targetFilePath).toBe('src/models.rs');
});
it('resolves user.save() to src/models.rs via return-type-inferred binding', () => {
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find(c => c.target === 'save' && c.source === 'main');
expect(saveCall).toBeDefined();
expect(saveCall!.targetFilePath).toBe('src/models.rs');
});
});

View file

@ -145,3 +145,37 @@ describe.skipIf(!swiftAvailable)('Swift cross-file User.init() inference', () =>
expect(greetCall!.source).toBe('main');
});
});
// ---------------------------------------------------------------------------
// Return type inference: let user = getUser(name: "alice"); user.save()
// Swift's CONSTRUCTOR_BINDING_SCANNER captures property_declaration with
// call_expression values, enabling return type inference from function results.
// ---------------------------------------------------------------------------
describe.skipIf(!swiftAvailable)('Swift return type inference', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'swift-return-type'),
() => {},
);
}, 60000);
it('detects User class and getUser function', () => {
expect(getNodesByLabel(result, 'Class')).toContain('User');
expect(getNodesByLabel(result, 'Function')).toContain('getUser');
});
it('detects save function on User (Swift class methods are Function nodes)', () => {
expect(getNodesByLabel(result, 'Function')).toContain('save');
});
it('resolves user.save() to User#save via return type of getUser() -> User', () => {
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find(c =>
c.target === 'save' && c.source === 'processUser' && c.targetFilePath.includes('Models.swift'),
);
expect(saveCall).toBeDefined();
});
});

View file

@ -854,9 +854,8 @@ describe('TypeScript nullable receiver resolution (optional chaining)', () => {
// ---------------------------------------------------------------------------
// Return type inference: const user = getUser('alice'); user.save()
// TS/JS has no CONSTRUCTOR_BINDING_SCANNER for plain function calls yet,
// so return type inference does NOT work end-to-end. These tests document
// the current state and will activate once a TS scanner is added.
// The TS/JS CONSTRUCTOR_BINDING_SCANNER captures variable_declarator nodes
// with plain call_expression values, enabling end-to-end return type inference.
// ---------------------------------------------------------------------------
describe('TypeScript return type inference via explicit function return type', () => {

View file

@ -877,12 +877,11 @@ class RepoService {
expect(flatGet(env, 'user')).toBe('BaseUser');
});
it('does not infer from namespaced constructor (known limitation)', () => {
// extractSimpleTypeName only handles simple identifiers, not member expressions
it('infers from namespaced constructor: new ns.Service()', () => {
// extractSimpleTypeName handles member_expression via property_identifier
const tree = parse('const svc = new ns.Service();', TypeScript.typescript);
const { env } = buildTypeEnv(tree, 'typescript');
// member_expression as constructor → extractSimpleTypeName returns undefined
expect(flatGet(env, 'svc')).toBeUndefined();
expect(flatGet(env, 'svc')).toBe('Service');
});
it('infers type from new expression with as cast', () => {
@ -1744,5 +1743,56 @@ svc = App::Models::Service.new
expect(constructorBindings.length).toBe(1);
expect(constructorBindings[0].scope).toMatch(/^process@\d+$/);
});
it('returns constructor bindings for TypeScript const user = getUser()', () => {
const tree = parse('const user = getUser();', TypeScript.typescript);
const { env, constructorBindings } = buildTypeEnv(tree, 'typescript');
expect(flatGet(env, 'user')).toBeUndefined();
expect(constructorBindings.length).toBe(1);
expect(constructorBindings[0].varName).toBe('user');
expect(constructorBindings[0].calleeName).toBe('getUser');
});
it('does NOT emit constructor binding when TypeScript var has explicit type annotation', () => {
const tree = parse('const user: User = getUser();', TypeScript.typescript);
const { env, constructorBindings } = buildTypeEnv(tree, 'typescript');
expect(flatGet(env, 'user')).toBe('User');
expect(constructorBindings.find(b => b.varName === 'user')).toBeUndefined();
});
it('skips destructuring patterns (array_pattern) for TypeScript', () => {
const tree = parse('const [a, b] = getPair();', TypeScript.typescript);
const { constructorBindings } = buildTypeEnv(tree, 'typescript');
expect(constructorBindings).toEqual([]);
});
it('skips destructuring patterns (object_pattern) for TypeScript', () => {
const tree = parse('const { name, age } = getUser();', TypeScript.typescript);
const { constructorBindings } = buildTypeEnv(tree, 'typescript');
expect(constructorBindings).toEqual([]);
});
it('unwraps await in TypeScript: const user = await fetchUser()', () => {
const tree = parse('async function f() { const user = await fetchUser(); }', TypeScript.typescript);
const { constructorBindings } = buildTypeEnv(tree, 'typescript');
expect(constructorBindings.length).toBe(1);
expect(constructorBindings[0].varName).toBe('user');
expect(constructorBindings[0].calleeName).toBe('fetchUser');
});
it('handles qualified callee in TypeScript: const user = repo.getUser()', () => {
const tree = parse('const user = repo.getUser();', TypeScript.typescript);
const { constructorBindings } = buildTypeEnv(tree, 'typescript');
expect(constructorBindings.length).toBe(1);
expect(constructorBindings[0].varName).toBe('user');
expect(constructorBindings[0].calleeName).toBe('getUser');
});
it('does not emit binding for TypeScript new expression (handled by extractInitializer)', () => {
const tree = parse('const user = new User();', TypeScript.typescript);
const { env, constructorBindings } = buildTypeEnv(tree, 'typescript');
expect(flatGet(env, 'user')).toBe('User');
expect(constructorBindings.find(b => b.varName === 'user')).toBeUndefined();
});
});
});