feat: METHOD_IMPLEMENTS edges + rename OVERRIDES to METHOD_OVERRIDES (#574)

Add METHOD_IMPLEMENTS relationship type to connect concrete methods to
the interface/abstract methods they implement, and rename OVERRIDES to
METHOD_OVERRIDES for naming consistency.

METHOD_IMPLEMENTS:
- New emitMethodImplementsEdges() pass in mro-processor after MRO loop
- Match by name + parameterTypes for overloaded interface methods
- Lenient matching when either side lacks type info
- Include ALL interface/trait methods (not just abstract)
- Edge direction: ConcreteMethod → InterfaceMethod
- 8 unit tests: interface dispatch, Rust trait, overloads, default
  methods, class-extends exclusion, Property skip, queryability
- 5 per-language integration tests: Java (interface-dispatch fixture),
  PHP (abstract-dispatch), Rust (trait impl), Swift (protocol), Python
  (documents ABC-extends behavior)

OVERRIDES → METHOD_OVERRIDES:
- Renamed across 24 files (shared types, schema, MRO processor, MCP
  tools, impact confidence, all resolver integration tests)
- Added migration in ensureInitialized: existing graphs with old
  OVERRIDES edges are auto-updated to METHOD_OVERRIDES on first connect
- Migration runs once per session per repo, silently skips if no edges

Closes #574
This commit is contained in:
Gergo Magyar 2026-04-03 16:47:17 +01:00
parent 63fc4c795f
commit 1966242675
24 changed files with 477 additions and 43 deletions

View file

@ -97,7 +97,8 @@ export type RelationshipType =
| 'CONTAINS'
| 'CALLS'
| 'INHERITS'
| 'OVERRIDES'
| 'METHOD_OVERRIDES'
| 'METHOD_IMPLEMENTS'
| 'IMPORTS'
| 'USES'
| 'DEFINES'

View file

@ -55,7 +55,8 @@ export const REL_TYPES = [
'HAS_METHOD',
'HAS_PROPERTY',
'ACCESSES',
'OVERRIDES',
'METHOD_OVERRIDES',
'METHOD_IMPLEMENTS',
'MEMBER_OF',
'STEP_IN_PROCESS',
'HANDLES_ROUTE',

View file

@ -3,7 +3,7 @@
*
* Walks the inheritance DAG (EXTENDS/IMPLEMENTS edges), collects methods from
* each ancestor via HAS_METHOD edges, detects method-name collisions across
* parents, and applies language-specific resolution rules to emit OVERRIDES edges.
* parents, and applies language-specific resolution rules to emit METHOD_OVERRIDES edges.
*
* Language-specific rules:
* - C++: leftmost base class in declaration order wins
@ -13,10 +13,10 @@
* - Rust: no auto-resolution — requires qualified syntax, resolvedTo = null
* - Default: single inheritance — first definition wins
*
* OVERRIDES edge direction: Class → Method (not Method → Method).
* METHOD_OVERRIDES edge direction: Class → Method (not Method → Method).
* The source is the child class that inherits conflicting methods,
* the target is the winning ancestor method node.
* Cypher: MATCH (c:Class)-[r:CodeRelation {type: 'OVERRIDES'}]->(m:Method)
* Cypher: MATCH (c:Class)-[r:CodeRelation {type: 'METHOD_OVERRIDES'}]->(m:Method)
*/
import { KnowledgeGraph } from '../graph/types.js';
@ -47,6 +47,7 @@ export interface MROResult {
entries: MROEntry[];
overrideEdges: number;
ambiguityCount: number;
methodImplementsEdges: number;
}
// ---------------------------------------------------------------------------
@ -401,13 +402,13 @@ export function computeMRO(graph: KnowledgeGraph): MROResult {
ambiguityCount++;
}
// Emit OVERRIDES edge if resolution found
// Emit METHOD_OVERRIDES edge if resolution found
if (resolution.resolvedTo !== null) {
graph.addRelationship({
id: generateId('OVERRIDES', `${classId}->${resolution.resolvedTo}`),
id: generateId('METHOD_OVERRIDES', `${classId}->${resolution.resolvedTo}`),
sourceId: classId,
targetId: resolution.resolvedTo,
type: 'OVERRIDES',
type: 'METHOD_OVERRIDES',
confidence: resolution.confidence,
reason: resolution.reason,
});
@ -424,7 +425,112 @@ export function computeMRO(graph: KnowledgeGraph): MROResult {
});
}
return { entries, overrideEdges, ambiguityCount };
const methodImplementsEdges = emitMethodImplementsEdges(
graph,
parentMap,
methodMap,
parentEdgeType,
);
return { entries, overrideEdges, ambiguityCount, methodImplementsEdges };
}
// ---------------------------------------------------------------------------
// METHOD_IMPLEMENTS edge emission
// ---------------------------------------------------------------------------
/**
* Check if two parameter type arrays match.
* Lenient: if either side has no type info, match by name only.
*/
function parameterTypesMatch(a: string[], b: string[]): boolean {
if (a.length === 0 || b.length === 0) return true;
if (a.length !== b.length) return false;
return a.every((t, i) => t === b[i]);
}
/**
* For each concrete class that implements/extends an interface or trait,
* find methods in the class that implement methods defined in the interface
* and emit METHOD_IMPLEMENTS edges: ConcreteMethod → InterfaceMethod.
*/
function emitMethodImplementsEdges(
graph: KnowledgeGraph,
parentMap: Map<string, string[]>,
methodMap: Map<string, string[]>,
parentEdgeType: Map<string, Map<string, 'EXTENDS' | 'IMPLEMENTS'>>,
): number {
let edgeCount = 0;
for (const [classId, parentIds] of parentMap) {
const classNode = graph.getNode(classId);
if (!classNode) continue;
// Get this class's own methods
const ownMethodIds = methodMap.get(classId) ?? [];
if (ownMethodIds.length === 0) continue;
// Build a lookup: methodName → Array<{methodId, parameterTypes}> for own methods
const ownMethodsByName = new Map<
string,
Array<{ methodId: string; parameterTypes: string[] }>
>();
for (const methodId of ownMethodIds) {
const methodNode = graph.getNode(methodId);
if (!methodNode || methodNode.label === 'Property') continue;
const name = methodNode.properties.name as string;
const parameterTypes = (methodNode.properties.parameterTypes as string[] | undefined) ?? [];
let bucket = ownMethodsByName.get(name);
if (!bucket) {
bucket = [];
ownMethodsByName.set(name, bucket);
}
bucket.push({ methodId, parameterTypes });
}
// For each parent, check if it's an interface/trait or connected via IMPLEMENTS
for (const parentId of parentIds) {
const parentNode = graph.getNode(parentId);
if (!parentNode) continue;
const isInterfaceLike = parentNode.label === 'Interface' || parentNode.label === 'Trait';
const edgeType = parentEdgeType.get(classId)?.get(parentId);
if (!isInterfaceLike && edgeType !== 'IMPLEMENTS') continue;
// Get parent's methods
const parentMethodIds = methodMap.get(parentId) ?? [];
for (const parentMethodId of parentMethodIds) {
const parentMethodNode = graph.getNode(parentMethodId);
if (!parentMethodNode || parentMethodNode.label === 'Property') continue;
const parentName = parentMethodNode.properties.name as string;
const parentParamTypes =
(parentMethodNode.properties.parameterTypes as string[] | undefined) ?? [];
// Find matching method in own class by name + parameterTypes
const candidates = ownMethodsByName.get(parentName);
if (!candidates) continue;
for (const candidate of candidates) {
if (parameterTypesMatch(candidate.parameterTypes, parentParamTypes)) {
graph.addRelationship({
id: generateId('METHOD_IMPLEMENTS', `${candidate.methodId}->${parentMethodId}`),
sourceId: candidate.methodId,
targetId: parentMethodId,
type: 'METHOD_IMPLEMENTS',
confidence: 1.0,
reason: '',
});
edgeCount++;
break; // first match wins for this parent method
}
}
}
}
}
return edgeCount;
}
/**

View file

@ -1103,7 +1103,7 @@ async function runChunkedParseAndResolve(
* Post-parse graph analysis: MRO, community detection, process extraction.
*
* @reads graph (all nodes and relationships from parse + resolve phases)
* @writes graph (Community nodes, Process nodes, MEMBER_OF edges, STEP_IN_PROCESS edges, OVERRIDES edges)
* @writes graph (Community nodes, Process nodes, MEMBER_OF edges, STEP_IN_PROCESS edges, METHOD_OVERRIDES edges)
*/
async function runGraphAnalysisPhases(
graph: ReturnType<typeof createKnowledgeGraph>,
@ -1126,7 +1126,7 @@ async function runGraphAnalysisPhases(
const mroResult = computeMRO(graph);
if (isDev && mroResult.entries.length > 0) {
console.log(
`🔀 MRO: ${mroResult.entries.length} classes analyzed, ${mroResult.ambiguityCount} ambiguities found, ${mroResult.overrideEdges} OVERRIDES edges`,
`🔀 MRO: ${mroResult.entries.length} classes analyzed, ${mroResult.ambiguityCount} ambiguities, ${mroResult.overrideEdges} METHOD_OVERRIDES, ${mroResult.methodImplementsEdges} METHOD_IMPLEMENTS`,
);
}

View file

@ -96,7 +96,7 @@ export const VALID_RELATION_TYPES = new Set([
'IMPLEMENTS',
'HAS_METHOD',
'HAS_PROPERTY',
'OVERRIDES',
'METHOD_OVERRIDES',
'ACCESSES',
'HANDLES_ROUTE',
'FETCHES',
@ -117,7 +117,7 @@ export const VALID_RELATION_TYPES = new Set([
* CALLS / IMPORTS – direct, strongly-typed references → 0.9
* EXTENDS – class hierarchy, statically verifiable → 0.85
* IMPLEMENTS – interface contract, statically verifiable → 0.85
* OVERRIDES – method override, statically verifiable → 0.85
* METHOD_OVERRIDES – method override, statically verifiable → 0.85
* HAS_METHOD – structural containment → 0.95
* HAS_PROPERTY – structural containment → 0.95
* ACCESSES – field read/write, may be indirect → 0.8
@ -129,7 +129,7 @@ export const IMPACT_RELATION_CONFIDENCE: Readonly<Record<string, number>> = {
IMPORTS: 0.9,
EXTENDS: 0.85,
IMPLEMENTS: 0.85,
OVERRIDES: 0.85,
METHOD_OVERRIDES: 0.85,
HAS_METHOD: 0.95,
HAS_PROPERTY: 0.95,
ACCESSES: 0.8,
@ -174,6 +174,7 @@ export class LocalBackend {
private repos: Map<string, RepoHandle> = new Map();
private contextCache: Map<string, CodebaseContext> = new Map();
private initializedRepos: Set<string> = new Set();
private migratedRepos: Set<string> = new Set();
private reinitPromises: Map<string, Promise<void>> = new Map();
private lastStalenessCheck: Map<string, number> = new Map();
private groupToolSvc: GroupService | null = null;
@ -406,6 +407,20 @@ export class LocalBackend {
try {
await initLbug(repoId, handle.lbugPath);
this.initializedRepos.add(repoId);
// Migrate legacy OVERRIDES → METHOD_OVERRIDES (once per session per repo)
// TODO: remove this migration after a few releases once most users have migrated indexes
if (!this.migratedRepos.has(repoId)) {
this.migratedRepos.add(repoId);
try {
await executeParameterized(
repoId,
`MATCH ()-[r:CodeRelation {type: 'OVERRIDES'}]->() SET r.type = 'METHOD_OVERRIDES'`,
{},
);
} catch {
/* Old index may not have OVERRIDES edges — ignore */
}
}
} catch (err: any) {
// If lock error, mark as not initialized so next call retries
this.initializedRepos.delete(repoId);
@ -1200,7 +1215,7 @@ export class LocalBackend {
repo.id,
`
MATCH (caller)-[r:CodeRelation]->(n {id: $symId})
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'HAS_PROPERTY', 'OVERRIDES', 'ACCESSES']
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'HAS_PROPERTY', 'METHOD_OVERRIDES', 'ACCESSES']
RETURN r.type AS relType, caller.id AS uid, caller.name AS name, caller.filePath AS filePath, labels(caller)[0] AS kind
LIMIT 30
`,
@ -1290,7 +1305,7 @@ export class LocalBackend {
repo.id,
`
MATCH (n {id: $symId})-[r:CodeRelation]->(target)
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'HAS_PROPERTY', 'OVERRIDES', 'ACCESSES']
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'HAS_PROPERTY', 'METHOD_OVERRIDES', 'ACCESSES']
RETURN r.type AS relType, target.id AS uid, target.name AS name, target.filePath AS filePath, labels(target)[0] AS kind
LIMIT 30
`,

View file

@ -353,7 +353,8 @@ relationships:
- HAS_METHOD: Class/Struct/Interface owns a Method
- HAS_PROPERTY: Class/Struct/Interface owns a Property (field)
- ACCESSES: Function/Method reads or writes a Property (reason: 'read' or 'write')
- OVERRIDES: Method overrides another Method (MRO)
- METHOD_OVERRIDES: Method overrides another Method (MRO)
- METHOD_IMPLEMENTS: ConcreteMethod implements InterfaceMethod (matched by name + parameterTypes)
- MEMBER_OF: Symbol belongs to community
- STEP_IN_PROCESS: Symbol is step N in process

View file

@ -99,7 +99,7 @@ SCHEMA:
- Nodes: File, Folder, Function, Class, Interface, Method, CodeElement, Community, Process, Route, Tool
- Multi-language nodes (use backticks): \`Struct\`, \`Enum\`, \`Trait\`, \`Impl\`, etc.
- All edges via single CodeRelation table with 'type' property
- Edge types: CONTAINS, DEFINES, CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, HAS_PROPERTY, ACCESSES, OVERRIDES, MEMBER_OF, STEP_IN_PROCESS, HANDLES_ROUTE, FETCHES, HANDLES_TOOL, ENTRY_POINT_OF
- Edge types: CONTAINS, DEFINES, CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, HAS_PROPERTY, ACCESSES, METHOD_OVERRIDES, MEMBER_OF, STEP_IN_PROCESS, HANDLES_ROUTE, FETCHES, HANDLES_TOOL, ENTRY_POINT_OF
- Edge properties: type (STRING), confidence (DOUBLE), reason (STRING), step (INT32)
EXAMPLES:
@ -122,7 +122,7 @@ EXAMPLES:
MATCH (f:Function)-[r:CodeRelation {type: 'ACCESSES', reason: 'write'}]->(p:Property) WHERE p.name = "address" RETURN f.name, f.filePath
• Find method overrides (MRO resolution):
MATCH (winner:Method)-[r:CodeRelation {type: 'OVERRIDES'}]->(loser:Method) RETURN winner.name, winner.filePath, loser.filePath, r.reason
MATCH (winner:Method)-[r:CodeRelation {type: 'METHOD_OVERRIDES'}]->(loser:Method) RETURN winner.name, winner.filePath, loser.filePath, r.reason
• Detect diamond inheritance:
MATCH (d:Class)-[:CodeRelation {type: 'EXTENDS'}]->(b1), (d)-[:CodeRelation {type: 'EXTENDS'}]->(b2), (b1)-[:CodeRelation {type: 'EXTENDS'}]->(a), (b2)-[:CodeRelation {type: 'EXTENDS'}]->(a) WHERE b1 <> b2 RETURN d.name, b1.name, b2.name, a.name
@ -265,7 +265,7 @@ Depth groups:
TIP: Default traversal uses CALLS/IMPORTS/EXTENDS/IMPLEMENTS. For class members, include HAS_METHOD and HAS_PROPERTY in relationTypes. For field access analysis, include ACCESSES in relationTypes.
EdgeType: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, HAS_PROPERTY, OVERRIDES, ACCESSES
EdgeType: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, HAS_PROPERTY, METHOD_OVERRIDES, ACCESSES
Confidence: 1.0 = certain, <0.8 = fuzzy match`,
inputSchema: {
type: 'object',
@ -284,7 +284,7 @@ Confidence: 1.0 = certain, <0.8 = fuzzy match`,
type: 'array',
items: { type: 'string' },
description:
'Filter: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, HAS_PROPERTY, OVERRIDES, ACCESSES (default: usage-based, ACCESSES excluded by default)',
'Filter: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, HAS_PROPERTY, METHOD_OVERRIDES, ACCESSES (default: usage-based, ACCESSES excluded by default)',
},
includeTests: { type: 'boolean', description: 'Include test files (default: false)' },
minConfidence: { type: 'number', description: 'Minimum confidence 0-1 (default: 0.7)' },

View file

@ -34,7 +34,7 @@ export const LOCAL_BACKEND_SEED_DATA = [
CREATE (c)-[:CodeRelation {type: 'HAS_METHOD', confidence: 1.0, reason: 'class-method', step: 0}]->(m)`,
// OVERRIDES: AuthService.authenticate -> BaseService.authenticate
`MATCH (a:Method), (b:Method) WHERE a.id = 'method:AuthService.authenticate' AND b.id = 'method:BaseService.authenticate'
CREATE (a)-[:CodeRelation {type: 'OVERRIDES', confidence: 1.0, reason: 'mro-resolution', step: 0}]->(b)`,
CREATE (a)-[:CodeRelation {type: 'METHOD_OVERRIDES', confidence: 1.0, reason: 'mro-resolution', step: 0}]->(b)`,
// HAS_METHOD: BaseService -> authenticate
`MATCH (c:Class), (m:Method) WHERE c.id = 'class:BaseService' AND m.id = 'method:BaseService.authenticate'
CREATE (c)-[:CodeRelation {type: 'HAS_METHOD', confidence: 1.0, reason: 'class-method', step: 0}]->(m)`,

View file

@ -144,7 +144,7 @@ withTestLbugDB(
const result = await backend.callTool('impact', {
target: 'authenticate',
direction: 'downstream',
relationTypes: ['OVERRIDES'],
relationTypes: ['METHOD_OVERRIDES'],
});
expect(result).not.toHaveProperty('error');
// AuthService.authenticate overrides BaseService.authenticate

View file

@ -94,7 +94,7 @@ withTestLbugDB(
'EXTENDS',
'IMPLEMENTS',
'HAS_METHOD',
'OVERRIDES',
'METHOD_OVERRIDES',
'ACCESSES',
];
const invalidTypes = ['CONTAINS', 'STEP_IN_PROCESS', 'MEMBER_OF', 'DROP_TABLE'];

View file

@ -61,7 +61,7 @@ describe('C++ diamond inheritance', () => {
});
it('no OVERRIDES edges target Property nodes', () => {
const overrides = getRelationships(result, 'OVERRIDES');
const overrides = getRelationships(result, 'METHOD_OVERRIDES');
for (const edge of overrides) {
const target = result.graph.getNode(edge.rel.targetId);
expect(target).toBeDefined();

View file

@ -86,7 +86,7 @@ describe('C# heritage resolution', () => {
});
it('no OVERRIDES edges target Property nodes', () => {
const overrides = getRelationships(result, 'OVERRIDES');
const overrides = getRelationships(result, 'METHOD_OVERRIDES');
for (const edge of overrides) {
const target = result.graph.getNode(edge.rel.targetId);
expect(target).toBeDefined();

View file

@ -80,7 +80,7 @@ describe('Go package import & call resolution', () => {
});
it('no OVERRIDES edges target Property nodes', () => {
const overrides = getRelationships(result, 'OVERRIDES');
const overrides = getRelationships(result, 'METHOD_OVERRIDES');
for (const edge of overrides) {
const target = result.graph.getNode(edge.rel.targetId);
expect(target).toBeDefined();

View file

@ -71,7 +71,7 @@ describe('Java heritage resolution', () => {
});
it('no OVERRIDES edges target Property nodes', () => {
const overrides = getRelationships(result, 'OVERRIDES');
const overrides = getRelationships(result, 'METHOD_OVERRIDES');
for (const edge of overrides) {
const target = result.graph.getNode(edge.rel.targetId);
expect(target).toBeDefined();
@ -1651,3 +1651,53 @@ describe('Java method enrichment', () => {
expect(classifyCall).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// Java interface dispatch (METHOD_IMPLEMENTS)
// Action interface: execute(), priority()
// LogEvent implements Action, SendEmail implements Action
// ---------------------------------------------------------------------------
describe('Java interface dispatch (METHOD_IMPLEMENTS)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'java-interface-dispatch'), () => {});
}, 60000);
it('emits METHOD_IMPLEMENTS edges from LogEvent.execute → Action.execute', () => {
const mi = getRelationships(result, 'METHOD_IMPLEMENTS');
const edge = mi.find(
(e) =>
e.source === 'execute' &&
e.target === 'execute' &&
e.sourceFilePath.includes('LogEvent') &&
e.targetFilePath.includes('Action'),
);
expect(edge).toBeDefined();
});
it('emits METHOD_IMPLEMENTS edges from SendEmail.execute → Action.execute', () => {
const mi = getRelationships(result, 'METHOD_IMPLEMENTS');
const edge = mi.find(
(e) =>
e.source === 'execute' &&
e.target === 'execute' &&
e.sourceFilePath.includes('SendEmail') &&
e.targetFilePath.includes('Action'),
);
expect(edge).toBeDefined();
});
it('emits METHOD_IMPLEMENTS for priority() in both implementors', () => {
const mi = getRelationships(result, 'METHOD_IMPLEMENTS');
const priorityEdges = mi.filter(
(e) =>
e.source === 'priority' && e.target === 'priority' && e.targetFilePath.includes('Action'),
);
expect(priorityEdges.length).toBe(2);
const sourceFiles = priorityEdges.map((e) => e.sourceFilePath).sort();
expect(sourceFiles.some((f) => f.includes('LogEvent'))).toBe(true);
expect(sourceFiles.some((f) => f.includes('SendEmail'))).toBe(true);
});
});

View file

@ -90,7 +90,7 @@ describe('Kotlin heritage resolution', () => {
});
it('no OVERRIDES edges target Property nodes', () => {
const overrides = getRelationships(result, 'OVERRIDES');
const overrides = getRelationships(result, 'METHOD_OVERRIDES');
for (const edge of overrides) {
const target = result.graph.getNode(edge.rel.targetId);
expect(target).toBeDefined();

View file

@ -128,7 +128,7 @@ describe('PHP heritage & import resolution', () => {
// --- Property OVERRIDES exclusion ---
it('does not emit OVERRIDES for property name collisions ($status in both traits)', () => {
const overrides = getRelationships(result, 'OVERRIDES');
const overrides = getRelationships(result, 'METHOD_OVERRIDES');
// OVERRIDES should only target Method nodes, never Property nodes
for (const edge of overrides) {
const target = result.graph.getNode(edge.rel.targetId);
@ -140,7 +140,7 @@ describe('PHP heritage & import resolution', () => {
// --- MRO: OVERRIDES edge ---
it('emits OVERRIDES edge for User overriding log (inherited from BaseModel)', () => {
const overrides = getRelationships(result, 'OVERRIDES');
const overrides = getRelationships(result, 'METHOD_OVERRIDES');
expect(overrides.length).toBe(1);
const logOverride = overrides.find((e) => e.source === 'User' && e.target === 'log');
expect(logOverride).toBeDefined();
@ -1769,4 +1769,14 @@ describe('PHP abstract dispatch', () => {
expect(params).toContain('int');
}
});
it('emits METHOD_IMPLEMENTS edges from SqlRepository methods → Repository interface methods', () => {
const mi = getRelationships(result, 'METHOD_IMPLEMENTS');
const edges = mi.filter(
(e) => e.sourceFilePath.includes('SqlRepository') && e.targetFilePath.includes('Repository'),
);
expect(edges.length).toBe(2);
const names = edges.map((e) => e.source).sort();
expect(names).toEqual(['find', 'save']);
});
});

View file

@ -64,7 +64,7 @@ describe('Python relative import & heritage resolution', () => {
});
it('no OVERRIDES edges target Property nodes', () => {
const overrides = getRelationships(result, 'OVERRIDES');
const overrides = getRelationships(result, 'METHOD_OVERRIDES');
for (const edge of overrides) {
const target = result.graph.getNode(edge.rel.targetId);
expect(target).toBeDefined();
@ -2100,4 +2100,14 @@ describe('Python abstract dispatch', () => {
expect(params).toContain('int');
}
});
it('does not emit METHOD_IMPLEMENTS for abstract-class inheritance (only interface/trait parents)', () => {
// Python ABC is modelled as a Class with EXTENDS (not Interface with IMPLEMENTS),
// so the MRO processor does not emit METHOD_IMPLEMENTS edges here.
const mi = getRelationships(result, 'METHOD_IMPLEMENTS');
const edges = mi.filter(
(e) => e.sourceFilePath.includes('impl.py') && e.targetFilePath.includes('base.py'),
);
expect(edges.length).toBe(0);
});
});

View file

@ -194,7 +194,7 @@ describe('Ruby require_relative, heritage & property resolution', () => {
// --- No OVERRIDES edges target Property nodes ---
it('no OVERRIDES edges target Property nodes', () => {
const overrides = getRelationships(result, 'OVERRIDES');
const overrides = getRelationships(result, 'METHOD_OVERRIDES');
for (const edge of overrides) {
const target = result.graph.getNode(edge.rel.targetId);
expect(target).toBeDefined();

View file

@ -66,7 +66,7 @@ describe('Rust trait implementation resolution', () => {
});
it('no OVERRIDES edges target Property nodes', () => {
const overrides = getRelationships(result, 'OVERRIDES');
const overrides = getRelationships(result, 'METHOD_OVERRIDES');
for (const edge of overrides) {
const target = result.graph.getNode(edge.rel.targetId);
expect(target).toBeDefined();
@ -1847,4 +1847,13 @@ describe('Rust abstract dispatch (Repository trait)', () => {
expect(saveCall).toBeDefined();
expect(countCall).toBeDefined();
});
it('emits METHOD_IMPLEMENTS edges from SqlRepo impl methods → Repository trait methods', () => {
const mi = getRelationships(result, 'METHOD_IMPLEMENTS');
// find and save are required trait methods; count has a default impl so no METHOD_IMPLEMENTS
const libEdges = mi.filter((e) => e.sourceFilePath.includes('lib.rs'));
expect(libEdges.length).toBe(2);
const names = libEdges.map((e) => e.source).sort();
expect(names).toEqual(['find', 'save']);
});
});

View file

@ -799,4 +799,12 @@ describe.skipIf(!swiftAvailable)('Swift abstract dispatch', () => {
expect(sqlFind).toBeDefined();
expect(sqlFind!.properties.returnType).toBe('String');
});
it('emits METHOD_IMPLEMENTS edges from SqlRepository methods → Repository protocol methods', () => {
const mi = getRelationships(result, 'METHOD_IMPLEMENTS');
const edges = mi.filter((e) => e.sourceFilePath.includes('Repository.swift'));
expect(edges.length).toBe(2);
const names = edges.map((e) => e.source).sort();
expect(names).toEqual(['find', 'save']);
});
});

View file

@ -75,7 +75,7 @@ describe('TypeScript heritage resolution', () => {
});
it('no OVERRIDES edges target Property nodes', () => {
const overrides = getRelationships(result, 'OVERRIDES');
const overrides = getRelationships(result, 'METHOD_OVERRIDES');
for (const edge of overrides) {
const target = result.graph.getNode(edge.rel.targetId);
expect(target).toBeDefined();

View file

@ -35,7 +35,7 @@ describe('IMPACT_RELATION_CONFIDENCE', () => {
});
it('OVERRIDES has confidence 0.85 (statically verifiable override)', () => {
expect(IMPACT_RELATION_CONFIDENCE['OVERRIDES']).toBe(0.85);
expect(IMPACT_RELATION_CONFIDENCE['METHOD_OVERRIDES']).toBe(0.85);
});
it('HAS_METHOD has confidence 0.95 (structural containment)', () => {
@ -76,7 +76,7 @@ describe('confidenceForRelType', () => {
expect(confidenceForRelType('IMPORTS')).toBe(0.9);
expect(confidenceForRelType('EXTENDS')).toBe(0.85);
expect(confidenceForRelType('IMPLEMENTS')).toBe(0.85);
expect(confidenceForRelType('OVERRIDES')).toBe(0.85);
expect(confidenceForRelType('METHOD_OVERRIDES')).toBe(0.85);
expect(confidenceForRelType('HAS_METHOD')).toBe(0.95);
expect(confidenceForRelType('HAS_PROPERTY')).toBe(0.95);
expect(confidenceForRelType('ACCESSES')).toBe(0.8);

View file

@ -28,13 +28,18 @@ function addMethod(
className: string,
methodName: string,
classLabel: 'Class' | 'Interface' | 'Struct' | 'Trait' = 'Class',
parameterTypes?: string[],
) {
const classId = generateId(classLabel, className);
const methodId = generateId('Method', `${className}.${methodName}`);
graph.addNode({
id: methodId,
label: 'Method',
properties: { name: methodName, filePath: `src/${className}.ts` },
properties: {
name: methodName,
filePath: `src/${className}.ts`,
...(parameterTypes ? { parameterTypes } : {}),
},
});
graph.addRelationship({
id: generateId('HAS_METHOD', `${classId}->${methodId}`),
@ -128,7 +133,7 @@ describe('computeMRO', () => {
// OVERRIDES edge emitted
expect(result.overrideEdges).toBeGreaterThanOrEqual(1);
const overrides = graph.relationships.filter((r) => r.type === 'OVERRIDES');
const overrides = graph.relationships.filter((r) => r.type === 'METHOD_OVERRIDES');
expect(overrides.some((r) => r.sourceId === dId && r.targetId === bFoo)).toBe(true);
});
@ -296,7 +301,7 @@ describe('computeMRO', () => {
// No OVERRIDES edge emitted for Rust ambiguity
const overrides = graph.relationships.filter(
(r) => r.type === 'OVERRIDES' && r.sourceId === generateId('Struct', 'MyStruct'),
(r) => r.type === 'METHOD_OVERRIDES' && r.sourceId === generateId('Struct', 'MyStruct'),
);
expect(overrides).toHaveLength(0);
});
@ -347,7 +352,7 @@ describe('computeMRO', () => {
const result = computeMRO(graph);
// No OVERRIDES edge should be emitted for properties
const overrides = graph.relationships.filter((r) => r.type === 'OVERRIDES');
const overrides = graph.relationships.filter((r) => r.type === 'METHOD_OVERRIDES');
expect(overrides).toHaveLength(0);
expect(result.overrideEdges).toBe(0);
});
@ -399,7 +404,7 @@ describe('computeMRO', () => {
const result = computeMRO(graph);
// Only 1 OVERRIDES edge (for the method, not the property)
const overrides = graph.relationships.filter((r) => r.type === 'OVERRIDES');
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);
@ -511,4 +516,222 @@ describe('computeMRO', () => {
expect(result).toBeDefined();
});
});
// ---- 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);
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'] },
});
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'] },
});
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'] },
});
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'] },
});
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);
});
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);
});
});
});

View file

@ -114,7 +114,7 @@ describe('VALID_RELATION_TYPES', () => {
'IMPLEMENTS',
'HAS_METHOD',
'HAS_PROPERTY',
'OVERRIDES',
'METHOD_OVERRIDES',
'ACCESSES',
'HANDLES_ROUTE',
'FETCHES',