mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-05 08:06:02 +00:00
* feat: Phase 8 field/property type resolution — resolve chained member access
Add field/property type extraction to the type resolution system so that
chained member access like `user.address.save()` resolves the intermediate
receiver type (`address → Address`) through Property symbols in SymbolTable.
Key changes:
- SymbolTable: add `declaredType` field, `fieldByOwner` O(1) index,
`lookupFieldByOwner()` method, P0 conditional callableIndex invalidation,
P2 exclude Properties from globalIndex to prevent namespace pollution
- tree-sitter queries: add `definition.property` for TypeScript, Java, Go
- parse-worker: extract declared types for Property nodes via
`extractPropertyDeclaredType()`, capture field-access receiver info
- call-processor: add `resolveFieldAccessType()` helper and field-access
branch in both sequential and worker receiver resolution paths
- Integration tests: new field-types test suite verifying end-to-end
`user.address.save() → Address#save` resolution
* fix: Go tree-sitter query captures field_declaration not field_declaration_list
Post-review fix: the Go struct field query incorrectly put @definition.property
on field_declaration_list (the list container) instead of field_declaration
(the individual field). Also removed unused `language` parameter from
extractPropertyDeclaredType.
* feat: expand field-type tests to 6 languages, fix Go ownerId and Kotlin navigation_expression
- Add integration test fixtures for Java, C#, Go, Kotlin, PHP (alongside existing TS)
- Fix Go: add type_declaration handling in findEnclosingClassId for struct fields
(field_declaration → field_declaration_list → struct_type → type_spec → type_declaration)
- Fix Kotlin: add navigation_expression handling in field-access resolution
(Kotlin uses navigation_expression + navigation_suffix, not member_expression)
- Add extractMemberAccessParts helper in call-processor for cross-language member access
- All 24 field-type tests pass across 6 languages, 181 Go+Kotlin tests pass with no regressions
* refactor: split HAS_METHOD into HAS_METHOD + HAS_PROPERTY edge types
Property nodes now use HAS_PROPERTY edges instead of HAS_METHOD, giving
the graph schema proper semantic separation between methods and fields.
- HAS_METHOD: Method, Constructor, Function (when inside a class)
- HAS_PROPERTY: Property nodes (class fields, struct fields, attributes)
MRO processor only reads HAS_METHOD — properties correctly excluded from
method resolution order. Impact analysis accepts both edge types.
Updated 12 files: graph types, schema, tools docs, parse-worker,
parsing-processor, call-processor, and 6 test files.
* fix(test): update security test to expect 7 VALID_RELATION_TYPES (added HAS_PROPERTY)
* test: add unit tests for Phase 8 SymbolTable features (39 tests, up from 19)
Cover all new branches: declaredType metadata, Property exclusion from
globalIndex, conditional callableIndex invalidation, lookupFieldByOwner
(happy path + edge cases), lookupFuzzyCallable filtering, and clear()
with fieldByOwner. Fixes branch coverage threshold (21.8% → 23%+).
* feat: Phase 8B mixed field+method chain resolution, C++/Rust chain fixes
Unify field and method chain resolution into a single `extractMixedChain`
walker that handles interleaved patterns like `svc.getUser().address.save()`.
Fix C++ chain calls (tree-sitter-cpp `field_expression` uses `argument` not
`object`), Rust unit struct instantiation (`let svc = TypeName;`), and add
stdlib passthrough for `unwrap()`/`clone()`/`expect()` in chain loops.
Key changes:
- Replace `receiverCallChain` + `receiverFieldAccess` with unified
`receiverMixedChain: MixedChainStep[]` on ExtractedCall
- Add `extractMixedChain` in utils.ts (handles both call_expression and
field_expression nodes, including C++ `argument` field)
- Add `TYPE_PRESERVING_METHODS` set for stdlib identity operations
- Add C++ inline method double-indexing guard in parsing-processor.ts
and parse-worker.ts
- Add Rust unit struct recognition in type-extractors/rust.ts
- Split field-types.test.ts into per-language test files
- Add ts-mixed-chain fixture and integration tests
- Resolve rust.test.ts todo: Option<T>.unwrap().save() now works
- Update roadmap: Phases 7+8 complete, Phase 9 is next
* fix: Python declaredType extraction and sequential-path property registration
- Move @definition.property capture from expression_statement to assignment
node in Python queries so Strategy 1 childForFieldName('type') succeeds
- Pass item.declaredType through ctx.symbols.add in sequential call-processor
path, matching worker path behavior (fixes Ruby YARD declaredType drop)
- Add Python chain resolution integration test (user.address.save → Address#save)
- Update Rust/Python status in roadmap and system docs to reflect actual coverage
* fix: Python/Ruby field type disambiguation and Rust chain test
Three fixes from PR #354 third review:
1. Python typed_parameter name extraction: tree-sitter-python's
typed_parameter uses positional children for the name, not a named
field. TypeEnv and extractParameter now fall back to firstNamedChild.
2. Ruby/Python call-step field resolution: Ruby's AST uses `call` nodes
for both property access and method calls. The chain walker now tries
resolveFieldAccessType before resolveCallTarget for call steps, so
attr_accessor properties resolve via declaredType.
3. Rust chain resolution test: added missing integration test asserting
user.address.save() resolves to Address#save.
Also splits C/C++ and TS/JS columns in type-resolution-system.md
language matrix with footnotes for accuracy.
1062 resolver integration tests passing, 0 failures.
* refactor: Phase 8 code review cleanup — extract walkMixedChain, fix MCP agent gaps
- Extract duplicated chain resolution loop into shared walkMixedChain() helper,
eliminating ~60 lines of copy-pasted code between sequential and worker paths
- Add returnType to ResolveResult, removing redundant lookupFuzzy+find per chain step
- Fix context() tool to include HAS_METHOD, HAS_PROPERTY, OVERRIDES in queries
so agents can discover class members
- Fix p.declaredType Cypher example (column doesn't exist) → p.description
- Add HAS_METHOD, HAS_PROPERTY, OVERRIDES to schema resource
- Document HAS_METHOD/HAS_PROPERTY in impact tool description
- Delete dead code extractMemberAccessParts (superseded by extractMixedChain)
- Replace any with SyntaxNode on extractPropertyDeclaredType
- Add Rust deep-field-chain test (5 tests), Java mixed-chain (4), Go mixed-chain (4)
- All 1075 tests pass (13 new, 0 regressions)
* refactor: type SymbolDefinition.type as NodeLabel, add O(1) receiver index
- Change SymbolDefinition.type from string to NodeLabel union (35 members)
across symbol-table.ts, parse-worker.ts, parsing-processor.ts — compiler
now enforces correctness at all comparison/assignment sites
- Replace O(N*M) linear scan in lookupReceiverType with pre-built
ReceiverTypeIndex (Map<funcName, Map<varName, Entry>>) for O(1) lookups
with proper ambiguity handling and file-level fallback
- All 1075 tests pass, 0 regressions
* fix: capture C++ pointer/ref fields, Kotlin data class props, PHP constructor promotion
Add tree-sitter query patterns for three previously missed property declaration
forms: C++ pointer/reference member fields (Address* addr; Address& ref;),
Kotlin primary constructor val/var parameters (data class User(val name: String)),
and PHP 8.0+ constructor property promotion (public Address $address).
Fix "10 languages" off-by-one in docs (Ruby is single-level only, not deep chain).
Update Python feature matrix cell from No* to Yes* after 31b95f0 fix.
11 new integration tests with per-language fixtures verify property capture,
HAS_PROPERTY edge emission, and field-access chain resolution.
461 lines
18 KiB
TypeScript
461 lines
18 KiB
TypeScript
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') {
|
|
const classId = generateId(classLabel, className);
|
|
const methodId = generateId('Method', `${className}.${methodName}`);
|
|
graph.addNode({
|
|
id: methodId,
|
|
label: 'Method',
|
|
properties: { name: methodName, filePath: `src/${className}.ts` },
|
|
});
|
|
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 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('C++ leftmost');
|
|
expect(fooAmbiguity!.reason).toContain('B');
|
|
|
|
// OVERRIDES edge emitted
|
|
expect(result.overrideEdges).toBeGreaterThanOrEqual(1);
|
|
const overrides = graph.relationships.filter(r => r.type === '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('Python C3');
|
|
});
|
|
});
|
|
|
|
// ---- 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 === '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 === '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 === '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();
|
|
});
|
|
});
|
|
|
|
// ---- 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();
|
|
});
|
|
});
|
|
});
|