diff --git a/gitnexus/src/core/ingestion/call-processor.ts b/gitnexus/src/core/ingestion/call-processor.ts index fd87de939..71a3ef8c4 100644 --- a/gitnexus/src/core/ingestion/call-processor.ts +++ b/gitnexus/src/core/ingestion/call-processor.ts @@ -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> = { + 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; diff --git a/gitnexus/src/core/ingestion/tree-sitter-queries.ts b/gitnexus/src/core/ingestion/tree-sitter-queries.ts index 2dc6cb546..f2b7c9c6e 100644 --- a/gitnexus/src/core/ingestion/tree-sitter-queries.ts +++ b/gitnexus/src/core/ingestion/tree-sitter-queries.ts @@ -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 diff --git a/gitnexus/src/core/ingestion/type-extractors/shared.ts b/gitnexus/src/core/ingestion/type-extractors/shared.ts index e04d1f2a0..12a0f4f80 100644 --- a/gitnexus/src/core/ingestion/type-extractors/shared.ts +++ b/gitnexus/src/core/ingestion/type-extractors/shared.ts @@ -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; } diff --git a/gitnexus/src/core/ingestion/utils.ts b/gitnexus/src/core/ingestion/utils.ts index 699ff47f4..77ead396f 100644 --- a/gitnexus/src/core/ingestion/utils.ts +++ b/gitnexus/src/core/ingestion/utils.ts @@ -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++) { diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-overload-param-types/service.cpp b/gitnexus/test/fixtures/lang-resolution/cpp-overload-param-types/service.cpp new file mode 100644 index 000000000..b65fdacfa --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-overload-param-types/service.cpp @@ -0,0 +1,19 @@ +#include + +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) + } +}; diff --git a/gitnexus/test/fixtures/lang-resolution/csharp-overload-param-types/Models/UserService.cs b/gitnexus/test/fixtures/lang-resolution/csharp-overload-param-types/Models/UserService.cs new file mode 100644 index 000000000..c03b367d5 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/csharp-overload-param-types/Models/UserService.cs @@ -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) + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/java-overload-param-types/models/User.java b/gitnexus/test/fixtures/lang-resolution/java-overload-param-types/models/User.java new file mode 100644 index 000000000..c45c00c5f --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/java-overload-param-types/models/User.java @@ -0,0 +1,7 @@ +package models; + +public class User { + public String getName() { + return "user"; + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/java-overload-param-types/models/UserService.java b/gitnexus/test/fixtures/lang-resolution/java-overload-param-types/models/UserService.java new file mode 100644 index 000000000..b9c022017 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/java-overload-param-types/models/UserService.java @@ -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) + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/java-virtual-dispatch/models/App.java b/gitnexus/test/fixtures/lang-resolution/java-virtual-dispatch/models/App.java new file mode 100644 index 000000000..1e82b84c2 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/java-virtual-dispatch/models/App.java @@ -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 + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/kotlin-overload-param-types/services/UserService.kt b/gitnexus/test/fixtures/lang-resolution/kotlin-overload-param-types/services/UserService.kt new file mode 100644 index 000000000..d81744524 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/kotlin-overload-param-types/services/UserService.kt @@ -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) + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/ts-virtual-dispatch/src/app.ts b/gitnexus/test/fixtures/lang-resolution/ts-virtual-dispatch/src/app.ts new file mode 100644 index 000000000..fe1df20ee --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/ts-virtual-dispatch/src/app.ts @@ -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 +} diff --git a/gitnexus/test/integration/resolvers/cpp.test.ts b/gitnexus/test/integration/resolvers/cpp.test.ts index d40f1f15e..9e2536103 100644 --- a/gitnexus/test/integration/resolvers/cpp.test.ts +++ b/gitnexus/test/integration/resolvers/cpp.test.ts @@ -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); + }); +}); diff --git a/gitnexus/test/integration/resolvers/csharp.test.ts b/gitnexus/test/integration/resolvers/csharp.test.ts index 049590524..5dbdb944f 100644 --- a/gitnexus/test/integration/resolvers/csharp.test.ts +++ b/gitnexus/test/integration/resolvers/csharp.test.ts @@ -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); + }); +}); diff --git a/gitnexus/test/integration/resolvers/java.test.ts b/gitnexus/test/integration/resolvers/java.test.ts index 78e3d309a..d9dbc98df 100644 --- a/gitnexus/test/integration/resolvers/java.test.ts +++ b/gitnexus/test/integration/resolvers/java.test.ts @@ -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); + }); +}); diff --git a/gitnexus/test/integration/resolvers/kotlin.test.ts b/gitnexus/test/integration/resolvers/kotlin.test.ts index 4db02d4dd..49e594f36 100644 --- a/gitnexus/test/integration/resolvers/kotlin.test.ts +++ b/gitnexus/test/integration/resolvers/kotlin.test.ts @@ -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); + }); +}); diff --git a/gitnexus/test/integration/resolvers/typescript.test.ts b/gitnexus/test/integration/resolvers/typescript.test.ts index 6352d4024..3bb205874 100644 --- a/gitnexus/test/integration/resolvers/typescript.test.ts +++ b/gitnexus/test/integration/resolvers/typescript.test.ts @@ -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); + }); +}); diff --git a/gitnexus/test/unit/method-signature.test.ts b/gitnexus/test/unit/method-signature.test.ts index ccd03b4ba..b6190c742 100644 --- a/gitnexus/test/unit/method-signature.test.ts +++ b/gitnexus/test/unit/method-signature.test.ts @@ -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 { diff --git a/gitnexus/test/unit/type-env.test.ts b/gitnexus/test/unit/type-env.test.ts index ce362c7a8..78f577e5f 100644 --- a/gitnexus/test/unit/type-env.test.ts +++ b/gitnexus/test/unit/type-env.test.ts @@ -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', () => {