import { describe, it, expect } from 'vitest'; import { computeMRO } from '../../src/core/ingestion/mro-processor.js'; import { createKnowledgeGraph } from '../../src/core/graph/graph.js'; import type { KnowledgeGraph } from '../../src/core/graph/types.js'; import { generateId } from '../../src/lib/utils.js'; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function addClass( graph: KnowledgeGraph, name: string, language: string, label: 'Class' | 'Interface' | 'Struct' | 'Trait' = 'Class', ) { const id = generateId(label, name); graph.addNode({ id, label, properties: { name, filePath: `src/${name}.ts`, language }, }); return id; } function addMethod( graph: KnowledgeGraph, className: string, methodName: string, classLabel: 'Class' | 'Interface' | 'Struct' | 'Trait' = 'Class', parameterTypes?: string[], opts?: { isAbstract?: boolean; parameterCount?: number }, ) { // Derive arity for the ID suffix: explicit parameterCount > parameterTypes.length > 0 const arity = opts?.parameterCount ?? parameterTypes?.length ?? 0; const classId = generateId(classLabel, className); const methodId = generateId('Method', `${className}.${methodName}#${arity}`); graph.addNode({ id: methodId, label: 'Method', properties: { name: methodName, filePath: `src/${className}.ts`, parameterCount: arity, ...(parameterTypes ? { parameterTypes } : {}), ...(opts?.isAbstract !== undefined ? { isAbstract: opts.isAbstract } : {}), }, }); graph.addRelationship({ id: generateId('HAS_METHOD', `${classId}->${methodId}`), sourceId: classId, targetId: methodId, type: 'HAS_METHOD', confidence: 1.0, reason: '', }); return methodId; } function addExtends( graph: KnowledgeGraph, childName: string, parentName: string, childLabel: 'Class' | 'Struct' = 'Class', parentLabel: 'Class' | 'Interface' | 'Trait' = 'Class', ) { const childId = generateId(childLabel, childName); const parentId = generateId(parentLabel, parentName); graph.addRelationship({ id: generateId('EXTENDS', `${childId}->${parentId}`), sourceId: childId, targetId: parentId, type: 'EXTENDS', confidence: 1.0, reason: '', }); } function addInterfaceExtends( graph: KnowledgeGraph, childName: string, parentName: string, childLabel: 'Interface' | 'Trait' = 'Interface', parentLabel: 'Interface' | 'Trait' = 'Interface', ) { const childId = generateId(childLabel, childName); const parentId = generateId(parentLabel, parentName); graph.addRelationship({ id: generateId('EXTENDS', `${childId}->${parentId}`), sourceId: childId, targetId: parentId, type: 'EXTENDS', confidence: 1.0, reason: '', }); } function addImplements( graph: KnowledgeGraph, childName: string, parentName: string, childLabel: 'Class' | 'Struct' = 'Class', parentLabel: 'Interface' | 'Trait' = 'Interface', ) { const childId = generateId(childLabel, childName); const parentId = generateId(parentLabel, parentName); graph.addRelationship({ id: generateId('IMPLEMENTS', `${childId}->${parentId}`), sourceId: childId, targetId: parentId, type: 'IMPLEMENTS', confidence: 1.0, reason: '', }); } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe('computeMRO', () => { // ---- C++ diamond -------------------------------------------------------- describe('C++ diamond inheritance', () => { it('leftmost base wins when both B and C override foo', () => { // Diamond: A <- B, A <- C, B <- D, C <- D const graph = createKnowledgeGraph(); const aId = addClass(graph, 'A', 'cpp'); const bId = addClass(graph, 'B', 'cpp'); const cId = addClass(graph, 'C', 'cpp'); const dId = addClass(graph, 'D', 'cpp'); addExtends(graph, 'B', 'A'); addExtends(graph, 'C', 'A'); addExtends(graph, 'D', 'B'); // B is leftmost addExtends(graph, 'D', 'C'); // A has foo, B overrides foo, C overrides foo addMethod(graph, 'A', 'foo'); const bFoo = addMethod(graph, 'B', 'foo'); const cFoo = addMethod(graph, 'C', 'foo'); const result = computeMRO(graph); // D should have an entry with ambiguity on foo const dEntry = result.entries.find((e) => e.className === 'D'); expect(dEntry).toBeDefined(); expect(dEntry!.language).toBe('cpp'); const fooAmbiguity = dEntry!.ambiguities.find((a) => a.methodName === 'foo'); expect(fooAmbiguity).toBeDefined(); expect(fooAmbiguity!.definedIn.length).toBeGreaterThanOrEqual(2); // Leftmost base (B) wins expect(fooAmbiguity!.resolvedTo).toBe(bFoo); expect(fooAmbiguity!.reason).toContain('leftmost base'); expect(fooAmbiguity!.reason).toContain('B'); // OVERRIDES edge emitted expect(result.overrideEdges).toBeGreaterThanOrEqual(1); const overrides = graph.relationships.filter((r) => r.type === 'METHOD_OVERRIDES'); expect(overrides.some((r) => r.sourceId === dId && r.targetId === bFoo)).toBe(true); }); it('no ambiguity when foo only in A (diamond no override)', () => { // Diamond: A <- B, A <- C, B <- D, C <- D, but only A has foo const graph = createKnowledgeGraph(); addClass(graph, 'A', 'cpp'); addClass(graph, 'B', 'cpp'); addClass(graph, 'C', 'cpp'); addClass(graph, 'D', 'cpp'); addExtends(graph, 'B', 'A'); addExtends(graph, 'C', 'A'); addExtends(graph, 'D', 'B'); addExtends(graph, 'D', 'C'); // Only A has foo addMethod(graph, 'A', 'foo'); const result = computeMRO(graph); const dEntry = result.entries.find((e) => e.className === 'D'); expect(dEntry).toBeDefined(); // A::foo appears only once across ancestors — no collision // (B and C don't have their own foo, the duplicate is A::foo seen through both paths) const fooAmbiguity = dEntry!.ambiguities.find((a) => a.methodName === 'foo'); expect(fooAmbiguity).toBeUndefined(); }); }); // ---- C# class + interface ----------------------------------------------- describe('C# class + interface', () => { it('class method beats interface default', () => { const graph = createKnowledgeGraph(); const classId = addClass(graph, 'MyClass', 'csharp'); const baseId = addClass(graph, 'BaseClass', 'csharp'); const ifaceId = addClass(graph, 'IDoSomething', 'csharp', 'Interface'); addExtends(graph, 'MyClass', 'BaseClass'); addImplements(graph, 'MyClass', 'IDoSomething'); const baseDoIt = addMethod(graph, 'BaseClass', 'doIt'); const ifaceDoIt = addMethod(graph, 'IDoSomething', 'doIt', 'Interface'); const result = computeMRO(graph); const entry = result.entries.find((e) => e.className === 'MyClass'); expect(entry).toBeDefined(); const doItAmbiguity = entry!.ambiguities.find((a) => a.methodName === 'doIt'); expect(doItAmbiguity).toBeDefined(); // Class method wins expect(doItAmbiguity!.resolvedTo).toBe(baseDoIt); expect(doItAmbiguity!.reason).toContain('class method wins'); }); it('multiple interface methods with same name are ambiguous', () => { const graph = createKnowledgeGraph(); addClass(graph, 'MyClass', 'csharp'); addClass(graph, 'IFoo', 'csharp', 'Interface'); addClass(graph, 'IBar', 'csharp', 'Interface'); addImplements(graph, 'MyClass', 'IFoo'); addImplements(graph, 'MyClass', 'IBar'); addMethod(graph, 'IFoo', 'process', 'Interface'); addMethod(graph, 'IBar', 'process', 'Interface'); const result = computeMRO(graph); const entry = result.entries.find((e) => e.className === 'MyClass'); expect(entry).toBeDefined(); const processAmbiguity = entry!.ambiguities.find((a) => a.methodName === 'process'); expect(processAmbiguity).toBeDefined(); expect(processAmbiguity!.resolvedTo).toBeNull(); expect(processAmbiguity!.reason).toContain('ambiguous'); expect(result.ambiguityCount).toBeGreaterThanOrEqual(1); }); }); // ---- Python C3 ---------------------------------------------------------- describe('Python C3 linearization', () => { it('C3 order determines winner in diamond with overrides', () => { // Diamond: A <- B, A <- C, B <- D, C <- D // class D(B, C) → C3 MRO: B, C, A const graph = createKnowledgeGraph(); addClass(graph, 'A', 'python'); addClass(graph, 'B', 'python'); addClass(graph, 'C', 'python'); const dId = addClass(graph, 'D', 'python'); addExtends(graph, 'B', 'A'); addExtends(graph, 'C', 'A'); addExtends(graph, 'D', 'B'); // B first → leftmost in C3 addExtends(graph, 'D', 'C'); addMethod(graph, 'A', 'foo'); const bFoo = addMethod(graph, 'B', 'foo'); addMethod(graph, 'C', 'foo'); const result = computeMRO(graph); const dEntry = result.entries.find((e) => e.className === 'D'); expect(dEntry).toBeDefined(); const fooAmbiguity = dEntry!.ambiguities.find((a) => a.methodName === 'foo'); expect(fooAmbiguity).toBeDefined(); // C3 linearization for D(B, C): B comes first expect(fooAmbiguity!.resolvedTo).toBe(bFoo); expect(fooAmbiguity!.reason).toContain('C3 MRO'); }); }); // ---- Java class + interface --------------------------------------------- describe('Java class + interface', () => { it('class method beats interface default', () => { const graph = createKnowledgeGraph(); addClass(graph, 'Service', 'java'); addClass(graph, 'BaseService', 'java'); addClass(graph, 'Runnable', 'java', 'Interface'); addExtends(graph, 'Service', 'BaseService'); addImplements(graph, 'Service', 'Runnable'); const baseRun = addMethod(graph, 'BaseService', 'run'); addMethod(graph, 'Runnable', 'run', 'Interface'); const result = computeMRO(graph); const entry = result.entries.find((e) => e.className === 'Service'); expect(entry).toBeDefined(); const runAmbiguity = entry!.ambiguities.find((a) => a.methodName === 'run'); expect(runAmbiguity).toBeDefined(); expect(runAmbiguity!.resolvedTo).toBe(baseRun); expect(runAmbiguity!.reason).toContain('class method wins'); }); }); // ---- Rust trait conflicts ----------------------------------------------- describe('Rust trait conflicts', () => { it('trait conflicts result in null resolution with qualified syntax reason', () => { const graph = createKnowledgeGraph(); addClass(graph, 'MyStruct', 'rust', 'Struct'); addClass(graph, 'TraitA', 'rust', 'Trait'); addClass(graph, 'TraitB', 'rust', 'Trait'); addImplements(graph, 'MyStruct', 'TraitA', 'Struct', 'Trait'); addImplements(graph, 'MyStruct', 'TraitB', 'Struct', 'Trait'); addMethod(graph, 'TraitA', 'execute', 'Trait'); addMethod(graph, 'TraitB', 'execute', 'Trait'); const result = computeMRO(graph); const entry = result.entries.find((e) => e.className === 'MyStruct'); expect(entry).toBeDefined(); const execAmbiguity = entry!.ambiguities.find((a) => a.methodName === 'execute'); expect(execAmbiguity).toBeDefined(); expect(execAmbiguity!.resolvedTo).toBeNull(); expect(execAmbiguity!.reason).toContain('qualified syntax'); expect(result.ambiguityCount).toBeGreaterThanOrEqual(1); // No OVERRIDES edge emitted for Rust ambiguity const overrides = graph.relationships.filter( (r) => r.type === 'METHOD_OVERRIDES' && r.sourceId === generateId('Struct', 'MyStruct'), ); expect(overrides).toHaveLength(0); }); }); // ---- Property collisions don't trigger OVERRIDES ------------------------ describe('Property nodes excluded from OVERRIDES', () => { it('property name collision across parents does not emit OVERRIDES edge', () => { const graph = createKnowledgeGraph(); const parentA = addClass(graph, 'ParentA', 'typescript'); const parentB = addClass(graph, 'ParentB', 'typescript'); const child = addClass(graph, 'Child', 'typescript'); addExtends(graph, 'Child', 'ParentA'); addExtends(graph, 'Child', 'ParentB'); // Add Property nodes (same name 'name') to both parents via HAS_PROPERTY const propA = generateId('Property', 'ParentA.name'); graph.addNode({ id: propA, label: 'Property', properties: { name: 'name', filePath: 'src/ParentA.ts' }, }); graph.addRelationship({ id: generateId('HAS_PROPERTY', `${parentA}->${propA}`), sourceId: parentA, targetId: propA, type: 'HAS_PROPERTY', confidence: 1.0, reason: '', }); const propB = generateId('Property', 'ParentB.name'); graph.addNode({ id: propB, label: 'Property', properties: { name: 'name', filePath: 'src/ParentB.ts' }, }); graph.addRelationship({ id: generateId('HAS_PROPERTY', `${parentB}->${propB}`), sourceId: parentB, targetId: propB, type: 'HAS_PROPERTY', confidence: 1.0, reason: '', }); const result = computeMRO(graph); // No OVERRIDES edge should be emitted for properties const overrides = graph.relationships.filter((r) => r.type === 'METHOD_OVERRIDES'); expect(overrides).toHaveLength(0); expect(result.overrideEdges).toBe(0); }); it('method collision still triggers OVERRIDES even when properties also collide', () => { const graph = createKnowledgeGraph(); const parentA = addClass(graph, 'PA', 'cpp'); const parentB = addClass(graph, 'PB', 'cpp'); addClass(graph, 'Ch', 'cpp'); addExtends(graph, 'Ch', 'PA'); addExtends(graph, 'Ch', 'PB'); // Method collision (should trigger OVERRIDES) const methodA = addMethod(graph, 'PA', 'doWork'); addMethod(graph, 'PB', 'doWork'); // Property collision (should NOT trigger OVERRIDES — properties use HAS_PROPERTY, not HAS_METHOD) const propA = generateId('Property', 'PA.id'); graph.addNode({ id: propA, label: 'Property', properties: { name: 'id', filePath: 'src/PA.ts' }, }); graph.addRelationship({ id: generateId('HAS_PROPERTY', `${parentA}->${propA}`), sourceId: parentA, targetId: propA, type: 'HAS_PROPERTY', confidence: 1.0, reason: '', }); const propB = generateId('Property', 'PB.id'); graph.addNode({ id: propB, label: 'Property', properties: { name: 'id', filePath: 'src/PB.ts' }, }); graph.addRelationship({ id: generateId('HAS_PROPERTY', `${parentB}->${propB}`), sourceId: parentB, targetId: propB, type: 'HAS_PROPERTY', confidence: 1.0, reason: '', }); const result = computeMRO(graph); // Only 1 OVERRIDES edge (for the method, not the property) const overrides = graph.relationships.filter((r) => r.type === 'METHOD_OVERRIDES'); expect(overrides).toHaveLength(1); expect(overrides[0].targetId).toBe(methodA); // leftmost base wins for C++ expect(result.overrideEdges).toBe(1); }); }); // ---- No ambiguity: single parent ---------------------------------------- describe('single parent, no ambiguity', () => { it('single parent with unique methods produces no ambiguities', () => { const graph = createKnowledgeGraph(); addClass(graph, 'Parent', 'typescript'); addClass(graph, 'Child', 'typescript'); addExtends(graph, 'Child', 'Parent'); addMethod(graph, 'Parent', 'foo'); addMethod(graph, 'Parent', 'bar'); const result = computeMRO(graph); const entry = result.entries.find((e) => e.className === 'Child'); expect(entry).toBeDefined(); expect(entry!.ambiguities).toHaveLength(0); }); }); // ---- No parents: standalone class not in entries ------------------------ describe('standalone class', () => { it('class with no parents is not included in entries', () => { const graph = createKnowledgeGraph(); addClass(graph, 'Standalone', 'typescript'); addMethod(graph, 'Standalone', 'doStuff'); const result = computeMRO(graph); const entry = result.entries.find((e) => e.className === 'Standalone'); expect(entry).toBeUndefined(); expect(result.overrideEdges).toBe(0); expect(result.ambiguityCount).toBe(0); }); }); // ---- Own method shadows ancestor ---------------------------------------- describe('own method shadows ancestor', () => { it('class defining its own method suppresses ambiguity', () => { const graph = createKnowledgeGraph(); addClass(graph, 'Base1', 'cpp'); addClass(graph, 'Base2', 'cpp'); addClass(graph, 'Child', 'cpp'); addExtends(graph, 'Child', 'Base1'); addExtends(graph, 'Child', 'Base2'); addMethod(graph, 'Base1', 'foo'); addMethod(graph, 'Base2', 'foo'); addMethod(graph, 'Child', 'foo'); // own method const result = computeMRO(graph); const entry = result.entries.find((e) => e.className === 'Child'); expect(entry).toBeDefined(); // No ambiguity because Child defines its own foo const fooAmbiguity = entry!.ambiguities.find((a) => a.methodName === 'foo'); expect(fooAmbiguity).toBeUndefined(); }); }); // ---- Single-ancestor override detection --------------------------------- describe('single-ancestor override detection', () => { it('emits one Class→Method edge when a subclass overrides a parent method', () => { const graph = createKnowledgeGraph(); const childId = addClass(graph, 'B', 'java'); addClass(graph, 'A', 'java'); addExtends(graph, 'B', 'A'); const aFoo = addMethod(graph, 'A', 'foo'); addMethod(graph, 'B', 'foo'); // B overrides A.foo const result = computeMRO(graph); const overrides = graph.relationships.filter((r) => r.type === 'METHOD_OVERRIDES'); expect(overrides).toHaveLength(1); expect(overrides[0].sourceId).toBe(childId); // child CLASS is the source expect(overrides[0].targetId).toBe(aFoo); // ancestor METHOD node, not the class expect(overrides[0].confidence).toBe(0.9); expect(result.overrideEdges).toBe(1); }); it('emits a distinct edge per overridden method of the same parent', () => { const graph = createKnowledgeGraph(); addClass(graph, 'B', 'java'); addClass(graph, 'A', 'java'); addExtends(graph, 'B', 'A'); const aFoo = addMethod(graph, 'A', 'foo'); const aBar = addMethod(graph, 'A', 'bar'); addMethod(graph, 'B', 'foo'); addMethod(graph, 'B', 'bar'); const result = computeMRO(graph); const overrides = graph.relationships.filter((r) => r.type === 'METHOD_OVERRIDES'); expect(overrides).toHaveLength(2); expect(overrides.map((r) => r.targetId).sort()).toEqual([aBar, aFoo].sort()); expect(result.overrideEdges).toBe(2); // counter matches edges actually added }); it('does not treat a same-named overload (different arity) as an override', () => { const graph = createKnowledgeGraph(); addClass(graph, 'B', 'java'); addClass(graph, 'A', 'java'); addExtends(graph, 'B', 'A'); addMethod(graph, 'A', 'foo', 'Class', undefined, { parameterCount: 0 }); addMethod(graph, 'B', 'foo', 'Class', undefined, { parameterCount: 1 }); // foo(x) — overload const result = computeMRO(graph); const overrides = graph.relationships.filter((r) => r.type === 'METHOD_OVERRIDES'); expect(overrides).toHaveLength(0); expect(result.overrideEdges).toBe(0); }); it('targets the nearest ancestor when multiple levels define the method', () => { const graph = createKnowledgeGraph(); addClass(graph, 'C', 'java'); addClass(graph, 'B', 'java'); addClass(graph, 'A', 'java'); addExtends(graph, 'C', 'B'); addExtends(graph, 'B', 'A'); addMethod(graph, 'A', 'foo'); const bFoo = addMethod(graph, 'B', 'foo'); // nearest ancestor of C with foo addMethod(graph, 'C', 'foo'); computeMRO(graph); const cOverride = graph.relationships.find( (r) => r.type === 'METHOD_OVERRIDES' && r.sourceId === generateId('Class', 'C'), ); expect(cOverride).toBeDefined(); expect(cOverride!.targetId).toBe(bFoo); // nearest (B), not A }); it('does not emit overrides for qualified-syntax languages (Rust)', () => { const graph = createKnowledgeGraph(); // Identical class-inheritance shape to the Java happy-path; the Rust // (qualified-syntax) gate must suppress it. addClass(graph, 'Derived', 'rust'); addClass(graph, 'Base', 'rust'); addExtends(graph, 'Derived', 'Base'); addMethod(graph, 'Base', 'run'); addMethod(graph, 'Derived', 'run'); const result = computeMRO(graph); const overrides = graph.relationships.filter((r) => r.type === 'METHOD_OVERRIDES'); expect(overrides).toHaveLength(0); expect(result.overrideEdges).toBe(0); }); it('does not emit an override for a same-named interface method (implements, not extends)', () => { const graph = createKnowledgeGraph(); addClass(graph, 'Impl', 'java'); addClass(graph, 'Iface', 'java', 'Interface'); addImplements(graph, 'Impl', 'Iface'); addMethod(graph, 'Iface', 'run', 'Interface'); addMethod(graph, 'Impl', 'run'); computeMRO(graph); const overrides = graph.relationships.filter((r) => r.type === 'METHOD_OVERRIDES'); expect(overrides).toHaveLength(0); // interface satisfaction is METHOD_IMPLEMENTS, not an override }); it('overrideEdges count equals the number of METHOD_OVERRIDES relationships', () => { const graph = createKnowledgeGraph(); addClass(graph, 'B', 'java'); addClass(graph, 'A', 'java'); addExtends(graph, 'B', 'A'); addMethod(graph, 'A', 'foo'); addMethod(graph, 'A', 'bar'); addMethod(graph, 'B', 'foo'); addMethod(graph, 'B', 'bar'); const result = computeMRO(graph); const overrides = graph.relationships.filter((r) => r.type === 'METHOD_OVERRIDES'); expect(result.overrideEdges).toBe(overrides.length); }); }); // ---- Empty graph -------------------------------------------------------- describe('empty graph', () => { it('returns empty result for graph with no classes', () => { const graph = createKnowledgeGraph(); const result = computeMRO(graph); expect(result.entries).toHaveLength(0); expect(result.overrideEdges).toBe(0); expect(result.ambiguityCount).toBe(0); }); }); // ---- Cyclic inheritance (P1 fix) ---------------------------------------- describe('cyclic inheritance', () => { it('does not stack overflow on cyclic Python hierarchy', () => { // A extends B, B extends A — cyclic const graph = createKnowledgeGraph(); addClass(graph, 'A', 'python'); addClass(graph, 'B', 'python'); addExtends(graph, 'A', 'B'); addExtends(graph, 'B', 'A'); addMethod(graph, 'A', 'foo'); addMethod(graph, 'B', 'foo'); // Should NOT throw — c3Linearize returns null, falls back to BFS const result = computeMRO(graph); expect(result).toBeDefined(); // Both A and B have parents, so both get entries expect(result.entries.length).toBeGreaterThanOrEqual(1); }); it('handles 3-node cycle gracefully', () => { // A → B → C → A const graph = createKnowledgeGraph(); addClass(graph, 'X', 'python'); addClass(graph, 'Y', 'python'); addClass(graph, 'Z', 'python'); addExtends(graph, 'X', 'Y'); addExtends(graph, 'Y', 'Z'); addExtends(graph, 'Z', 'X'); const result = computeMRO(graph); expect(result).toBeDefined(); }); it('returns null for C3 merge-conflict inconsistency (non-cyclic)', () => { // Classic incompatible ordering: A(X,Y) and B(Y,X) → C(A,B) is unresolvable const graph = createKnowledgeGraph(); addClass(graph, 'X', 'python'); addClass(graph, 'Y', 'python'); addClass(graph, 'A', 'python'); addClass(graph, 'B', 'python'); addClass(graph, 'C', 'python'); addExtends(graph, 'A', 'X'); addExtends(graph, 'A', 'Y'); addExtends(graph, 'B', 'Y'); addExtends(graph, 'B', 'X'); addExtends(graph, 'C', 'A'); addExtends(graph, 'C', 'B'); addMethod(graph, 'X', 'foo'); const result = computeMRO(graph); expect(result).toBeDefined(); // C3 fails for C — falls back to BFS ancestors const entryC = result.entries.find((e) => e.className === 'C'); expect(entryC).toBeDefined(); // BFS fallback still produces an MRO (just not C3-ordered) expect(entryC!.mro.length).toBeGreaterThanOrEqual(2); }); }); // ---- Performance (deep chains) ------------------------------------------- describe('performance', () => { it('handles very deep single-inheritance chain without stack overflow', () => { const graph = createKnowledgeGraph(); const DEPTH = 2000; for (let i = 0; i < DEPTH; i++) { addClass(graph, `C${i}`, 'python'); } for (let i = 1; i < DEPTH; i++) { addExtends(graph, `C${i}`, `C${i - 1}`); } addMethod(graph, 'C0', 'baseMethod'); const result = computeMRO(graph); expect(result).toBeDefined(); const deepest = result.entries.find((e) => e.className === `C${DEPTH - 1}`); if (deepest) { expect(deepest.mro.length).toBe(DEPTH - 1); } }, 15_000); }); // ---- METHOD_IMPLEMENTS edges ----------------------------------------------- describe('METHOD_IMPLEMENTS edges', () => { it('emits METHOD_IMPLEMENTS for class implementing interface method', () => { // IAnimal { speak() } <-- Dog { speak() } const graph = createKnowledgeGraph(); addClass(graph, 'IAnimal', 'java', 'Interface'); addClass(graph, 'Dog', 'java'); addImplements(graph, 'Dog', 'IAnimal'); const ifaceMethod = addMethod(graph, 'IAnimal', 'speak', 'Interface'); const classMethod = addMethod(graph, 'Dog', 'speak'); const result = computeMRO(graph); expect(result.methodImplementsEdges).toBe(1); // Verify the edge exists: ConcreteMethod → InterfaceMethod const edges: any[] = []; graph.forEachRelationship((rel) => { if (rel.type === 'METHOD_IMPLEMENTS') edges.push(rel); }); expect(edges).toHaveLength(1); expect(edges[0].sourceId).toBe(classMethod); expect(edges[0].targetId).toBe(ifaceMethod); // Both sides have parameterCount=0 (arity match) → confidence 1.0 expect(edges[0].confidence).toBe(1.0); }); it('emits METHOD_IMPLEMENTS for Rust struct implementing trait', () => { // Drawable { draw() } <-- Circle { draw() } const graph = createKnowledgeGraph(); addClass(graph, 'Drawable', 'rust', 'Trait'); addClass(graph, 'Circle', 'rust', 'Struct'); addImplements(graph, 'Circle', 'Drawable', 'Struct', 'Trait'); const traitMethod = addMethod(graph, 'Drawable', 'draw', 'Trait'); const structMethod = addMethod(graph, 'Circle', 'draw', 'Struct'); const result = computeMRO(graph); expect(result.methodImplementsEdges).toBe(1); const edges: any[] = []; graph.forEachRelationship((rel) => { if (rel.type === 'METHOD_IMPLEMENTS') edges.push(rel); }); expect(edges[0].sourceId).toBe(structMethod); expect(edges[0].targetId).toBe(traitMethod); }); it('matches overloaded interface methods by parameterTypes', () => { // IRepo { find(String), find(String, int) } <-- SqlRepo { find(String), find(String, int) } const graph = createKnowledgeGraph(); addClass(graph, 'IRepo', 'java', 'Interface'); addClass(graph, 'SqlRepo', 'java'); addImplements(graph, 'SqlRepo', 'IRepo'); // Use manual IDs to avoid overloaded-name collision (same name, different types) const ifaceFind1 = generateId('Method', 'IRepo.find#1'); graph.addNode({ id: ifaceFind1, label: 'Method', properties: { name: 'find', filePath: 'src/IRepo.ts', parameterTypes: ['String'], parameterCount: 1, }, }); graph.addRelationship({ id: generateId('HAS_METHOD', `${generateId('Interface', 'IRepo')}->${ifaceFind1}`), sourceId: generateId('Interface', 'IRepo'), targetId: ifaceFind1, type: 'HAS_METHOD', confidence: 1.0, reason: '', }); const ifaceFind2 = generateId('Method', 'IRepo.find#2'); graph.addNode({ id: ifaceFind2, label: 'Method', properties: { name: 'find', filePath: 'src/IRepo.ts', parameterTypes: ['String', 'int'], parameterCount: 2, }, }); graph.addRelationship({ id: generateId('HAS_METHOD', `${generateId('Interface', 'IRepo')}->${ifaceFind2}`), sourceId: generateId('Interface', 'IRepo'), targetId: ifaceFind2, type: 'HAS_METHOD', confidence: 1.0, reason: '', }); const sqlFind1Id = generateId('Method', 'SqlRepo.find#1'); graph.addNode({ id: sqlFind1Id, label: 'Method', properties: { name: 'find', filePath: 'src/SqlRepo.ts', parameterTypes: ['String'], parameterCount: 1, }, }); graph.addRelationship({ id: generateId('HAS_METHOD', `${generateId('Class', 'SqlRepo')}->${sqlFind1Id}`), sourceId: generateId('Class', 'SqlRepo'), targetId: sqlFind1Id, type: 'HAS_METHOD', confidence: 1.0, reason: '', }); const sqlFind2Id = generateId('Method', 'SqlRepo.find#2'); graph.addNode({ id: sqlFind2Id, label: 'Method', properties: { name: 'find', filePath: 'src/SqlRepo.ts', parameterTypes: ['String', 'int'], parameterCount: 2, }, }); graph.addRelationship({ id: generateId('HAS_METHOD', `${generateId('Class', 'SqlRepo')}->${sqlFind2Id}`), sourceId: generateId('Class', 'SqlRepo'), targetId: sqlFind2Id, type: 'HAS_METHOD', confidence: 1.0, reason: '', }); const result = computeMRO(graph); expect(result.methodImplementsEdges).toBe(2); const edges: any[] = []; graph.forEachRelationship((rel) => { if (rel.type === 'METHOD_IMPLEMENTS') edges.push(rel); }); expect(edges).toHaveLength(2); // find(String) → find(String) and find(String, int) → find(String, int) const edge1 = edges.find((e) => e.targetId === ifaceFind1); const edge2 = edges.find((e) => e.targetId === ifaceFind2); expect(edge1).toBeDefined(); expect(edge1!.sourceId).toBe(sqlFind1Id); expect(edge2).toBeDefined(); expect(edge2!.sourceId).toBe(sqlFind2Id); }); it('includes default interface methods (not just abstract)', () => { // Java 8 default method: IFoo { bar() } <-- Baz { bar() } const graph = createKnowledgeGraph(); addClass(graph, 'IFoo', 'java', 'Interface'); addClass(graph, 'Baz', 'java'); addImplements(graph, 'Baz', 'IFoo'); // Default method (has body, not abstract) — should still get METHOD_IMPLEMENTS addMethod(graph, 'IFoo', 'bar', 'Interface'); addMethod(graph, 'Baz', 'bar'); const result = computeMRO(graph); expect(result.methodImplementsEdges).toBe(1); }); it('does not emit METHOD_IMPLEMENTS for class extending another class', () => { // Animal { speak() } <-- Dog { speak() } — EXTENDS, not IMPLEMENTS const graph = createKnowledgeGraph(); addClass(graph, 'Animal', 'java'); addClass(graph, 'Dog', 'java'); addExtends(graph, 'Dog', 'Animal'); addMethod(graph, 'Animal', 'speak'); addMethod(graph, 'Dog', 'speak'); const result = computeMRO(graph); expect(result.methodImplementsEdges).toBe(0); }); it('does not emit METHOD_IMPLEMENTS when class has no matching method', () => { // IAnimal { speak() } <-- Dog { bark() } — no name match const graph = createKnowledgeGraph(); addClass(graph, 'IAnimal', 'java', 'Interface'); addClass(graph, 'Dog', 'java'); addImplements(graph, 'Dog', 'IAnimal'); addMethod(graph, 'IAnimal', 'speak', 'Interface'); addMethod(graph, 'Dog', 'bark'); const result = computeMRO(graph); expect(result.methodImplementsEdges).toBe(0); }); it('skips Property nodes on interface', () => { const graph = createKnowledgeGraph(); addClass(graph, 'IFoo', 'csharp', 'Interface'); addClass(graph, 'Bar', 'csharp'); addImplements(graph, 'Bar', 'IFoo'); // Add a Property to the interface (not a Method) const propId = generateId('Property', 'IFoo.name'); graph.addNode({ id: propId, label: 'Property', properties: { name: 'name', filePath: 'src/IFoo.ts' }, }); graph.addRelationship({ id: generateId('HAS_METHOD', `${generateId('Interface', 'IFoo')}->${propId}`), sourceId: generateId('Interface', 'IFoo'), targetId: propId, type: 'HAS_METHOD', confidence: 1.0, reason: '', }); addMethod(graph, 'Bar', 'name'); const result = computeMRO(graph); expect(result.methodImplementsEdges).toBe(0); }); describe('METHOD_IMPLEMENTS transitive ancestors', () => { it('transitive interface chain: C.foo links to both B.foo and A.foo', () => { // A (Interface) has foo, B (Interface) has foo extends A, C (Class) implements B const graph = createKnowledgeGraph(); addClass(graph, 'A', 'java', 'Interface'); addClass(graph, 'B', 'java', 'Interface'); addClass(graph, 'C', 'java'); addInterfaceExtends(graph, 'B', 'A'); addImplements(graph, 'C', 'B'); const aFoo = addMethod(graph, 'A', 'foo', 'Interface'); const bFoo = addMethod(graph, 'B', 'foo', 'Interface'); addMethod(graph, 'C', 'foo'); const result = computeMRO(graph); const edges: any[] = []; graph.forEachRelationship((rel) => { if (rel.type === 'METHOD_IMPLEMENTS') edges.push(rel); }); // C.foo should link to both B.foo and A.foo expect(edges.some((e) => e.targetId === bFoo)).toBe(true); expect(edges.some((e) => e.targetId === aFoo)).toBe(true); expect(result.methodImplementsEdges).toBeGreaterThanOrEqual(2); }); it('inherited contract method only on grandparent: C.bar links to A.bar', () => { // A (Interface) has bar, B (Interface) extends A but has NO bar, C implements B const graph = createKnowledgeGraph(); addClass(graph, 'A', 'java', 'Interface'); addClass(graph, 'B', 'java', 'Interface'); addClass(graph, 'C', 'java'); addInterfaceExtends(graph, 'B', 'A'); addImplements(graph, 'C', 'B'); const aBar = addMethod(graph, 'A', 'bar', 'Interface'); // B has no bar method addMethod(graph, 'C', 'bar'); const result = computeMRO(graph); const edges: any[] = []; graph.forEachRelationship((rel) => { if (rel.type === 'METHOD_IMPLEMENTS') edges.push(rel); }); // C.bar should link to A.bar even though A is not a direct parent expect(edges.some((e) => e.targetId === aBar)).toBe(true); expect(result.methodImplementsEdges).toBeGreaterThanOrEqual(1); }); it('diamond deduplication: E.foo gets exactly one edge to A.foo', () => { // A (Interface) has foo // B (Interface) has foo, extends A // D (Interface) has foo, extends A // E (Class) implements B and D const graph = createKnowledgeGraph(); addClass(graph, 'A', 'java', 'Interface'); addClass(graph, 'B', 'java', 'Interface'); addClass(graph, 'D', 'java', 'Interface'); addClass(graph, 'E', 'java'); addInterfaceExtends(graph, 'B', 'A'); addInterfaceExtends(graph, 'D', 'A'); addImplements(graph, 'E', 'B'); addImplements(graph, 'E', 'D'); const aFoo = addMethod(graph, 'A', 'foo', 'Interface'); const bFoo = addMethod(graph, 'B', 'foo', 'Interface'); const dFoo = addMethod(graph, 'D', 'foo', 'Interface'); addMethod(graph, 'E', 'foo'); const result = computeMRO(graph); const eFoo = generateId('Method', 'E.foo#0'); const edges: any[] = []; graph.forEachRelationship((rel) => { if (rel.type === 'METHOD_IMPLEMENTS') edges.push(rel); }); // Filter to only edges FROM E.foo const eFooEdges = edges.filter((e) => e.sourceId === eFoo); // E.foo should link to B.foo, D.foo, and exactly ONE A.foo (deduplicated) expect(eFooEdges.filter((e) => e.targetId === bFoo)).toHaveLength(1); expect(eFooEdges.filter((e) => e.targetId === dFoo)).toHaveLength(1); expect(eFooEdges.filter((e) => e.targetId === aFoo)).toHaveLength(1); // Total from E.foo: 3 edges (B.foo + D.foo + A.foo), not 4 expect(eFooEdges).toHaveLength(3); }); it('no transitive through class-only chain', () => { // A (Class) has foo, B (Class) extends A has foo, C (Class) extends B has foo const graph = createKnowledgeGraph(); addClass(graph, 'A', 'java'); addClass(graph, 'B', 'java'); addClass(graph, 'C', 'java'); addExtends(graph, 'B', 'A'); addExtends(graph, 'C', 'B'); addMethod(graph, 'A', 'foo'); addMethod(graph, 'B', 'foo'); addMethod(graph, 'C', 'foo'); const result = computeMRO(graph); // All class-extends, no interface involved → 0 METHOD_IMPLEMENTS edges expect(result.methodImplementsEdges).toBe(0); }); }); it('is queryable via MATCH pattern', () => { const graph = createKnowledgeGraph(); addClass(graph, 'IRepo', 'typescript', 'Interface'); addClass(graph, 'SqlRepo', 'typescript'); addImplements(graph, 'SqlRepo', 'IRepo'); addMethod(graph, 'IRepo', 'fetch', 'Interface'); const concreteId = addMethod(graph, 'SqlRepo', 'fetch'); computeMRO(graph); // Simulate MATCH (m)-[:METHOD_IMPLEMENTS]->(i) RETURN m const implementingMethods: string[] = []; graph.forEachRelationship((rel) => { if (rel.type === 'METHOD_IMPLEMENTS') { implementingMethods.push(rel.sourceId); } }); expect(implementingMethods).toContain(concreteId); }); describe('METHOD_IMPLEMENTS inherited + arity matching', () => { it('inherited implementation: Base.foo satisfies I.foo when C has no own foo', () => { const graph = createKnowledgeGraph(); addClass(graph, 'Base', 'java'); addClass(graph, 'I', 'java', 'Interface'); addClass(graph, 'C', 'java'); addExtends(graph, 'C', 'Base'); addImplements(graph, 'C', 'I'); const baseFoo = addMethod(graph, 'Base', 'foo'); const iFoo = addMethod(graph, 'I', 'foo', 'Interface'); const result = computeMRO(graph); const edges: any[] = []; graph.forEachRelationship((rel) => { if (rel.type === 'METHOD_IMPLEMENTS') edges.push(rel); }); expect(edges).toHaveLength(1); expect(edges[0].sourceId).toBe(baseFoo); expect(edges[0].targetId).toBe(iFoo); expect(result.methodImplementsEdges).toBe(1); }); it('class has own method — no inherited lookup needed', () => { const graph = createKnowledgeGraph(); addClass(graph, 'Base2', 'java'); addClass(graph, 'I2', 'java', 'Interface'); addClass(graph, 'C2', 'java'); addExtends(graph, 'C2', 'Base2'); addImplements(graph, 'C2', 'I2'); const baseFoo = addMethod(graph, 'Base2', 'foo'); const iFoo = addMethod(graph, 'I2', 'foo', 'Interface'); const cFoo = addMethod(graph, 'C2', 'foo'); const result = computeMRO(graph); const edges: any[] = []; graph.forEachRelationship((rel) => { if (rel.type === 'METHOD_IMPLEMENTS') edges.push(rel); }); // Should use C2.foo, not Base2.foo expect(edges).toHaveLength(1); expect(edges[0].sourceId).toBe(cFoo); expect(edges[0].targetId).toBe(iFoo); }); it('deep inheritance chain: GrandBase.foo satisfies I.foo', () => { const graph = createKnowledgeGraph(); addClass(graph, 'GrandBase', 'java'); addClass(graph, 'Base3', 'java'); addClass(graph, 'I3', 'java', 'Interface'); addClass(graph, 'C3', 'java'); addExtends(graph, 'Base3', 'GrandBase'); addExtends(graph, 'C3', 'Base3'); addImplements(graph, 'C3', 'I3'); const grandFoo = addMethod(graph, 'GrandBase', 'foo'); // Base3 has NO foo const iFoo = addMethod(graph, 'I3', 'foo', 'Interface'); const result = computeMRO(graph); const edges: any[] = []; graph.forEachRelationship((rel) => { if (rel.type === 'METHOD_IMPLEMENTS') edges.push(rel); }); expect(edges).toHaveLength(1); expect(edges[0].sourceId).toBe(grandFoo); expect(edges[0].targetId).toBe(iFoo); expect(result.methodImplementsEdges).toBe(1); }); it('arity mismatch prevents false match', () => { const graph = createKnowledgeGraph(); addClass(graph, 'IArity', 'java', 'Interface'); addClass(graph, 'CArity', 'java'); addImplements(graph, 'CArity', 'IArity'); // Interface method: parameterCount=2, no parameterTypes const iMethodId = generateId('Method', 'IArity.process#2'); graph.addNode({ id: iMethodId, label: 'Method', properties: { name: 'process', filePath: 'src/IArity.ts', parameterCount: 2 }, }); graph.addRelationship({ id: generateId('HAS_METHOD', `${generateId('Interface', 'IArity')}->${iMethodId}`), sourceId: generateId('Interface', 'IArity'), targetId: iMethodId, type: 'HAS_METHOD', confidence: 1.0, reason: '', }); // Class method: parameterCount=3, no parameterTypes const cMethodId = generateId('Method', 'CArity.process#3'); graph.addNode({ id: cMethodId, label: 'Method', properties: { name: 'process', filePath: 'src/CArity.ts', parameterCount: 3 }, }); graph.addRelationship({ id: generateId('HAS_METHOD', `${generateId('Class', 'CArity')}->${cMethodId}`), sourceId: generateId('Class', 'CArity'), targetId: cMethodId, type: 'HAS_METHOD', confidence: 1.0, reason: '', }); const result = computeMRO(graph); expect(result.methodImplementsEdges).toBe(0); }); it('arity match when types missing', () => { const graph = createKnowledgeGraph(); addClass(graph, 'IArityOk', 'java', 'Interface'); addClass(graph, 'CArityOk', 'java'); addImplements(graph, 'CArityOk', 'IArityOk'); // Interface method: parameterCount=2, no parameterTypes const iMethodId = generateId('Method', 'IArityOk.process#2'); graph.addNode({ id: iMethodId, label: 'Method', properties: { name: 'process', filePath: 'src/IArityOk.ts', parameterCount: 2 }, }); graph.addRelationship({ id: generateId('HAS_METHOD', `${generateId('Interface', 'IArityOk')}->${iMethodId}`), sourceId: generateId('Interface', 'IArityOk'), targetId: iMethodId, type: 'HAS_METHOD', confidence: 1.0, reason: '', }); // Class method: parameterCount=2, no parameterTypes const cMethodId = generateId('Method', 'CArityOk.process#2'); graph.addNode({ id: cMethodId, label: 'Method', properties: { name: 'process', filePath: 'src/CArityOk.ts', parameterCount: 2 }, }); graph.addRelationship({ id: generateId('HAS_METHOD', `${generateId('Class', 'CArityOk')}->${cMethodId}`), sourceId: generateId('Class', 'CArityOk'), targetId: cMethodId, type: 'HAS_METHOD', confidence: 1.0, reason: '', }); const result = computeMRO(graph); expect(result.methodImplementsEdges).toBe(1); }); it('multiple same-arity candidates = ambiguous, no edge emitted', () => { const graph = createKnowledgeGraph(); addClass(graph, 'IAmbig', 'java', 'Interface'); addClass(graph, 'CAmbig', 'java'); addImplements(graph, 'CAmbig', 'IAmbig'); // Interface method: parameterCount=1, no parameterTypes const iMethodId = generateId('Method', 'IAmbig.handle#1'); graph.addNode({ id: iMethodId, label: 'Method', properties: { name: 'handle', filePath: 'src/IAmbig.ts', parameterCount: 1 }, }); graph.addRelationship({ id: generateId('HAS_METHOD', `${generateId('Interface', 'IAmbig')}->${iMethodId}`), sourceId: generateId('Interface', 'IAmbig'), targetId: iMethodId, type: 'HAS_METHOD', confidence: 1.0, reason: '', }); // Two class methods named handle, both with parameterCount=1 const cMethod1 = generateId('Method', 'CAmbig.handle.1#1'); graph.addNode({ id: cMethod1, label: 'Method', properties: { name: 'handle', filePath: 'src/CAmbig.ts', parameterCount: 1 }, }); graph.addRelationship({ id: generateId('HAS_METHOD', `${generateId('Class', 'CAmbig')}->${cMethod1}`), sourceId: generateId('Class', 'CAmbig'), targetId: cMethod1, type: 'HAS_METHOD', confidence: 1.0, reason: '', }); const cMethod2 = generateId('Method', 'CAmbig.handle.2#1'); graph.addNode({ id: cMethod2, label: 'Method', properties: { name: 'handle', filePath: 'src/CAmbig.ts', parameterCount: 1 }, }); graph.addRelationship({ id: generateId('HAS_METHOD', `${generateId('Class', 'CAmbig')}->${cMethod2}`), sourceId: generateId('Class', 'CAmbig'), targetId: cMethod2, type: 'HAS_METHOD', confidence: 1.0, reason: '', }); const result = computeMRO(graph); expect(result.methodImplementsEdges).toBe(0); }); }); }); // ---- findInheritedMethod ambiguity detection ------------------------------ describe('findInheritedMethod ambiguity', () => { it('returns null when two EXTENDS parents both provide matching method', () => { // I { foo() }, B { foo() }, M { foo() }, C extends B + M, C implements I const graph = createKnowledgeGraph(); addClass(graph, 'I', 'cpp', 'Interface'); addClass(graph, 'B', 'cpp'); addClass(graph, 'M', 'cpp'); addClass(graph, 'C', 'cpp'); addImplements(graph, 'C', 'I'); addExtends(graph, 'C', 'B'); addExtends(graph, 'C', 'M'); addMethod(graph, 'I', 'foo', 'Interface'); addMethod(graph, 'B', 'foo'); addMethod(graph, 'M', 'foo'); // C has NO own foo — must walk EXTENDS chain const result = computeMRO(graph); // Ambiguous: B.foo and M.foo both match — no METHOD_IMPLEMENTS edge const mi = graph.relationships.filter((r) => r.type === 'METHOD_IMPLEMENTS'); const fooEdges = mi.filter((e) => graph.getNode(e.targetId)?.properties.name === 'foo'); expect(fooEdges).toHaveLength(0); }); it('diamond dedup: same method via two paths is NOT ambiguous', () => { // I { foo() }, GrandBase { foo() }, B extends GrandBase, M extends GrandBase // C extends B + M, C implements I const graph = createKnowledgeGraph(); addClass(graph, 'I', 'cpp', 'Interface'); addClass(graph, 'GrandBase', 'cpp'); addClass(graph, 'B', 'cpp'); addClass(graph, 'M', 'cpp'); addClass(graph, 'C', 'cpp'); addImplements(graph, 'C', 'I'); addExtends(graph, 'C', 'B'); addExtends(graph, 'C', 'M'); addExtends(graph, 'B', 'GrandBase'); addExtends(graph, 'M', 'GrandBase'); addMethod(graph, 'I', 'foo', 'Interface'); const gbFoo = addMethod(graph, 'GrandBase', 'foo'); // B and M have NO own foo — both inherit from GrandBase const result = computeMRO(graph); // Not ambiguous: same GrandBase.foo via both paths const mi = graph.relationships.filter((r) => r.type === 'METHOD_IMPLEMENTS'); const fooEdge = mi.find((e) => e.sourceId === gbFoo); expect(fooEdge).toBeDefined(); }); it('C extends B extends A, B and A both have foo → returns B.foo (nearest)', () => { // I { foo() }, A { foo() }, B extends A { foo() }, C extends B implements I { no foo } const graph = createKnowledgeGraph(); addClass(graph, 'I', 'java', 'Interface'); addClass(graph, 'A', 'java'); addClass(graph, 'B', 'java'); addClass(graph, 'C', 'java'); addImplements(graph, 'C', 'I'); addExtends(graph, 'C', 'B'); addExtends(graph, 'B', 'A'); addMethod(graph, 'I', 'foo', 'Interface'); addMethod(graph, 'A', 'foo'); const bFoo = addMethod(graph, 'B', 'foo'); // C has NO own foo — nearest is B.foo at depth 1 computeMRO(graph); const mi = graph.relationships.filter((r) => r.type === 'METHOD_IMPLEMENTS'); const fooEdge = mi.find((e) => e.sourceId === bFoo); expect(fooEdge).toBeDefined(); // A.foo should NOT be reached const aFooId = generateId('Method', 'A.foo#0'); const aFooEdge = mi.find((e) => e.sourceId === aFooId); expect(aFooEdge).toBeUndefined(); }); it('C extends B extends A, only A has foo → returns A.foo (single match at depth 2)', () => { // I { foo() }, A { foo() }, B extends A { no foo }, C extends B implements I { no foo } const graph = createKnowledgeGraph(); addClass(graph, 'I', 'java', 'Interface'); addClass(graph, 'A', 'java'); addClass(graph, 'B', 'java'); addClass(graph, 'C', 'java'); addImplements(graph, 'C', 'I'); addExtends(graph, 'C', 'B'); addExtends(graph, 'B', 'A'); addMethod(graph, 'I', 'foo', 'Interface'); const aFoo = addMethod(graph, 'A', 'foo'); // B has NO foo, C has NO foo — only A.foo at depth 2 computeMRO(graph); const mi = graph.relationships.filter((r) => r.type === 'METHOD_IMPLEMENTS'); const fooEdge = mi.find((e) => e.sourceId === aFoo); expect(fooEdge).toBeDefined(); }); }); // ---- METHOD_IMPLEMENTS concrete-source guard ---------------------------- describe('METHOD_IMPLEMENTS concrete-source guard', () => { it('interface B extends interface A, B redeclares foo → 0 METHOD_IMPLEMENTS', () => { const graph = createKnowledgeGraph(); addClass(graph, 'A', 'java', 'Interface'); addClass(graph, 'B', 'java', 'Interface'); addInterfaceExtends(graph, 'B', 'A'); addMethod(graph, 'A', 'foo', 'Interface'); addMethod(graph, 'B', 'foo', 'Interface'); computeMRO(graph); const mi = graph.relationships.filter((r) => r.type === 'METHOD_IMPLEMENTS'); expect(mi).toHaveLength(0); }); it('abstract class C implements I, C has abstract foo → 0 METHOD_IMPLEMENTS for foo', () => { const graph = createKnowledgeGraph(); addClass(graph, 'I', 'java', 'Interface'); addClass(graph, 'C', 'java', 'Class'); addImplements(graph, 'C', 'I'); addMethod(graph, 'I', 'foo', 'Interface'); // Add abstract method manually with isAbstract flag const classId = generateId('Class', 'C'); const methodId = generateId('Method', 'C.foo#0'); graph.addNode({ id: methodId, label: 'Method', properties: { name: 'foo', filePath: 'src/C.ts', isAbstract: true, parameterCount: 0 }, }); graph.addRelationship({ id: generateId('HAS_METHOD', `${classId}->${methodId}`), sourceId: classId, targetId: methodId, type: 'HAS_METHOD', confidence: 1.0, reason: '', }); computeMRO(graph); const mi = graph.relationships.filter((r) => r.type === 'METHOD_IMPLEMENTS'); expect(mi).toHaveLength(0); }); it('abstract class C implements I, C has concrete bar → 1 METHOD_IMPLEMENTS for bar', () => { const graph = createKnowledgeGraph(); addClass(graph, 'I', 'java', 'Interface'); addClass(graph, 'C', 'java', 'Class'); addImplements(graph, 'C', 'I'); addMethod(graph, 'I', 'bar', 'Interface'); const cBar = addMethod(graph, 'C', 'bar'); computeMRO(graph); const mi = graph.relationships.filter((r) => r.type === 'METHOD_IMPLEMENTS'); expect(mi).toHaveLength(1); expect(mi[0].sourceId).toBe(cBar); }); it('concrete class implements interface → 1 METHOD_IMPLEMENTS (regression)', () => { const graph = createKnowledgeGraph(); addClass(graph, 'I', 'java', 'Interface'); addClass(graph, 'C', 'java', 'Class'); addImplements(graph, 'C', 'I'); addMethod(graph, 'I', 'foo', 'Interface'); const cFoo = addMethod(graph, 'C', 'foo'); computeMRO(graph); const mi = graph.relationships.filter((r) => r.type === 'METHOD_IMPLEMENTS'); expect(mi).toHaveLength(1); expect(mi[0].sourceId).toBe(cFoo); }); }); describe('default interface method resolution', () => { it('interface default method satisfies grandparent interface contract', () => { // I1 has abstract bar, I2 extends I1 and provides concrete bar, // C implements I2 with no own bar → edge from I2.bar → I1.bar const graph = createKnowledgeGraph(); addClass(graph, 'I1Def', 'java', 'Interface'); addClass(graph, 'I2Def', 'java', 'Interface'); addClass(graph, 'CDef', 'java'); addInterfaceExtends(graph, 'I2Def', 'I1Def'); addImplements(graph, 'CDef', 'I2Def'); const i1Bar = addMethod(graph, 'I1Def', 'bar', 'Interface', undefined, { isAbstract: true }); const i2Bar = addMethod(graph, 'I2Def', 'bar', 'Interface', undefined, { isAbstract: false, }); const result = computeMRO(graph); const edges = graph.relationships.filter((r) => r.type === 'METHOD_IMPLEMENTS'); expect(edges).toHaveLength(1); expect(edges[0].sourceId).toBe(i2Bar); expect(edges[0].targetId).toBe(i1Bar); }); it('own method takes priority over interface default', () => { // I has concrete default bar, C implements I and has own bar // → edge from C.bar → I.bar (own method wins, no IMPLEMENTS fallback needed) const graph = createKnowledgeGraph(); addClass(graph, 'IOwn', 'java', 'Interface'); addClass(graph, 'COwn', 'java'); addImplements(graph, 'COwn', 'IOwn'); const iBar = addMethod(graph, 'IOwn', 'bar', 'Interface', undefined, { isAbstract: false }); const cBar = addMethod(graph, 'COwn', 'bar'); computeMRO(graph); const edges = graph.relationships.filter((r) => r.type === 'METHOD_IMPLEMENTS'); expect(edges).toHaveLength(1); expect(edges[0].sourceId).toBe(cBar); expect(edges[0].targetId).toBe(iBar); }); it('transitive interface default: C implements I2, I2 extends I1, I1 has abstract bar, I2 has default bar → I2.bar satisfies I1.bar', () => { // I1 (Interface) has abstract bar // I2 (Interface) extends I1, has concrete default bar // C (Class) implements I2, has NO bar // The main emitter processes CImpl's ancestor I1 (transitive via I2). // I1.bar is abstract → CImpl has no own bar → findInheritedMethod runs. // EXTENDS BFS: nothing. IMPLEMENTS BFS: walks I2 → finds concrete I2.bar. // Edge: I2.bar → I1.bar const graph = createKnowledgeGraph(); addClass(graph, 'I1', 'java', 'Interface'); addClass(graph, 'I2', 'java', 'Interface'); addClass(graph, 'CImpl', 'java'); // I1 has abstract bar const i1Bar = addMethod(graph, 'I1', 'bar', 'Interface', undefined, { isAbstract: true }); // I2 has concrete default bar const i2Bar = addMethod(graph, 'I2', 'bar', 'Interface'); // I2 extends I1 const i2Id = generateId('Interface', 'I2'); const i1Id = generateId('Interface', 'I1'); graph.addRelationship({ id: generateId('EXTENDS', `${i2Id}->${i1Id}`), sourceId: i2Id, targetId: i1Id, type: 'EXTENDS', confidence: 1.0, reason: '', }); // CImpl implements I2 addImplements(graph, 'CImpl', 'I2'); const result = computeMRO(graph); const mi = graph.relationships.filter((r) => r.type === 'METHOD_IMPLEMENTS'); // I2.bar (concrete default) satisfies I1.bar (abstract contract) const barEdge = mi.find((e) => e.targetId === i1Bar && e.sourceId === i2Bar); expect(barEdge).toBeDefined(); }); it('EXTENDS method takes priority over interface default', () => { // I has concrete default foo, Base has concrete foo, // C extends Base and implements I with no own foo // → edge from Base.foo → I.foo (EXTENDS wins over IMPLEMENTS default) const graph = createKnowledgeGraph(); addClass(graph, 'IExtPri', 'java', 'Interface'); addClass(graph, 'BaseExtPri', 'java'); addClass(graph, 'CExtPri', 'java'); addExtends(graph, 'CExtPri', 'BaseExtPri'); addImplements(graph, 'CExtPri', 'IExtPri'); const iFoo = addMethod(graph, 'IExtPri', 'foo', 'Interface', undefined, { isAbstract: false, }); const baseFoo = addMethod(graph, 'BaseExtPri', 'foo'); computeMRO(graph); const edges = graph.relationships.filter((r) => r.type === 'METHOD_IMPLEMENTS'); expect(edges).toHaveLength(1); expect(edges[0].sourceId).toBe(baseFoo); expect(edges[0].targetId).toBe(iFoo); }); it('Dart implements Class — does NOT inherit concrete method bodies', () => { // Dart: class C implements AbstractBase (labeled Class, not Interface) // AbstractBase has concrete method foo // C has NO foo — but Dart implements does NOT inherit bodies // → 0 METHOD_IMPLEMENTS edges from the IMPLEMENTS fallback const graph = createKnowledgeGraph(); addClass(graph, 'AbstractBase', 'dart'); // Class label, not Interface addClass(graph, 'DartImpl', 'dart'); // AbstractBase has concrete foo addMethod(graph, 'AbstractBase', 'foo'); // DartImpl implements AbstractBase (IMPLEMENTS edge to a Class) addImplements(graph, 'DartImpl', 'AbstractBase', 'Class', 'Interface'); // But we need AbstractBase to be a Class, not Interface — fix the label // Actually addImplements creates the edge, but AbstractBase was added as Class. // The IMPLEMENTS edge target needs to match the actual node ID. // Let's do this manually: const dartImplId = generateId('Class', 'DartImpl'); const absBaseId = generateId('Class', 'AbstractBase'); graph.addRelationship({ id: generateId('IMPLEMENTS', `${dartImplId}->${absBaseId}`), sourceId: dartImplId, targetId: absBaseId, type: 'IMPLEMENTS', confidence: 1.0, reason: '', }); computeMRO(graph); const mi = graph.relationships.filter((r) => r.type === 'METHOD_IMPLEMENTS'); // No edges — IMPLEMENTS fallback skips Class-labeled parents expect(mi).toHaveLength(0); }); it('Interface default still works after Dart label gate', () => { const graph = createKnowledgeGraph(); addClass(graph, 'IDefault', 'java', 'Interface'); addClass(graph, 'Impl', 'java'); addImplements(graph, 'Impl', 'IDefault'); // IDefault has abstract contract method const iFoo = addMethod(graph, 'IDefault', 'foo', 'Interface', undefined, { isAbstract: true, }); // IDefault also has concrete default bar const iBar = addMethod(graph, 'IDefault', 'bar', 'Interface'); // Impl has foo but not bar addMethod(graph, 'Impl', 'foo'); computeMRO(graph); const mi = graph.relationships.filter((r) => r.type === 'METHOD_IMPLEMENTS'); // foo: own method matches → edge from Impl.foo → IDefault.foo const fooEdge = mi.find((e) => e.targetId === iFoo); expect(fooEdge).toBeDefined(); // bar: no own method, IMPLEMENTS fallback finds IDefault.bar (Interface label OK) const barEdge = mi.find((e) => e.sourceId === iBar && e.targetId === iBar); // Actually bar is the same method — it's the default implementation satisfying itself. // The emitter processes IDefault.bar as an ancestor method, Impl has no bar, // findInheritedMethod runs, walks IMPLEMENTS → finds IDefault.bar (non-abstract). // But excludeMethodId = ancestorMethodId = iBar → skipped to prevent self-edge! // So no bar edge. This is correct — the default satisfies the contract inherently. }); }); describe('METHOD_IMPLEMENTS confidence tiering', () => { it('fully-typed match gets confidence 1.0', () => { const graph = createKnowledgeGraph(); addClass(graph, 'ITyped', 'java', 'Interface'); addClass(graph, 'CTyped', 'java'); addImplements(graph, 'CTyped', 'ITyped'); const iFoo = addMethod(graph, 'ITyped', 'foo', 'Interface', ['int', 'String']); const cFoo = addMethod(graph, 'CTyped', 'foo', 'Class', ['int', 'String']); computeMRO(graph); const edges = graph.relationships.filter((r) => r.type === 'METHOD_IMPLEMENTS'); expect(edges).toHaveLength(1); expect(edges[0].sourceId).toBe(cFoo); expect(edges[0].targetId).toBe(iFoo); expect(edges[0].confidence).toBe(1.0); }); it('arity-only match (both have parameterCount, no types) gets confidence 1.0', () => { const graph = createKnowledgeGraph(); addClass(graph, 'IArity', 'java', 'Interface'); addClass(graph, 'CArity', 'java'); addImplements(graph, 'CArity', 'IArity'); // Manually add methods with parameterCount but no parameterTypes const iBarId = generateId('Method', 'IArity.bar#2'); graph.addNode({ id: iBarId, label: 'Method', properties: { name: 'bar', filePath: 'src/IArity.ts', parameterCount: 2 }, }); graph.addRelationship({ id: generateId('HAS_METHOD', `${generateId('Interface', 'IArity')}->${iBarId}`), sourceId: generateId('Interface', 'IArity'), targetId: iBarId, type: 'HAS_METHOD', confidence: 1.0, reason: '', }); const cBarId = generateId('Method', 'CArity.bar#2'); graph.addNode({ id: cBarId, label: 'Method', properties: { name: 'bar', filePath: 'src/CArity.ts', parameterCount: 2 }, }); graph.addRelationship({ id: generateId('HAS_METHOD', `${generateId('Class', 'CArity')}->${cBarId}`), sourceId: generateId('Class', 'CArity'), targetId: cBarId, type: 'HAS_METHOD', confidence: 1.0, reason: '', }); computeMRO(graph); const edges = graph.relationships.filter((r) => r.type === 'METHOD_IMPLEMENTS'); expect(edges).toHaveLength(1); expect(edges[0].sourceId).toBe(cBarId); expect(edges[0].targetId).toBe(iBarId); expect(edges[0].confidence).toBe(1.0); }); it('lenient match (no types, no parameterCount on both sides) gets confidence 0.7', () => { const graph = createKnowledgeGraph(); addClass(graph, 'ILenient', 'java', 'Interface'); addClass(graph, 'CLenient', 'java'); addImplements(graph, 'CLenient', 'ILenient'); // Manually create methods WITHOUT parameterCount to simulate legacy/missing arity const iBazId = generateId('Method', 'ILenient.baz'); graph.addNode({ id: iBazId, label: 'Method', properties: { name: 'baz', filePath: 'src/ILenient.ts' }, }); graph.addRelationship({ id: generateId('HAS_METHOD', `${generateId('Interface', 'ILenient')}->${iBazId}`), sourceId: generateId('Interface', 'ILenient'), targetId: iBazId, type: 'HAS_METHOD', confidence: 1.0, reason: '', }); const cBazId = generateId('Method', 'CLenient.baz'); graph.addNode({ id: cBazId, label: 'Method', properties: { name: 'baz', filePath: 'src/CLenient.ts' }, }); graph.addRelationship({ id: generateId('HAS_METHOD', `${generateId('Class', 'CLenient')}->${cBazId}`), sourceId: generateId('Class', 'CLenient'), targetId: cBazId, type: 'HAS_METHOD', confidence: 1.0, reason: '', }); computeMRO(graph); const edges = graph.relationships.filter((r) => r.type === 'METHOD_IMPLEMENTS'); expect(edges).toHaveLength(1); expect(edges[0].sourceId).toBe(cBazId); expect(edges[0].targetId).toBe(iBazId); expect(edges[0].confidence).toBe(0.7); }); it('one side has parameterCount, other does not → confidence 0.7', () => { const graph = createKnowledgeGraph(); addClass(graph, 'IHalf', 'java', 'Interface'); addClass(graph, 'CHalf', 'java'); addImplements(graph, 'CHalf', 'IHalf'); // Interface method has parameterCount but no parameterTypes const iQuxId = generateId('Method', 'IHalf.qux#2'); graph.addNode({ id: iQuxId, label: 'Method', properties: { name: 'qux', filePath: 'src/IHalf.ts', parameterCount: 2 }, }); graph.addRelationship({ id: generateId('HAS_METHOD', `${generateId('Interface', 'IHalf')}->${iQuxId}`), sourceId: generateId('Interface', 'IHalf'), targetId: iQuxId, type: 'HAS_METHOD', confidence: 1.0, reason: '', }); // Class method has neither parameterTypes nor parameterCount (manually constructed) const cQuxId = generateId('Method', 'CHalf.qux'); graph.addNode({ id: cQuxId, label: 'Method', properties: { name: 'qux', filePath: 'src/CHalf.ts' }, }); graph.addRelationship({ id: generateId('HAS_METHOD', `${generateId('Class', 'CHalf')}->${cQuxId}`), sourceId: generateId('Class', 'CHalf'), targetId: cQuxId, type: 'HAS_METHOD', confidence: 1.0, reason: '', }); computeMRO(graph); const edges = graph.relationships.filter((r) => r.type === 'METHOD_IMPLEMENTS'); expect(edges).toHaveLength(1); expect(edges[0].sourceId).toBe(cQuxId); expect(edges[0].targetId).toBe(iQuxId); expect(edges[0].confidence).toBe(0.7); }); }); // ---- IMPLEMENTS BFS ambiguity for default methods ------------------------- describe('IMPLEMENTS BFS ambiguity for default methods', () => { it('does not emit METHOD_IMPLEMENTS when two interfaces provide same default method (ambiguous)', () => { // IAncestor (Interface) has abstract process() // IAlpha (Interface) extends IAncestor, has concrete process() // IBeta (Interface) extends IAncestor, has concrete process() // CImpl (Class) implements IAlpha, implements IBeta // CImpl has NO process() method // // findInheritedMethod walks IMPLEMENTS BFS and finds process() in BOTH // IAlpha and IBeta => ambiguous => null => no METHOD_IMPLEMENTS edge. const graph = createKnowledgeGraph(); addClass(graph, 'IAncestor', 'java', 'Interface'); addClass(graph, 'IAlpha', 'java', 'Interface'); addClass(graph, 'IBeta', 'java', 'Interface'); addClass(graph, 'CImpl', 'java'); addInterfaceExtends(graph, 'IAlpha', 'IAncestor'); addInterfaceExtends(graph, 'IBeta', 'IAncestor'); addImplements(graph, 'CImpl', 'IAlpha'); addImplements(graph, 'CImpl', 'IBeta'); addMethod(graph, 'IAncestor', 'process', 'Interface', undefined, { isAbstract: true }); addMethod(graph, 'IAlpha', 'process', 'Interface'); addMethod(graph, 'IBeta', 'process', 'Interface'); const result = computeMRO(graph); // IAlpha.process -> IAncestor.process and IBeta.process -> IAncestor.process // are legitimate edges from sub-interface processing. The ambiguity check // ensures that NO additional edge is emitted on behalf of CImpl (which has // no own process()). Since CImpl has no methods, no edge should be sourced // from a CImpl method. Verify by checking that the only METHOD_IMPLEMENTS // edges for process() are the two interface-to-interface ones. const edges = graph.relationships.filter((r) => r.type === 'METHOD_IMPLEMENTS'); const processEdges = edges.filter((e) => { const target = graph.getNode(e.targetId); return target?.properties.name === 'process'; }); // IAlpha.process->IAncestor.process and IBeta.process->IAncestor.process // are emitted from the sub-interface processing (each concrete method // implements the ancestor's abstract method). No additional edge should be // emitted on behalf of CImpl because findInheritedMethod returns null // (ambiguous: two candidates at the same BFS depth). expect(processEdges).toHaveLength(2); const alphaProcess = generateId('Method', 'IAlpha.process#0'); const betaProcess = generateId('Method', 'IBeta.process#0'); const sourceIds = processEdges.map((e) => e.sourceId).sort(); expect(sourceIds).toEqual([alphaProcess, betaProcess].sort()); }); it('emits METHOD_IMPLEMENTS when only one interface provides the default method (unambiguous)', () => { // IAncestor (Interface) has abstract process() // IAlpha (Interface) extends IAncestor, has concrete process() // IBeta (Interface) extends IAncestor, does NOT have process() // CImpl (Class) implements IAlpha, implements IBeta // CImpl has NO process() method // // findInheritedMethod walks IMPLEMENTS BFS and finds process() only in // IAlpha => unambiguous => emits 1 METHOD_IMPLEMENTS edge. const graph = createKnowledgeGraph(); addClass(graph, 'IAncestor', 'java', 'Interface'); addClass(graph, 'IAlpha', 'java', 'Interface'); addClass(graph, 'IBeta', 'java', 'Interface'); addClass(graph, 'CImpl', 'java'); addInterfaceExtends(graph, 'IAlpha', 'IAncestor'); addInterfaceExtends(graph, 'IBeta', 'IAncestor'); addImplements(graph, 'CImpl', 'IAlpha'); addImplements(graph, 'CImpl', 'IBeta'); const ancestorProcess = addMethod(graph, 'IAncestor', 'process', 'Interface', undefined, { isAbstract: true, }); const alphaProcess = addMethod(graph, 'IAlpha', 'process', 'Interface'); // IBeta has no process() method const result = computeMRO(graph); const edges = graph.relationships.filter((r) => r.type === 'METHOD_IMPLEMENTS'); const processEdges = edges.filter((e) => { const target = graph.getNode(e.targetId); return target?.properties.name === 'process'; }); // At minimum, IAlpha.process -> IAncestor.process is emitted (from IAlpha's // own processing). CImpl's findInheritedMethod also finds IAlpha.process as // the sole unambiguous match, potentially emitting the same edge again. expect(processEdges.length).toBeGreaterThanOrEqual(1); // Every process edge should point from IAlpha.process to IAncestor.process for (const edge of processEdges) { expect(edge.sourceId).toBe(alphaProcess); expect(edge.targetId).toBe(ancestorProcess); } }); }); // ---- PHM parent-order pinning ------------------------------------------ // // PHM Unit 2 split buildAdjacency's single forEachRelationship into three // typed iterations (EXTENDS, IMPLEMENTS, HAS_METHOD). Parent enumeration // now runs ALL EXTENDS edges before ANY IMPLEMENTS edges. For classes // with parents added in interleaved order, this re-orders `parentMap` // and any C3 linearization that consumes it. // // Python (single EXTENDS model) and Java/C# (resolveCsharpJava partitions // by edge type regardless of order) are unaffected in practice. This // test pins the new behavior so a future "simplification" back to a // single loop would surface as a deliberate change rather than a silent // semantic drift. describe('PHM: interleaved EXTENDS + IMPLEMENTS parent ordering', () => { it('class methods win regardless of the order EXTENDS/IMPLEMENTS edges were added', () => { const graph = createKnowledgeGraph(); // C extends Base (class) AND implements Iface (interface). Edges // added in INTERLEAVED order: IMPLEMENTS first, then EXTENDS. // Under the old single-loop adjacency, parentMap[C] would be // [IfaceId, BaseId]. Under the new grouped adjacency, // parentMap[C] is [BaseId, IfaceId] (EXTENDS bucket first). // // For resolveCsharpJava, class-method-wins is invariant to parent // order — both produce the same winner. This test encodes that // invariant, guarding the behavioral claim that 'Java/C# are // unaffected' in the PHM commit message. addClass(graph, 'Base', 'java'); addClass(graph, 'C', 'java'); addClass(graph, 'Iface', 'java', 'Interface'); addMethod(graph, 'Base', 'greet'); addMethod(graph, 'Iface', 'greet', 'Interface'); addMethod(graph, 'C', 'greet'); // Add IMPLEMENTS BEFORE EXTENDS to exercise interleaving. addImplements(graph, 'C', 'Iface'); addExtends(graph, 'C', 'Base'); const result = computeMRO(graph); const cId = generateId('Class', 'C'); const entry = result.entries.find((e) => e.classId === cId); expect(entry).toBeDefined(); const mro = entry!.mro; // Grouped iteration yields EXTENDS parents first. This pin fails // loudly if a future refactor reverts the typed-bucket iteration // to a single full-graph scan and restores insertion-order // semantics. // Grouped EXTENDS-before-IMPLEMENTS iteration produces this exact // MRO for C: [Base, Iface]. A single-loop reversion would yield // [Iface, Base] (IMPLEMENTS added first in this test). expect(mro).toEqual(['Base', 'Iface']); }); }); });