diff --git a/gitnexus/test/fixtures/lang-resolution/csharp-overload-dispatch/App.cs b/gitnexus/test/fixtures/lang-resolution/csharp-overload-dispatch/App.cs new file mode 100644 index 000000000..faaa9431d --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/csharp-overload-dispatch/App.cs @@ -0,0 +1,8 @@ +public class App { + public void Run() { + var repo = new SqlRepository(); + repo.Find(42); + repo.Find("alice", true); + repo.Save("test"); + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/csharp-overload-dispatch/IRepository.cs b/gitnexus/test/fixtures/lang-resolution/csharp-overload-dispatch/IRepository.cs new file mode 100644 index 000000000..35bc58c56 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/csharp-overload-dispatch/IRepository.cs @@ -0,0 +1,5 @@ +public interface IRepository { + string Find(int id); + string Find(string name, bool exact); + void Save(string data); +} diff --git a/gitnexus/test/fixtures/lang-resolution/csharp-overload-dispatch/SqlRepository.cs b/gitnexus/test/fixtures/lang-resolution/csharp-overload-dispatch/SqlRepository.cs new file mode 100644 index 000000000..aae4b8c4f --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/csharp-overload-dispatch/SqlRepository.cs @@ -0,0 +1,11 @@ +public class SqlRepository : IRepository { + public string Find(int id) { + return "found-by-id"; + } + public string Find(string name, bool exact) { + return "found-by-name"; + } + public void Save(string data) { + Console.WriteLine(data); + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/java-overload-dispatch/App.java b/gitnexus/test/fixtures/lang-resolution/java-overload-dispatch/App.java new file mode 100644 index 000000000..1da1ce3d7 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/java-overload-dispatch/App.java @@ -0,0 +1,8 @@ +public class App { + public void run() { + SqlRepository repo = new SqlRepository(); + repo.find(42); + repo.find("alice", true); + repo.save("test"); + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/java-overload-dispatch/Repository.java b/gitnexus/test/fixtures/lang-resolution/java-overload-dispatch/Repository.java new file mode 100644 index 000000000..b4e855473 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/java-overload-dispatch/Repository.java @@ -0,0 +1,5 @@ +public interface Repository { + String find(int id); + String find(String name, boolean exact); + void save(String data); +} diff --git a/gitnexus/test/fixtures/lang-resolution/java-overload-dispatch/SqlRepository.java b/gitnexus/test/fixtures/lang-resolution/java-overload-dispatch/SqlRepository.java new file mode 100644 index 000000000..315a8e0a6 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/java-overload-dispatch/SqlRepository.java @@ -0,0 +1,13 @@ +public class SqlRepository implements Repository { + public String find(int id) { + return "found-by-id"; + } + + public String find(String name, boolean exact) { + return "found-by-name"; + } + + public void save(String data) { + System.out.println(data); + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/kotlin-overload-dispatch/App.kt b/gitnexus/test/fixtures/lang-resolution/kotlin-overload-dispatch/App.kt new file mode 100644 index 000000000..13a655ea0 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/kotlin-overload-dispatch/App.kt @@ -0,0 +1,6 @@ +fun main() { + val repo = SqlRepository() + repo.find(42) + repo.find("alice", true) + repo.save("test") +} diff --git a/gitnexus/test/fixtures/lang-resolution/kotlin-overload-dispatch/Repository.kt b/gitnexus/test/fixtures/lang-resolution/kotlin-overload-dispatch/Repository.kt new file mode 100644 index 000000000..92b73e1b0 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/kotlin-overload-dispatch/Repository.kt @@ -0,0 +1,5 @@ +interface Repository { + fun find(id: Int): String + fun find(name: String, exact: Boolean): String + fun save(data: String) +} diff --git a/gitnexus/test/fixtures/lang-resolution/kotlin-overload-dispatch/SqlRepository.kt b/gitnexus/test/fixtures/lang-resolution/kotlin-overload-dispatch/SqlRepository.kt new file mode 100644 index 000000000..a562a8dc8 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/kotlin-overload-dispatch/SqlRepository.kt @@ -0,0 +1,5 @@ +class SqlRepository : Repository { + override fun find(id: Int): String = "found-by-id" + override fun find(name: String, exact: Boolean): String = "found-by-name" + override fun save(data: String) { println(data) } +} diff --git a/gitnexus/test/fixtures/lang-resolution/swift-overload-dispatch/App.swift b/gitnexus/test/fixtures/lang-resolution/swift-overload-dispatch/App.swift new file mode 100644 index 000000000..d57e328a8 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/swift-overload-dispatch/App.swift @@ -0,0 +1,4 @@ +let repo = SqlRepository() +repo.find(id: 42) +repo.find(name: "alice", exact: true) +repo.save(data: "test") diff --git a/gitnexus/test/fixtures/lang-resolution/swift-overload-dispatch/Repository.swift b/gitnexus/test/fixtures/lang-resolution/swift-overload-dispatch/Repository.swift new file mode 100644 index 000000000..fa735509a --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/swift-overload-dispatch/Repository.swift @@ -0,0 +1,5 @@ +protocol Repository { + func find(id: Int) -> String + func find(name: String, exact: Bool) -> String + func save(data: String) +} diff --git a/gitnexus/test/fixtures/lang-resolution/swift-overload-dispatch/SqlRepository.swift b/gitnexus/test/fixtures/lang-resolution/swift-overload-dispatch/SqlRepository.swift new file mode 100644 index 000000000..b6d2a1546 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/swift-overload-dispatch/SqlRepository.swift @@ -0,0 +1,5 @@ +class SqlRepository: Repository { + func find(id: Int) -> String { return "found-by-id" } + func find(name: String, exact: Bool) -> String { return "found-by-name" } + func save(data: String) { print(data) } +} diff --git a/gitnexus/test/fixtures/lang-resolution/ts-overload-dispatch/app.ts b/gitnexus/test/fixtures/lang-resolution/ts-overload-dispatch/app.ts new file mode 100644 index 000000000..e526de6a2 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/ts-overload-dispatch/app.ts @@ -0,0 +1,6 @@ +import { SqlRepository } from './sql-repository'; + +const repo = new SqlRepository(); +repo.find(42); +repo.find('alice'); +repo.save('test'); diff --git a/gitnexus/test/fixtures/lang-resolution/ts-overload-dispatch/repository.ts b/gitnexus/test/fixtures/lang-resolution/ts-overload-dispatch/repository.ts new file mode 100644 index 000000000..6ad14c0da --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/ts-overload-dispatch/repository.ts @@ -0,0 +1,5 @@ +export interface IRepository { + find(id: number): string; + find(name: string): string; + save(data: string): void; +} diff --git a/gitnexus/test/fixtures/lang-resolution/ts-overload-dispatch/sql-repository.ts b/gitnexus/test/fixtures/lang-resolution/ts-overload-dispatch/sql-repository.ts new file mode 100644 index 000000000..e937ee689 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/ts-overload-dispatch/sql-repository.ts @@ -0,0 +1,12 @@ +import { IRepository } from './repository'; + +export class SqlRepository implements IRepository { + find(id: number): string; + find(name: string): string; + find(arg: number | string): string { + return typeof arg === 'number' ? 'found-by-id' : 'found-by-name'; + } + save(data: string): void { + console.log(data); + } +} diff --git a/gitnexus/test/integration/resolvers/csharp.test.ts b/gitnexus/test/integration/resolvers/csharp.test.ts index 03cd1d9d9..0348f1eb6 100644 --- a/gitnexus/test/integration/resolvers/csharp.test.ts +++ b/gitnexus/test/integration/resolvers/csharp.test.ts @@ -1767,3 +1767,64 @@ describe('C# interface dispatch (METHOD_IMPLEMENTS)', () => { expect(saveEdge).toBeDefined(); }); }); + +// --------------------------------------------------------------------------- +// Overloaded method disambiguation: METHOD_IMPLEMENTS with overloads +// IRepository declares Find(int), Find(string), Save(string). +// SqlRepository implements all three. +// Overloaded methods (same name, different params) collapse into a single +// graph node (generateId drops startLine), so Find appears once per file. +// METHOD_IMPLEMENTS still emits one edge per unique (source, target) pair. +// --------------------------------------------------------------------------- + +describe('C# overloaded method disambiguation (METHOD_IMPLEMENTS)', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo(path.join(FIXTURES, 'csharp-overload-dispatch'), () => {}); + }, 60000); + + it('detects 2 distinct Find Method nodes on SqlRepository (different arities)', () => { + const methods = getNodesByLabelFull(result, 'Method'); + const findOnSql = methods.filter( + (m) => m.name === 'Find' && m.properties.filePath?.includes('SqlRepository'), + ); + expect(findOnSql.length).toBe(2); + }); + + it('emits METHOD_IMPLEMENTS edges for both Find overloads', () => { + const mi = getRelationships(result, 'METHOD_IMPLEMENTS'); + const findEdges = mi.filter( + (e) => + e.source === 'Find' && + e.target === 'Find' && + e.sourceFilePath.includes('SqlRepository') && + e.targetFilePath.includes('IRepository'), + ); + expect(findEdges.length).toBe(2); + }); + + it('emits METHOD_IMPLEMENTS for Save -> IRepository.Save', () => { + const mi = getRelationships(result, 'METHOD_IMPLEMENTS'); + const saveEdge = mi.find( + (e) => + e.source === 'Save' && + e.target === 'Save' && + e.sourceFilePath.includes('SqlRepository') && + e.targetFilePath.includes('IRepository'), + ); + expect(saveEdge).toBeDefined(); + }); + + it('emits exactly 3 METHOD_IMPLEMENTS edges', () => { + const mi = getRelationships(result, 'METHOD_IMPLEMENTS'); + expect(mi.length).toBe(3); + }); + + it('detects SqlRepository class and IRepository interface', () => { + const classes = getNodesByLabel(result, 'Class'); + const ifaces = getNodesByLabel(result, 'Interface'); + expect(classes).toContain('SqlRepository'); + expect(ifaces).toContain('IRepository'); + }); +}); diff --git a/gitnexus/test/integration/resolvers/java.test.ts b/gitnexus/test/integration/resolvers/java.test.ts index ff26fef19..05772819e 100644 --- a/gitnexus/test/integration/resolvers/java.test.ts +++ b/gitnexus/test/integration/resolvers/java.test.ts @@ -1701,3 +1701,115 @@ describe('Java interface dispatch (METHOD_IMPLEMENTS)', () => { expect(sourceFiles.some((f) => f.includes('SendEmail'))).toBe(true); }); }); + +// --------------------------------------------------------------------------- +// Java overloaded method disambiguation (METHOD_IMPLEMENTS with arity) +// Repository interface: find(int), find(String, boolean), save(String) +// SqlRepository implements Repository with matching overloads +// --------------------------------------------------------------------------- + +describe('Java overloaded method disambiguation (METHOD_IMPLEMENTS)', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo(path.join(FIXTURES, 'java-overload-dispatch'), () => {}); + }, 60000); + + it('detects distinct Method nodes for overloaded find methods on SqlRepository', () => { + const methods = getNodesByLabelFull(result, 'Method'); + const findMethods = methods.filter( + (m) => m.name === 'find' && m.properties.filePath?.includes('SqlRepository'), + ); + expect(findMethods.length).toBe(2); + const paramCounts = findMethods.map((m) => m.properties.parameterCount).sort(); + expect(paramCounts).toEqual([1, 2]); + }); + + it('detects distinct Method nodes for overloaded find methods on Repository interface', () => { + const methods = getNodesByLabelFull(result, 'Method'); + const findMethods = methods.filter( + (m) => + m.name === 'find' && + m.properties.filePath?.includes('Repository') && + !m.properties.filePath?.includes('SqlRepository'), + ); + expect(findMethods.length).toBe(2); + const paramCounts = findMethods.map((m) => m.properties.parameterCount).sort(); + expect(paramCounts).toEqual([1, 2]); + }); + + it('emits METHOD_IMPLEMENTS for find(int) → Repository.find(int)', () => { + const mi = getRelationships(result, 'METHOD_IMPLEMENTS'); + const edge = mi.find( + (e) => + e.source === 'find' && + e.target === 'find' && + e.sourceFilePath.includes('SqlRepository') && + e.targetFilePath.includes('Repository'), + ); + expect(edge).toBeDefined(); + // Verify at least one find→find edge has arity 1 on source side + const findEdges = mi.filter( + (e) => + e.source === 'find' && + e.target === 'find' && + e.sourceFilePath.includes('SqlRepository') && + e.targetFilePath.includes('Repository'), + ); + const sourceNodes = findEdges.map((e) => { + const methods = getNodesByLabelFull(result, 'Method'); + return methods.find( + (m) => + m.name === 'find' && + m.properties.filePath?.includes('SqlRepository') && + m.properties.parameterCount === 1, + ); + }); + expect(sourceNodes.some((n) => n !== undefined)).toBe(true); + }); + + it('emits METHOD_IMPLEMENTS for find(String, boolean) → Repository.find(String, boolean)', () => { + const mi = getRelationships(result, 'METHOD_IMPLEMENTS'); + const findEdges = mi.filter( + (e) => + e.source === 'find' && + e.target === 'find' && + e.sourceFilePath.includes('SqlRepository') && + e.targetFilePath.includes('Repository'), + ); + // There should be two find→find edges (one per overload) + expect(findEdges.length).toBe(2); + }); + + it('emits METHOD_IMPLEMENTS for save(String) → Repository.save(String)', () => { + const mi = getRelationships(result, 'METHOD_IMPLEMENTS'); + const edge = mi.find( + (e) => + e.source === 'save' && + e.target === 'save' && + e.sourceFilePath.includes('SqlRepository') && + e.targetFilePath.includes('Repository'), + ); + expect(edge).toBeDefined(); + }); + + it('emits exactly 3 METHOD_IMPLEMENTS edges', () => { + const mi = getRelationships(result, 'METHOD_IMPLEMENTS'); + const edges = mi.filter( + (e) => e.sourceFilePath.includes('SqlRepository') && e.targetFilePath.includes('Repository'), + ); + expect(edges.length).toBe(3); + }); + + it('emits CALLS edges from run() to both find overloads', () => { + const calls = getRelationships(result, 'CALLS'); + const findCalls = calls.filter( + (c) => + c.source === 'run' && + c.target === 'find' && + c.sourceFilePath.includes('App') && + c.targetFilePath.includes('SqlRepository'), + ); + expect(findCalls.length).toBe(2); + }); +}); diff --git a/gitnexus/test/integration/resolvers/kotlin.test.ts b/gitnexus/test/integration/resolvers/kotlin.test.ts index f91fc9eaa..361996527 100644 --- a/gitnexus/test/integration/resolvers/kotlin.test.ts +++ b/gitnexus/test/integration/resolvers/kotlin.test.ts @@ -1857,3 +1857,54 @@ describe('Kotlin interface dispatch (METHOD_IMPLEMENTS)', () => { expect(saveEdge).toBeDefined(); }); }); + +// --------------------------------------------------------------------------- +// Overloaded method disambiguation: interface with overloaded find + save, +// concrete class implements all three. Verifies METHOD_IMPLEMENTS edges +// correctly distinguish between overloaded signatures. +// --------------------------------------------------------------------------- + +describe('Kotlin overloaded method disambiguation', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo(path.join(FIXTURES, 'kotlin-overload-dispatch'), () => {}); + }, 60000); + + it('detects 2 distinct find Method nodes on SqlRepository', () => { + const methods = getNodesByLabelFull(result, 'Method'); + const sqlRepoFinds = methods.filter( + (m) => m.name === 'find' && m.properties.filePath?.includes('SqlRepository'), + ); + expect(sqlRepoFinds.length).toBe(2); + }); + + it('emits METHOD_IMPLEMENTS edges for both find overloads', () => { + const mi = getRelationships(result, 'METHOD_IMPLEMENTS'); + const findEdges = mi.filter( + (e) => + e.source === 'find' && + e.target === 'find' && + e.sourceFilePath.includes('SqlRepository') && + e.targetFilePath.includes('Repository'), + ); + expect(findEdges.length).toBe(2); + }); + + it('emits METHOD_IMPLEMENTS edge for save', () => { + const mi = getRelationships(result, 'METHOD_IMPLEMENTS'); + const saveEdge = mi.find( + (e) => + e.source === 'save' && + e.target === 'save' && + e.sourceFilePath.includes('SqlRepository') && + e.targetFilePath.includes('Repository'), + ); + expect(saveEdge).toBeDefined(); + }); + + it('emits exactly 3 METHOD_IMPLEMENTS edges total', () => { + const mi = getRelationships(result, 'METHOD_IMPLEMENTS'); + expect(mi.length).toBe(3); + }); +}); diff --git a/gitnexus/test/integration/resolvers/swift.test.ts b/gitnexus/test/integration/resolvers/swift.test.ts index 62fb75615..272770527 100644 --- a/gitnexus/test/integration/resolvers/swift.test.ts +++ b/gitnexus/test/integration/resolvers/swift.test.ts @@ -808,3 +808,60 @@ describe.skipIf(!swiftAvailable)('Swift abstract dispatch', () => { expect(names).toEqual(['find', 'save']); }); }); + +// --------------------------------------------------------------------------- +// Overloaded method disambiguation: protocol with overloaded find + save, +// concrete class implements all three. Verifies METHOD_IMPLEMENTS edges +// correctly distinguish between overloaded signatures. +// --------------------------------------------------------------------------- + +describe.skipIf(!swiftAvailable)('Swift overloaded method disambiguation', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo(path.join(FIXTURES, 'swift-overload-dispatch'), () => {}); + }, 60000); + + it('detects 2 distinct find Method nodes on SqlRepository', () => { + const methods = getNodesByLabelFull(result, 'Method'); + const sqlRepoFinds = methods.filter( + (m) => m.name === 'find' && m.properties.filePath?.includes('SqlRepository'), + ); + // Swift class methods may be emitted as Function nodes + const functions = getNodesByLabelFull(result, 'Function'); + const sqlRepoFindFns = functions.filter( + (m) => m.name === 'find' && m.properties.filePath?.includes('SqlRepository'), + ); + const totalFinds = sqlRepoFinds.length + sqlRepoFindFns.length; + expect(totalFinds).toBe(2); + }); + + it('emits METHOD_IMPLEMENTS edges for both find overloads', () => { + const mi = getRelationships(result, 'METHOD_IMPLEMENTS'); + const findEdges = mi.filter( + (e) => + e.source === 'find' && + e.target === 'find' && + e.sourceFilePath.includes('SqlRepository') && + e.targetFilePath.includes('Repository'), + ); + expect(findEdges.length).toBe(2); + }); + + it('emits METHOD_IMPLEMENTS edge for save', () => { + const mi = getRelationships(result, 'METHOD_IMPLEMENTS'); + const saveEdge = mi.find( + (e) => + e.source === 'save' && + e.target === 'save' && + e.sourceFilePath.includes('SqlRepository') && + e.targetFilePath.includes('Repository'), + ); + expect(saveEdge).toBeDefined(); + }); + + it('emits exactly 3 METHOD_IMPLEMENTS edges total', () => { + const mi = getRelationships(result, 'METHOD_IMPLEMENTS'); + expect(mi.length).toBe(3); + }); +}); diff --git a/gitnexus/test/integration/resolvers/typescript.test.ts b/gitnexus/test/integration/resolvers/typescript.test.ts index a59538dde..33a28f604 100644 --- a/gitnexus/test/integration/resolvers/typescript.test.ts +++ b/gitnexus/test/integration/resolvers/typescript.test.ts @@ -2443,3 +2443,56 @@ describe('TypeScript interface dispatch (METHOD_IMPLEMENTS)', () => { expect(saveEdge).toBeDefined(); }); }); + +// --------------------------------------------------------------------------- +// Overloaded method disambiguation: interface with overloaded find + save, +// concrete class implements all three. TypeScript overloads collapse to one +// implementation signature — expect the implementation body, not individual +// overload signatures. +// --------------------------------------------------------------------------- + +describe('TypeScript overloaded method disambiguation', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo(path.join(FIXTURES, 'ts-overload-dispatch'), () => {}); + }, 60000); + + it('emits METHOD_IMPLEMENTS edge for find', () => { + const mi = getRelationships(result, 'METHOD_IMPLEMENTS'); + const findEdge = mi.find( + (e) => + e.source === 'find' && + e.target === 'find' && + e.sourceFilePath.includes('sql-repository') && + e.targetFilePath.includes('repository'), + ); + expect(findEdge).toBeDefined(); + }); + + it('emits METHOD_IMPLEMENTS edge for save', () => { + const mi = getRelationships(result, 'METHOD_IMPLEMENTS'); + const saveEdge = mi.find( + (e) => + e.source === 'save' && + e.target === 'save' && + e.sourceFilePath.includes('sql-repository') && + e.targetFilePath.includes('repository'), + ); + expect(saveEdge).toBeDefined(); + }); + + it('TypeScript overloads collapse — find has one implementation METHOD_IMPLEMENTS edge', () => { + const mi = getRelationships(result, 'METHOD_IMPLEMENTS'); + // TypeScript overloads collapse to one implementation signature, + // so we expect a single METHOD_IMPLEMENTS edge for find (not two) + const findEdges = mi.filter( + (e) => + e.source === 'find' && + e.target === 'find' && + e.sourceFilePath.includes('sql-repository') && + e.targetFilePath.includes('repository'), + ); + expect(findEdges.length).toBe(1); + }); +});