test: METHOD_IMPLEMENTS integration tests for C#, TypeScript, Kotlin, Dart

Add interface dispatch fixtures and METHOD_IMPLEMENTS edge assertions for
the 4 remaining languages that support interfaces:

- C#: IRepository interface with Find/Save, SqlRepository implements
- TypeScript: IRepository interface with find/save, SqlRepository implements
- Kotlin: Repository interface with find/save, SqlRepository implements
- Dart: abstract Repository with find/save, SqlRepository implements

Fix: Kotlin interfaces use class_declaration AST node type — added
keyword-based detection in findEnclosingClassInfo to correctly label
them as Interface (not Class), enabling METHOD_IMPLEMENTS edge emission.

Total METHOD_IMPLEMENTS integration coverage: 9 languages
(Java, PHP, Rust, Swift, Python, C#, TypeScript, Kotlin, Dart)
This commit is contained in:
Gergo Magyar 2026-04-03 17:09:20 +01:00
parent 1966242675
commit deabfcb046
17 changed files with 277 additions and 1 deletions

View file

@ -337,7 +337,17 @@ export const findEnclosingClassInfo = (
c.type === 'constant',
);
if (nameNode) {
const label = CONTAINER_TYPE_TO_LABEL[current.type] || 'Class';
let label = CONTAINER_TYPE_TO_LABEL[current.type] || 'Class';
// Kotlin: class_declaration with an anonymous "interface" keyword child
// is actually an interface, not a class. Refine the label to match the
// node ID generated from the tree-sitter query capture (@definition.interface).
if (
current.type === 'class_declaration' &&
label === 'Class' &&
current.children?.some((c: SyntaxNode) => c.type === 'interface')
) {
label = 'Interface';
}
return {
classId: generateId(label, `${filePath}:${nameNode.text}`),
className: nameNode.text,

View file

@ -0,0 +1,7 @@
public class App {
public static void Main() {
IRepository repo = new SqlRepository();
repo.Find(1);
repo.Save("test");
}
}

View file

@ -0,0 +1,4 @@
public interface IRepository {
string Find(int id);
bool Save(string entity);
}

View file

@ -0,0 +1,9 @@
public class SqlRepository : IRepository {
public string Find(int id) {
return "found";
}
public bool Save(string entity) {
return true;
}
}

View file

@ -0,0 +1,7 @@
import 'sql_repository.dart';
void main() {
final repo = SqlRepository();
repo.find(1);
repo.save("test");
}

View file

@ -0,0 +1,4 @@
abstract class Repository {
String find(int id);
bool save(String entity);
}

View file

@ -0,0 +1,13 @@
import 'repository.dart';
class SqlRepository implements Repository {
@override
String find(int id) {
return "found";
}
@override
bool save(String entity) {
return true;
}
}

View file

@ -0,0 +1,5 @@
fun main() {
val repo: Repository = SqlRepository()
repo.find(1)
repo.save("test")
}

View file

@ -0,0 +1,4 @@
interface Repository {
fun find(id: Int): String
fun save(entity: String): Boolean
}

View file

@ -0,0 +1,9 @@
class SqlRepository : Repository {
override fun find(id: Int): String {
return "found"
}
override fun save(entity: String): Boolean {
return true
}
}

View file

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

View file

@ -0,0 +1,4 @@
export interface IRepository {
find(id: number): string;
save(entity: string): boolean;
}

View file

@ -0,0 +1,11 @@
import { IRepository } from './repository';
export class SqlRepository implements IRepository {
find(id: number): string {
return "found";
}
save(entity: string): boolean {
return true;
}
}

View file

@ -1722,3 +1722,48 @@ describe('C# method enrichment', () => {
expect(classifyCall).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// Interface dispatch: METHOD_IMPLEMENTS edges
// ---------------------------------------------------------------------------
describe('C# interface dispatch (METHOD_IMPLEMENTS)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'csharp-interface-dispatch'), () => {});
}, 60000);
it('detects IRepository interface and SqlRepository class', () => {
const classes = getNodesByLabel(result, 'Class');
const ifaces = getNodesByLabel(result, 'Interface');
expect(classes).toContain('SqlRepository');
expect(ifaces).toContain('IRepository');
});
it('emits IMPLEMENTS edge SqlRepository → IRepository', () => {
const impl = getRelationships(result, 'IMPLEMENTS');
const edge = impl.find((e) => e.source === 'SqlRepository' && e.target === 'IRepository');
expect(edge).toBeDefined();
});
it('emits METHOD_IMPLEMENTS edges for Find and Save', () => {
const mi = getRelationships(result, 'METHOD_IMPLEMENTS');
const findEdge = mi.find(
(e) =>
e.source === 'Find' &&
e.target === 'Find' &&
e.sourceFilePath.includes('SqlRepository') &&
e.targetFilePath.includes('IRepository'),
);
const saveEdge = mi.find(
(e) =>
e.source === 'Save' &&
e.target === 'Save' &&
e.sourceFilePath.includes('SqlRepository') &&
e.targetFilePath.includes('IRepository'),
);
expect(findEdge).toBeDefined();
expect(saveEdge).toBeDefined();
});
});

View file

@ -429,3 +429,48 @@ describe.skipIf(!dartAvailable)('Dart async method detection', () => {
expect(formatName!.properties.returnType).toBe('String');
});
});
// ---------------------------------------------------------------------------
// Interface dispatch: METHOD_IMPLEMENTS edges from concrete → abstract methods
// abstract Repository with find/save, SqlRepository implements them
// ---------------------------------------------------------------------------
describe.skipIf(!dartAvailable)('Dart interface dispatch (METHOD_IMPLEMENTS)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'dart-interface-dispatch'), () => {});
}, 60000);
it('detects Repository class and SqlRepository class', () => {
const classes = getNodesByLabel(result, 'Class');
expect(classes).toContain('Repository');
expect(classes).toContain('SqlRepository');
});
it('emits IMPLEMENTS edge SqlRepository → Repository', () => {
const impl = getRelationships(result, 'IMPLEMENTS');
const edge = impl.find((e) => e.source === 'SqlRepository' && e.target === 'Repository');
expect(edge).toBeDefined();
});
it('emits METHOD_IMPLEMENTS edges for find and save', () => {
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'),
);
const saveEdge = mi.find(
(e) =>
e.source === 'save' &&
e.target === 'save' &&
e.sourceFilePath.includes('sql_repository') &&
e.targetFilePath.includes('repository'),
);
expect(findEdge).toBeDefined();
expect(saveEdge).toBeDefined();
});
});

View file

@ -1811,3 +1811,49 @@ describe('Kotlin method enrichment', () => {
expect(classifyCall).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// Interface dispatch: METHOD_IMPLEMENTS edges from concrete → interface methods
// Repository interface with find/save, SqlRepository implements them
// ---------------------------------------------------------------------------
describe('Kotlin interface dispatch (METHOD_IMPLEMENTS)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'kotlin-interface-dispatch'), () => {});
}, 60000);
it('detects Repository interface and SqlRepository class', () => {
const classes = getNodesByLabel(result, 'Class');
const ifaces = getNodesByLabel(result, 'Interface');
expect(classes).toContain('SqlRepository');
expect(ifaces).toContain('Repository');
});
it('emits IMPLEMENTS edge SqlRepository → Repository', () => {
const impl = getRelationships(result, 'IMPLEMENTS');
const edge = impl.find((e) => e.source === 'SqlRepository' && e.target === 'Repository');
expect(edge).toBeDefined();
});
it('emits METHOD_IMPLEMENTS edges for find and save', () => {
const mi = getRelationships(result, 'METHOD_IMPLEMENTS');
const findEdge = mi.find(
(e) =>
e.source === 'find' &&
e.target === 'find' &&
e.sourceFilePath.includes('SqlRepository') &&
e.targetFilePath.includes('Repository'),
);
const saveEdge = mi.find(
(e) =>
e.source === 'save' &&
e.target === 'save' &&
e.sourceFilePath.includes('SqlRepository') &&
e.targetFilePath.includes('Repository'),
);
expect(findEdge).toBeDefined();
expect(saveEdge).toBeDefined();
});
});

View file

@ -2395,3 +2395,51 @@ describe('TypeScript method enrichment', () => {
expect(classifyCall).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// Interface dispatch: METHOD_IMPLEMENTS edges
// ---------------------------------------------------------------------------
describe('TypeScript interface dispatch (METHOD_IMPLEMENTS)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'typescript-interface-dispatch'),
() => {},
);
}, 60000);
it('detects IRepository interface and SqlRepository class', () => {
const classes = getNodesByLabel(result, 'Class');
const ifaces = getNodesByLabel(result, 'Interface');
expect(classes).toContain('SqlRepository');
expect(ifaces).toContain('IRepository');
});
it('emits IMPLEMENTS edge SqlRepository → IRepository', () => {
const impl = getRelationships(result, 'IMPLEMENTS');
const edge = impl.find((e) => e.source === 'SqlRepository' && e.target === 'IRepository');
expect(edge).toBeDefined();
});
it('emits METHOD_IMPLEMENTS edges for find and save', () => {
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'),
);
const saveEdge = mi.find(
(e) =>
e.source === 'save' &&
e.target === 'save' &&
e.sourceFilePath.includes('sql-repository') &&
e.targetFilePath.includes('repository'),
);
expect(findEdge).toBeDefined();
expect(saveEdge).toBeDefined();
});
});