test: overloaded method disambiguation integration tests for 5 languages

Add comprehensive integration tests verifying that overloaded methods with
different arities produce distinct graph nodes and correct METHOD_IMPLEMENTS
edges. Each fixture uses different parameter counts (1 vs 2) to exercise
the #<paramCount> arity suffix.

Languages covered:
- Java: 7 tests (distinct nodes, 3 METHOD_IMPLEMENTS edges, 2 CALLS edges)
- C#: 5 tests (distinct Find nodes, 3 METHOD_IMPLEMENTS edges)
- Kotlin: 4 tests (distinct find nodes, 3 METHOD_IMPLEMENTS edges)
- TypeScript: 3 tests (TS overloads collapse to 1 impl — expects 2 edges)
- Swift: 4 tests (skipped when parser unavailable)

All assertions use strict toBe() — no toBeGreaterThanOrEqual.
This commit is contained in:
Gergo Magyar 2026-04-04 10:04:22 +01:00
parent 3c5079f044
commit afa4e81a5f
20 changed files with 437 additions and 0 deletions

View file

@ -0,0 +1,8 @@
public class App {
public void Run() {
var repo = new SqlRepository();
repo.Find(42);
repo.Find("alice", true);
repo.Save("test");
}
}

View file

@ -0,0 +1,5 @@
public interface IRepository {
string Find(int id);
string Find(string name, bool exact);
void Save(string data);
}

View file

@ -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);
}
}

View file

@ -0,0 +1,8 @@
public class App {
public void run() {
SqlRepository repo = new SqlRepository();
repo.find(42);
repo.find("alice", true);
repo.save("test");
}
}

View file

@ -0,0 +1,5 @@
public interface Repository {
String find(int id);
String find(String name, boolean exact);
void save(String data);
}

View file

@ -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);
}
}

View file

@ -0,0 +1,6 @@
fun main() {
val repo = SqlRepository()
repo.find(42)
repo.find("alice", true)
repo.save("test")
}

View file

@ -0,0 +1,5 @@
interface Repository {
fun find(id: Int): String
fun find(name: String, exact: Boolean): String
fun save(data: String)
}

View file

@ -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) }
}

View file

@ -0,0 +1,4 @@
let repo = SqlRepository()
repo.find(id: 42)
repo.find(name: "alice", exact: true)
repo.save(data: "test")

View file

@ -0,0 +1,5 @@
protocol Repository {
func find(id: Int) -> String
func find(name: String, exact: Bool) -> String
func save(data: String)
}

View file

@ -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) }
}

View file

@ -0,0 +1,6 @@
import { SqlRepository } from './sql-repository';
const repo = new SqlRepository();
repo.find(42);
repo.find('alice');
repo.save('test');

View file

@ -0,0 +1,5 @@
export interface IRepository {
find(id: number): string;
find(name: string): string;
save(data: string): void;
}

View file

@ -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);
}
}

View file

@ -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');
});
});

View file

@ -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);
});
});

View file

@ -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);
});
});

View file

@ -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);
});
});

View file

@ -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);
});
});