mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-05 02:43:32 +00:00
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:
parent
63fc4c795f
commit
1966242675
24 changed files with 477 additions and 43 deletions
|
|
@ -97,7 +97,8 @@ export type RelationshipType =
|
|||
| 'CONTAINS'
|
||||
| 'CALLS'
|
||||
| 'INHERITS'
|
||||
| 'OVERRIDES'
|
||||
| 'METHOD_OVERRIDES'
|
||||
| 'METHOD_IMPLEMENTS'
|
||||
| 'IMPORTS'
|
||||
| 'USES'
|
||||
| 'DEFINES'
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
`,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)' },
|
||||
|
|
|
|||
2
gitnexus/test/fixtures/local-backend-seed.ts
vendored
2
gitnexus/test/fixtures/local-backend-seed.ts
vendored
|
|
@ -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)`,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ withTestLbugDB(
|
|||
'EXTENDS',
|
||||
'IMPLEMENTS',
|
||||
'HAS_METHOD',
|
||||
'OVERRIDES',
|
||||
'METHOD_OVERRIDES',
|
||||
'ACCESSES',
|
||||
];
|
||||
const invalidTypes = ['CONTAINS', 'STEP_IN_PROCESS', 'MEMBER_OF', 'DROP_TABLE'];
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ describe('VALID_RELATION_TYPES', () => {
|
|||
'IMPLEMENTS',
|
||||
'HAS_METHOD',
|
||||
'HAS_PROPERTY',
|
||||
'OVERRIDES',
|
||||
'METHOD_OVERRIDES',
|
||||
'ACCESSES',
|
||||
'HANDLES_ROUTE',
|
||||
'FETCHES',
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue