mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-09 03:17:54 +00:00
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:
parent
55c13fc0e8
commit
dbf37cd572
28 changed files with 661 additions and 18 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -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
|
||||
|
|
@ -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 };
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
10
gitnexus/test/fixtures/lang-resolution/cpp-return-type/app.cpp
vendored
Normal file
10
gitnexus/test/fixtures/lang-resolution/cpp-return-type/app.cpp
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
#include "user.h"
|
||||
|
||||
User getUser(const char* name) {
|
||||
return User(name);
|
||||
}
|
||||
|
||||
void processUser() {
|
||||
auto user = getUser("alice");
|
||||
user.save();
|
||||
}
|
||||
11
gitnexus/test/fixtures/lang-resolution/cpp-return-type/user.h
vendored
Normal file
11
gitnexus/test/fixtures/lang-resolution/cpp-return-type/user.h
vendored
Normal 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);
|
||||
24
gitnexus/test/fixtures/lang-resolution/csharp-return-type/Models/User.cs
vendored
Normal file
24
gitnexus/test/fixtures/lang-resolution/csharp-return-type/Models/User.cs
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
13
gitnexus/test/fixtures/lang-resolution/csharp-return-type/Services/App.cs
vendored
Normal file
13
gitnexus/test/fixtures/lang-resolution/csharp-return-type/Services/App.cs
vendored
Normal 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();
|
||||
}
|
||||
}
|
||||
9
gitnexus/test/fixtures/lang-resolution/kotlin-return-type/models/User.kt
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/kotlin-return-type/models/User.kt
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package models
|
||||
|
||||
class User(val name: String) {
|
||||
fun save() {}
|
||||
}
|
||||
|
||||
fun getUser(name: String): User {
|
||||
return User(name)
|
||||
}
|
||||
8
gitnexus/test/fixtures/lang-resolution/kotlin-return-type/services/App.kt
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/kotlin-return-type/services/App.kt
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
package services
|
||||
|
||||
import models.getUser
|
||||
|
||||
fun processUser() {
|
||||
val user = getUser("alice")
|
||||
user.save()
|
||||
}
|
||||
15
gitnexus/test/fixtures/lang-resolution/php-return-type/app/Models/User.php
vendored
Normal file
15
gitnexus/test/fixtures/lang-resolution/php-return-type/app/Models/User.php
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
16
gitnexus/test/fixtures/lang-resolution/php-return-type/app/Services/UserService.php
vendored
Normal file
16
gitnexus/test/fixtures/lang-resolution/php-return-type/app/Services/UserService.php
vendored
Normal 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();
|
||||
}
|
||||
}
|
||||
6
gitnexus/test/fixtures/lang-resolution/ruby-return-type/app.rb
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/ruby-return-type/app.rb
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
require_relative 'models'
|
||||
|
||||
def process_user
|
||||
user = get_user('alice')
|
||||
user.save
|
||||
end
|
||||
13
gitnexus/test/fixtures/lang-resolution/ruby-return-type/models.rb
vendored
Normal file
13
gitnexus/test/fixtures/lang-resolution/ruby-return-type/models.rb
vendored
Normal 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
|
||||
7
gitnexus/test/fixtures/lang-resolution/rust-return-type/src/main.rs
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/rust-return-type/src/main.rs
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
mod models;
|
||||
use crate::models::get_user;
|
||||
|
||||
fn main() {
|
||||
let user = get_user("alice");
|
||||
user.save();
|
||||
}
|
||||
11
gitnexus/test/fixtures/lang-resolution/rust-return-type/src/models.rs
vendored
Normal file
11
gitnexus/test/fixtures/lang-resolution/rust-return-type/src/models.rs
vendored
Normal 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() }
|
||||
}
|
||||
4
gitnexus/test/fixtures/lang-resolution/swift-return-type/App.swift
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/swift-return-type/App.swift
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
func processUser() {
|
||||
let user = getUser(name: "alice")
|
||||
user.save()
|
||||
}
|
||||
13
gitnexus/test/fixtures/lang-resolution/swift-return-type/Models.swift
vendored
Normal file
13
gitnexus/test/fixtures/lang-resolution/swift-return-type/Models.swift
vendored
Normal 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)
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue