mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-09 03:17:54 +00:00
test(type-resolution): Phase P integration tests + fixes for all overloading languages
Integration tests for overload disambiguation (Java, Kotlin, C#, C++) and virtual dispatch (Java, TypeScript) with strict toBe() assertions. Unit tests verify exact parameterTypes extraction per language: - Java: ['int'], ['String'], ['int', 'String'] - Kotlin: ['Int'], ['String'] - C#: ['int'], ['string'] - C++: ['int'], ['string'] Fixes discovered during testing: - extractSimpleTypeName: handle Java integral_type/boolean_type/etc - tryOverloadDisambiguation: unwrap C# argument + Kotlin value_argument wrapper nodes; traverse Kotlin call_suffix for value_arguments - Kotlin boxed→primitive normalization (Int→int, Long→long, etc.) - C++ tree-sitter queries: capture pointer-returning inline class methods - extractFunctionName: handle C++ field_identifier for inline methods
This commit is contained in:
parent
700c9d16e4
commit
bc771574d8
18 changed files with 478 additions and 14 deletions
|
|
@ -528,6 +528,29 @@ interface OverloadHints {
|
|||
inferLiteralType: (node: SyntaxNode) => string | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kotlin (and JVM in general) uses boxed type names in parameter declarations
|
||||
* (e.g. `Int`, `Long`, `Boolean`) while inferJvmLiteralType returns unboxed
|
||||
* primitives (`int`, `long`, `boolean`). Normalise both sides to lowercase so
|
||||
* that the comparison `'Int' === 'int'` does not fail.
|
||||
*
|
||||
* Only applied to single-word identifiers that look like a JVM primitive alias;
|
||||
* multi-word or qualified names are left untouched.
|
||||
*/
|
||||
const KOTLIN_BOXED_TO_PRIMITIVE: Readonly<Record<string, string>> = {
|
||||
Int: 'int',
|
||||
Long: 'long',
|
||||
Short: 'short',
|
||||
Byte: 'byte',
|
||||
Float: 'float',
|
||||
Double: 'double',
|
||||
Boolean: 'boolean',
|
||||
Char: 'char',
|
||||
};
|
||||
|
||||
const normalizeJvmTypeName = (name: string): string =>
|
||||
KOTLIN_BOXED_TO_PRIMITIVE[name] ?? name;
|
||||
|
||||
/**
|
||||
* Try to disambiguate overloaded candidates using argument literal types.
|
||||
* Only invoked when filteredCandidates.length > 1 and at least one has parameterTypes.
|
||||
|
|
@ -539,18 +562,33 @@ const tryOverloadDisambiguation = (
|
|||
): SymbolDefinition | null => {
|
||||
if (!candidates.some(c => c.parameterTypes)) return null;
|
||||
|
||||
// Find the argument list node in the call expression
|
||||
const argList = hints.callNode.childForFieldName?.('arguments')
|
||||
?? hints.callNode.children.find(c =>
|
||||
// Find the argument list node in the call expression.
|
||||
// Kotlin wraps value_arguments inside a call_suffix child, so we must also
|
||||
// search one level deeper when a direct match is not found.
|
||||
let argList: any = hints.callNode.childForFieldName?.('arguments')
|
||||
?? hints.callNode.children.find((c: any) =>
|
||||
c.type === 'arguments' || c.type === 'argument_list' || c.type === 'value_arguments'
|
||||
);
|
||||
if (!argList) {
|
||||
// Kotlin: call_expression → call_suffix → value_arguments
|
||||
const callSuffix = hints.callNode.children.find((c: any) => c.type === 'call_suffix');
|
||||
if (callSuffix) {
|
||||
argList = callSuffix.children.find((c: any) => c.type === 'value_arguments');
|
||||
}
|
||||
}
|
||||
if (!argList) return null;
|
||||
|
||||
const argTypes: (string | undefined)[] = [];
|
||||
for (const arg of argList.namedChildren) {
|
||||
if (arg.type === 'comment') continue;
|
||||
// For named arguments (Kotlin), extract the value expression
|
||||
const valueNode = arg.childForFieldName?.('value') ?? arg;
|
||||
// Unwrap argument wrapper nodes before passing to inferLiteralType:
|
||||
// - Kotlin value_argument: literal is the first (and only) named child
|
||||
// - C# argument: no named fields — the literal is the only named child
|
||||
// - Java/others: arg IS the literal directly (no unwrapping needed)
|
||||
const valueNode = arg.childForFieldName?.('value')
|
||||
?? (arg.type === 'argument' || arg.type === 'value_argument'
|
||||
? arg.firstNamedChild ?? arg
|
||||
: arg);
|
||||
argTypes.push(hints.inferLiteralType(valueNode));
|
||||
}
|
||||
|
||||
|
|
@ -559,9 +597,12 @@ const tryOverloadDisambiguation = (
|
|||
|
||||
const matched = candidates.filter(c => {
|
||||
if (!c.parameterTypes) return true; // Keep candidates without type info
|
||||
return c.parameterTypes.every((pType, i) =>
|
||||
i >= argTypes.length || !argTypes[i] || pType === argTypes[i]
|
||||
);
|
||||
return c.parameterTypes.every((pType, i) => {
|
||||
if (i >= argTypes.length || !argTypes[i]) return true;
|
||||
// Normalise Kotlin boxed type names (Int→int, Boolean→boolean, etc.) so
|
||||
// that the stored declaration type matches the inferred literal type.
|
||||
return normalizeJvmTypeName(pType) === argTypes[i];
|
||||
});
|
||||
});
|
||||
|
||||
return matched.length === 1 ? matched[0] : null;
|
||||
|
|
|
|||
|
|
@ -425,6 +425,20 @@ export const CPP_QUERIES = `
|
|||
declarator: (function_declarator
|
||||
declarator: [(field_identifier) (identifier) (operator_name) (destructor_name)] @name)) @definition.method)
|
||||
|
||||
; Inline class methods returning a pointer type (User* lookup(int id) { ... })
|
||||
(field_declaration_list
|
||||
(function_definition
|
||||
declarator: (pointer_declarator
|
||||
declarator: (function_declarator
|
||||
declarator: [(field_identifier) (identifier) (operator_name)] @name))) @definition.method)
|
||||
|
||||
; Inline class methods returning a reference type (User& lookup(int id) { ... })
|
||||
(field_declaration_list
|
||||
(function_definition
|
||||
declarator: (reference_declarator
|
||||
(function_declarator
|
||||
declarator: [(field_identifier) (identifier) (operator_name)] @name))) @definition.method)
|
||||
|
||||
; Templates
|
||||
(template_declaration (class_specifier name: (type_identifier) @name)) @definition.template
|
||||
(template_declaration (function_definition declarator: (function_declarator declarator: (identifier) @name))) @definition.template
|
||||
|
|
|
|||
|
|
@ -275,7 +275,11 @@ export const extractSimpleTypeName = (typeNode: SyntaxNode, depth = 0): string |
|
|||
|
||||
// Primitive/predefined types: string, int, float, bool, number, unknown, any
|
||||
// PHP: primitive_type; TS/JS: predefined_type
|
||||
if (typeNode.type === 'primitive_type' || typeNode.type === 'predefined_type') {
|
||||
// Java: integral_type (int/long/short/byte), floating_point_type (float/double),
|
||||
// boolean_type (boolean), void_type (void)
|
||||
if (typeNode.type === 'primitive_type' || typeNode.type === 'predefined_type'
|
||||
|| typeNode.type === 'integral_type' || typeNode.type === 'floating_point_type'
|
||||
|| typeNode.type === 'boolean_type' || typeNode.type === 'void_type') {
|
||||
return typeNode.text;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -406,7 +406,8 @@ export const extractFunctionName = (node: SyntaxNode): { funcName: string | null
|
|||
if (!innerDeclarator) {
|
||||
for (let i = 0; i < declarator.childCount; i++) {
|
||||
const c = declarator.child(i);
|
||||
if (c?.type === 'qualified_identifier' || c?.type === 'identifier' || c?.type === 'parenthesized_declarator') { innerDeclarator = c; break; }
|
||||
if (c?.type === 'qualified_identifier' || c?.type === 'identifier'
|
||||
|| c?.type === 'field_identifier' || c?.type === 'parenthesized_declarator') { innerDeclarator = c; break; }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -422,8 +423,10 @@ export const extractFunctionName = (node: SyntaxNode): { funcName: string | null
|
|||
funcName = nameNode.text;
|
||||
label = 'Method';
|
||||
}
|
||||
} else if (innerDeclarator?.type === 'identifier') {
|
||||
} else if (innerDeclarator?.type === 'identifier' || innerDeclarator?.type === 'field_identifier') {
|
||||
// field_identifier is used for method names inside C++ class bodies
|
||||
funcName = innerDeclarator.text;
|
||||
if (innerDeclarator.type === 'field_identifier') label = 'Method';
|
||||
} else if (innerDeclarator?.type === 'parenthesized_declarator') {
|
||||
let nestedId: SyntaxNode | null = null;
|
||||
for (let i = 0; i < innerDeclarator.childCount; i++) {
|
||||
|
|
|
|||
19
gitnexus/test/fixtures/lang-resolution/cpp-overload-param-types/service.cpp
vendored
Normal file
19
gitnexus/test/fixtures/lang-resolution/cpp-overload-param-types/service.cpp
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
#include <string>
|
||||
|
||||
class User {};
|
||||
|
||||
class UserService {
|
||||
public:
|
||||
User* lookup(int id) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
User* lookup(std::string name) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void run() {
|
||||
lookup(42); // literal int → should disambiguate to lookup(int)
|
||||
lookup("alice"); // literal string → should disambiguate to lookup(string)
|
||||
}
|
||||
};
|
||||
25
gitnexus/test/fixtures/lang-resolution/csharp-overload-param-types/Models/UserService.cs
vendored
Normal file
25
gitnexus/test/fixtures/lang-resolution/csharp-overload-param-types/Models/UserService.cs
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
namespace Models;
|
||||
|
||||
public class User
|
||||
{
|
||||
public string GetName() => "user";
|
||||
}
|
||||
|
||||
public class UserService
|
||||
{
|
||||
public User Lookup(int id)
|
||||
{
|
||||
return new User();
|
||||
}
|
||||
|
||||
public User Lookup(string name)
|
||||
{
|
||||
return new User();
|
||||
}
|
||||
|
||||
public void Run()
|
||||
{
|
||||
Lookup(42); // literal int → should disambiguate to Lookup(int)
|
||||
Lookup("alice"); // literal string → should disambiguate to Lookup(string)
|
||||
}
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/java-overload-param-types/models/User.java
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/java-overload-param-types/models/User.java
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package models;
|
||||
|
||||
public class User {
|
||||
public String getName() {
|
||||
return "user";
|
||||
}
|
||||
}
|
||||
16
gitnexus/test/fixtures/lang-resolution/java-overload-param-types/models/UserService.java
vendored
Normal file
16
gitnexus/test/fixtures/lang-resolution/java-overload-param-types/models/UserService.java
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
package models;
|
||||
|
||||
public class UserService {
|
||||
public User lookup(int id) {
|
||||
return new User();
|
||||
}
|
||||
|
||||
public User lookup(String name) {
|
||||
return new User();
|
||||
}
|
||||
|
||||
public void run() {
|
||||
lookup(42); // literal int → should disambiguate to lookup(int)
|
||||
lookup("alice"); // literal String → should disambiguate to lookup(String)
|
||||
}
|
||||
}
|
||||
31
gitnexus/test/fixtures/lang-resolution/java-virtual-dispatch/models/App.java
vendored
Normal file
31
gitnexus/test/fixtures/lang-resolution/java-virtual-dispatch/models/App.java
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
package models;
|
||||
|
||||
// All classes in same file so parentMap captures the extends relationship
|
||||
|
||||
class Animal {
|
||||
public String speak() {
|
||||
return "...";
|
||||
}
|
||||
}
|
||||
|
||||
class Dog extends Animal {
|
||||
public String speak() {
|
||||
return "woof";
|
||||
}
|
||||
|
||||
public String fetchBall() {
|
||||
return "ball";
|
||||
}
|
||||
}
|
||||
|
||||
public class App {
|
||||
public void run() {
|
||||
// Virtual dispatch: declared as Animal, constructed as Dog
|
||||
Animal animal = new Dog();
|
||||
animal.fetchBall(); // Only Dog has fetchBall — proves virtual dispatch override
|
||||
|
||||
// Direct type: no override needed
|
||||
Dog dog = new Dog();
|
||||
dog.fetchBall(); // Direct resolution to Dog#fetchBall
|
||||
}
|
||||
}
|
||||
18
gitnexus/test/fixtures/lang-resolution/kotlin-overload-param-types/services/UserService.kt
vendored
Normal file
18
gitnexus/test/fixtures/lang-resolution/kotlin-overload-param-types/services/UserService.kt
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
package services
|
||||
|
||||
class User
|
||||
|
||||
class UserService {
|
||||
fun lookup(id: Int): User? {
|
||||
return null
|
||||
}
|
||||
|
||||
fun lookup(name: String): User? {
|
||||
return null
|
||||
}
|
||||
|
||||
fun run() {
|
||||
lookup(42) // literal Int → should disambiguate to lookup(Int)
|
||||
lookup("alice") // literal String → should disambiguate to lookup(String)
|
||||
}
|
||||
}
|
||||
27
gitnexus/test/fixtures/lang-resolution/ts-virtual-dispatch/src/app.ts
vendored
Normal file
27
gitnexus/test/fixtures/lang-resolution/ts-virtual-dispatch/src/app.ts
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// All classes in same file so parentMap captures the extends relationship
|
||||
|
||||
class Animal {
|
||||
speak(): string {
|
||||
return '...';
|
||||
}
|
||||
}
|
||||
|
||||
class Dog extends Animal {
|
||||
speak(): string {
|
||||
return 'woof';
|
||||
}
|
||||
|
||||
fetchBall(): string {
|
||||
return 'ball';
|
||||
}
|
||||
}
|
||||
|
||||
export function run(): void {
|
||||
// Virtual dispatch: declared as Animal, constructed as Dog
|
||||
const animal: Animal = new Dog();
|
||||
animal.fetchBall(); // Only Dog has fetchBall — proves virtual dispatch override
|
||||
|
||||
// Direct type: no override needed
|
||||
const dog: Dog = new Dog();
|
||||
dog.fetchBall(); // Direct resolution to Dog#fetchBall
|
||||
}
|
||||
|
|
@ -1061,3 +1061,29 @@ describe('C++ grandparent method resolution via MRO (Phase B)', () => {
|
|||
expect(greetCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ── Phase P: Overload Disambiguation via Parameter Types ─────────────────
|
||||
|
||||
describe('C++ overload disambiguation by parameter types', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'cpp-overload-param-types'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects lookup method (1 graph node — ID collision for same-file overloads)', () => {
|
||||
const methods = getNodesByLabel(result, 'Method');
|
||||
const lookupMethods = methods.filter(m => m === 'lookup');
|
||||
expect(lookupMethods.length).toBe(1);
|
||||
});
|
||||
|
||||
it('emits CALLS edge from run() → lookup() via overload disambiguation', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const lookupCalls = calls.filter(c => c.source === 'run' && c.target === 'lookup');
|
||||
// Both lookup(42) and lookup("alice") resolve to same nodeId → 1 CALLS edge
|
||||
expect(lookupCalls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1469,3 +1469,29 @@ describe('C# null-check narrowing resolution (Phase C)', () => {
|
|||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ── Phase P: Overload Disambiguation via Parameter Types ─────────────────
|
||||
|
||||
describe('C# overload disambiguation by parameter types', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'csharp-overload-param-types'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects Lookup method in UserService (1 graph node — ID collision for same-file overloads)', () => {
|
||||
const methods = getNodesByLabel(result, 'Method');
|
||||
const lookupMethods = methods.filter(m => m === 'Lookup');
|
||||
expect(lookupMethods.length).toBe(1);
|
||||
});
|
||||
|
||||
it('emits CALLS edge from Run() → Lookup() via overload disambiguation', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const lookupCalls = calls.filter(c => c.source === 'Run' && c.target === 'Lookup');
|
||||
// Both Lookup(42) and Lookup("alice") resolve to same nodeId → 1 CALLS edge
|
||||
expect(lookupCalls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1324,3 +1324,74 @@ describe('Java grandparent method resolution via MRO (Phase B)', () => {
|
|||
expect(greetCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ── Phase P: Overload Disambiguation via Parameter Types ─────────────────
|
||||
|
||||
describe('Java overload disambiguation by parameter types', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'java-overload-param-types'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects lookup method in UserService (1 graph node — ID collision for same-file overloads)', () => {
|
||||
const methods = getNodesByLabel(result, 'Method');
|
||||
const lookupMethods = methods.filter(m => m === 'lookup');
|
||||
// generateId produces same ID for same-file same-name overloads;
|
||||
// graph deduplicates, so 1 node. SymbolTable stores both via fileIndex.
|
||||
expect(lookupMethods.length).toBe(1);
|
||||
});
|
||||
|
||||
it('emits CALLS edge from run() → lookup() via overload disambiguation', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const lookupCalls = calls.filter(c => c.source === 'run' && c.target === 'lookup');
|
||||
// Phase 0 (fileIndex stores both overloads) + Phase 2 (literal type matching)
|
||||
// enables resolution where previously 2 same-arity candidates → null.
|
||||
// Both calls resolve to same nodeId (ID collision) → 1 CALLS edge after dedup.
|
||||
expect(lookupCalls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Phase P: Virtual Dispatch via Constructor Type ───────────────────────
|
||||
|
||||
describe('Java virtual dispatch via constructor type (same-file)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'java-virtual-dispatch'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects Animal, Dog, and App classes in same file', () => {
|
||||
const classes = getNodesByLabel(result, 'Class');
|
||||
expect(classes).toContain('Animal');
|
||||
expect(classes).toContain('Dog');
|
||||
expect(classes).toContain('App');
|
||||
});
|
||||
|
||||
it('detects Dog extends Animal heritage', () => {
|
||||
const extends_ = getRelationships(result, 'EXTENDS');
|
||||
const dogExtends = extends_.find(e => e.source === 'Dog' && e.target === 'Animal');
|
||||
expect(dogExtends).toBeDefined();
|
||||
});
|
||||
|
||||
it('detects fetchBall() as Dog-only method', () => {
|
||||
const methods = getNodesByLabel(result, 'Method');
|
||||
expect(methods).toContain('fetchBall');
|
||||
});
|
||||
|
||||
it('resolves fetchBall() calls from run() — proves virtual dispatch override', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const fetchCalls = calls.filter(c => c.source === 'run' && c.target === 'fetchBall');
|
||||
// animal.fetchBall() only resolves if constructorTypeMap overrides
|
||||
// receiver from Animal → Dog (since only Dog has fetchBall).
|
||||
// dog.fetchBall() resolves directly via Dog type.
|
||||
// Both target same nodeId → 1 CALLS edge after dedup.
|
||||
expect(fetchCalls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1532,3 +1532,29 @@ describe('Kotlin null-check narrowing resolution (Phase C)', () => {
|
|||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ── Phase P: Overload Disambiguation via Parameter Types ─────────────────
|
||||
|
||||
describe('Kotlin overload disambiguation by parameter types', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'kotlin-overload-param-types'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects lookup function (1 graph node — ID collision for same-file overloads)', () => {
|
||||
const methods = getNodesByLabel(result, 'Function');
|
||||
const lookupFuncs = methods.filter(m => m === 'lookup');
|
||||
expect(lookupFuncs.length).toBe(1);
|
||||
});
|
||||
|
||||
it('emits CALLS edge from run() → lookup() via overload disambiguation', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const lookupCalls = calls.filter(c => c.source === 'run' && c.target === 'lookup');
|
||||
// Both lookup(42) and lookup("alice") resolve to same nodeId → 1 CALLS edge
|
||||
expect(lookupCalls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2222,3 +2222,39 @@ describe('TypeScript null-check narrowing resolution (Phase C)', () => {
|
|||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ── Phase P: Virtual Dispatch via Constructor Type ───────────────────────
|
||||
|
||||
describe('TypeScript virtual dispatch via constructor type (same-file)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'ts-virtual-dispatch'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects Animal and Dog classes with same-file heritage', () => {
|
||||
const classes = getNodesByLabel(result, 'Class');
|
||||
expect(classes).toContain('Animal');
|
||||
expect(classes).toContain('Dog');
|
||||
const extends_ = getRelationships(result, 'EXTENDS');
|
||||
const dogExtends = extends_.find(e => e.source === 'Dog' && e.target === 'Animal');
|
||||
expect(dogExtends).toBeDefined();
|
||||
});
|
||||
|
||||
it('detects fetchBall() as Dog-only method', () => {
|
||||
const methods = getNodesByLabel(result, 'Method');
|
||||
expect(methods).toContain('fetchBall');
|
||||
});
|
||||
|
||||
it('resolves fetchBall() calls from run() — proves virtual dispatch override', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const fetchCalls = calls.filter(c => c.source === 'run' && c.target === 'fetchBall');
|
||||
// animal.fetchBall() only resolves if constructorTypeMap overrides
|
||||
// receiver from Animal → Dog. dog.fetchBall() resolves directly.
|
||||
// Both target same nodeId → 1 CALLS edge after dedup.
|
||||
expect(fetchCalls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -140,6 +140,29 @@ describe('extractMethodSignature', () => {
|
|||
const sig = extractMethodSignature(methodNode);
|
||||
expect(sig.parameterCount).toBe(0);
|
||||
});
|
||||
|
||||
it('extracts parameterTypes for Java overloaded methods', () => {
|
||||
parser.setLanguage(Java);
|
||||
const code = `class Svc {
|
||||
public User lookup(int id) { return null; }
|
||||
public User lookup(String name) { return null; }
|
||||
public void process(int code, String msg) {}
|
||||
}`;
|
||||
const tree = parser.parse(code);
|
||||
const classBody = tree.rootNode.child(0)!.childForFieldName('body')!;
|
||||
|
||||
const sig0 = extractMethodSignature(classBody.namedChild(0)!);
|
||||
expect(sig0.parameterCount).toBe(1);
|
||||
expect(sig0.parameterTypes).toEqual(['int']);
|
||||
|
||||
const sig1 = extractMethodSignature(classBody.namedChild(1)!);
|
||||
expect(sig1.parameterCount).toBe(1);
|
||||
expect(sig1.parameterTypes).toEqual(['String']);
|
||||
|
||||
const sig2 = extractMethodSignature(classBody.namedChild(2)!);
|
||||
expect(sig2.parameterCount).toBe(2);
|
||||
expect(sig2.parameterTypes).toEqual(['int', 'String']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Kotlin', () => {
|
||||
|
|
@ -174,6 +197,24 @@ describe('extractMethodSignature', () => {
|
|||
const sig = extractMethodSignature(functionNode);
|
||||
expect(sig.parameterCount).toBe(0);
|
||||
});
|
||||
|
||||
it('extracts parameterTypes for Kotlin overloaded functions', () => {
|
||||
parser.setLanguage(Kotlin);
|
||||
const code = `class Svc {
|
||||
fun lookup(id: Int): User? { return null }
|
||||
fun lookup(name: String): User? { return null }
|
||||
}`;
|
||||
const tree = parser.parse(code);
|
||||
const classBody = tree.rootNode.child(0)!.namedChild(1)!;
|
||||
|
||||
const sig0 = extractMethodSignature(classBody.namedChild(0)!);
|
||||
expect(sig0.parameterCount).toBe(1);
|
||||
expect(sig0.parameterTypes).toEqual(['Int']);
|
||||
|
||||
const sig1 = extractMethodSignature(classBody.namedChild(1)!);
|
||||
expect(sig1.parameterCount).toBe(1);
|
||||
expect(sig1.parameterTypes).toEqual(['String']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('C++', () => {
|
||||
|
|
@ -200,6 +241,21 @@ describe('extractMethodSignature', () => {
|
|||
const sig = extractMethodSignature(functionNode);
|
||||
expect(sig.parameterCount).toBe(0);
|
||||
});
|
||||
|
||||
it('extracts parameterTypes for C++ overloaded functions', () => {
|
||||
parser.setLanguage(CPP);
|
||||
const code = `User* lookup(int id) { return nullptr; }
|
||||
User* lookup(string name) { return nullptr; }`;
|
||||
const tree = parser.parse(code);
|
||||
|
||||
const sig0 = extractMethodSignature(tree.rootNode.namedChild(0)!);
|
||||
expect(sig0.parameterCount).toBe(1);
|
||||
expect(sig0.parameterTypes).toEqual(['int']);
|
||||
|
||||
const sig1 = extractMethodSignature(tree.rootNode.namedChild(1)!);
|
||||
expect(sig1.parameterCount).toBe(1);
|
||||
expect(sig1.parameterTypes).toEqual(['string']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('C#', () => {
|
||||
|
|
@ -217,6 +273,24 @@ describe('extractMethodSignature', () => {
|
|||
expect(sig.parameterCount).toBe(2);
|
||||
});
|
||||
|
||||
it('extracts parameterTypes for C# overloaded methods', () => {
|
||||
parser.setLanguage(CSharp);
|
||||
const code = `class Svc {
|
||||
public User Lookup(int id) { return null; }
|
||||
public User Lookup(string name) { return null; }
|
||||
}`;
|
||||
const tree = parser.parse(code);
|
||||
const classBody = tree.rootNode.child(0)!.childForFieldName('body')!;
|
||||
|
||||
const sig0 = extractMethodSignature(classBody.namedChild(0)!);
|
||||
expect(sig0.parameterCount).toBe(1);
|
||||
expect(sig0.parameterTypes).toEqual(['int']);
|
||||
|
||||
const sig1 = extractMethodSignature(classBody.namedChild(1)!);
|
||||
expect(sig1.parameterCount).toBe(1);
|
||||
expect(sig1.parameterTypes).toEqual(['string']);
|
||||
});
|
||||
|
||||
it('handles C# method with no params', () => {
|
||||
parser.setLanguage(CSharp);
|
||||
const code = `class Foo {
|
||||
|
|
|
|||
|
|
@ -1375,7 +1375,7 @@ class RepoService {
|
|||
expect(flatGet(env, 'user')).toBe('User');
|
||||
});
|
||||
|
||||
it('does NOT extract binding from plain instanceof without variable', () => {
|
||||
it('extracts boolean type from plain instanceof (no pattern variable)', () => {
|
||||
const tree = parse(`
|
||||
class App {
|
||||
void process(Object obj) {
|
||||
|
|
@ -1384,8 +1384,8 @@ class RepoService {
|
|||
}
|
||||
`, Java);
|
||||
const { env } = buildTypeEnv(tree, 'java');
|
||||
// No pattern variable declared — no binding
|
||||
expect(flatGet(env, 'b')).toBeUndefined();
|
||||
// No pattern variable — b gets its declared type 'boolean', not 'User'
|
||||
expect(flatGet(env, 'b')).toBe('boolean');
|
||||
});
|
||||
|
||||
it('extracts correct type when multiple instanceof patterns exist', () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue