mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-07 02:58:02 +00:00
test(SM-11): address second PR #744 review round + per-language integration tests
Review fixes (https://github.com/abhigyanpatwari/GitNexus/pull/744#issuecomment-4211877593): P1 (Performance): Replace Map allocation in resolveMethodByOwner with a firstDef+ambiguous flag pattern. Zero allocation for the common single-candidate case on the hot path — the previous Map approach allocated on every member call regardless of whether deduplication was needed. P2 (Test gap): Strengthen the module-alias D0 skip test with a homonym fixture (two Users in different files). Previously the test passed whether or not D0 was actually bypassed; the new version proves D0 must be skipped by showing that resolveMemberCall directly returns null (ambiguous) but D1-D4 with alias narrowing picks the right one. Also fixes the underlying D2-vs-alias widening interaction: when filteredCandidates was narrowed by module-alias disambiguation, D2 no longer widens back to the full fuzzy pool (introduces aliasNarrowed boolean flag). L1 (Language coverage): Add C# and Kotlin implements-split tests at the resolveMemberCall layer. L2 (Maintainability): Export OverloadHints as @internal so the test can use a direct cast instead of fragile Parameters<...> type inference. Per-language integration tests: - rust-child-extends-parent: Direct impl method resolution via D0 (with honest documentation of the trait-method-as-Function gap that is Phase 5 / SM-16 scope) - java-interface-default-method: User implements Validator with default method resolved via implements-split MRO - csharp-interface-default-method: Same pattern for C# 8.0+ default interface methods - kotlin-interface-default-method: Same pattern for Kotlin interfaces with default implementations - python-multi-level-mro: 3-level C3 linearization (Grandparent ← Parent ← Child) - cpp-diamond-inheritance: Classic diamond (Base ← A, B ← Derived) via leftmost-base MRO Verification: - tsc --noEmit: clean - vitest run test/unit/: 3015 passed - vitest run test/integration/resolvers/: 1763 passed (+17 new per-language tests)
This commit is contained in:
parent
c159743cb1
commit
2e628c71c3
29 changed files with 501 additions and 51 deletions
|
|
@ -1191,9 +1191,15 @@ const toResolveResult = (definition: SymbolDefinition, tier: ResolutionTier): Re
|
|||
returnType: definition.returnType,
|
||||
});
|
||||
|
||||
/** Optional hints for overload disambiguation via argument literal types.
|
||||
* Only available on the sequential path (has AST); worker path passes undefined. */
|
||||
interface OverloadHints {
|
||||
/**
|
||||
* Optional hints for overload disambiguation via argument literal types.
|
||||
* Only available on the sequential path (has AST); worker path passes undefined.
|
||||
*
|
||||
* @internal Exported so tests can exercise the D0 skip-condition path without
|
||||
* constructing a real SyntaxNode. Do not use outside `call-processor.ts`
|
||||
* and its unit tests.
|
||||
*/
|
||||
export interface OverloadHints {
|
||||
callNode: SyntaxNode;
|
||||
inferLiteralType: LiteralTypeInferrer;
|
||||
typeEnv?: TypeEnvironment;
|
||||
|
|
@ -1353,6 +1359,10 @@ const resolveCallTarget = (
|
|||
// selects auth.py via moduleAliasMap. Runs for ALL member calls with a known module alias,
|
||||
// not just ambiguous ones — same-file tier may shadow the correct cross-module target when
|
||||
// the caller defines a function with the same name as the callee (Issue #417).
|
||||
//
|
||||
// Tracks `aliasNarrowed` so the D2 widening step below does NOT undo the alias filtering
|
||||
// by calling lookupFuzzy again (which would re-introduce homonym candidates from other files).
|
||||
let aliasNarrowed = false;
|
||||
if (call.callForm === 'member' && call.receiverName) {
|
||||
const aliasMap = ctx.moduleAliasMap?.get(currentFile);
|
||||
if (aliasMap) {
|
||||
|
|
@ -1361,6 +1371,7 @@ const resolveCallTarget = (
|
|||
const aliasFiltered = filteredCandidates.filter((c) => c.filePath === moduleFile);
|
||||
if (aliasFiltered.length > 0) {
|
||||
filteredCandidates = aliasFiltered;
|
||||
aliasNarrowed = true;
|
||||
} else {
|
||||
// Same-file tier returned a local match, but the alias points elsewhere.
|
||||
// Widen to global candidates and filter to the aliased module's file.
|
||||
|
|
@ -1375,7 +1386,10 @@ const resolveCallTarget = (
|
|||
const widened = filterCallableCandidates(fuzzyDefs, call.argCount, call.callForm).filter(
|
||||
(c) => c.filePath === moduleFile,
|
||||
);
|
||||
if (widened.length > 0) filteredCandidates = widened;
|
||||
if (widened.length > 0) {
|
||||
filteredCandidates = widened;
|
||||
aliasNarrowed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1424,8 +1438,13 @@ const resolveCallTarget = (
|
|||
// D2. Widen candidates: same-file tier may miss the parent's method when
|
||||
// it lives in another file. Query the symbol table directly for all
|
||||
// global methods with this name, then apply arity/kind filtering.
|
||||
//
|
||||
// When the candidate set was already narrowed by module-alias
|
||||
// disambiguation, do NOT widen back to the full fuzzy pool — that
|
||||
// would undo the alias narrowing and reintroduce homonym candidates
|
||||
// from other files.
|
||||
const methodPool =
|
||||
filteredCandidates.length <= 1
|
||||
filteredCandidates.length <= 1 && !aliasNarrowed
|
||||
? filterCallableCandidates(
|
||||
ctx.symbols.lookupFuzzy(call.calledName),
|
||||
call.argCount,
|
||||
|
|
@ -1694,22 +1713,33 @@ const resolveMethodByOwner = (
|
|||
const language = heritageMap ? getLanguageFromFilename(filePath) : null;
|
||||
const canWalkMRO = heritageMap != null && language != null;
|
||||
|
||||
// Iterate ALL class-like candidates. Unique hits (by nodeId) land in `matches`:
|
||||
// matches.size === 0 → owner-scoped resolution found nothing
|
||||
// matches.size === 1 → unambiguous answer
|
||||
// matches.size > 1 → genuine homonym ambiguity — refuse to pick one
|
||||
const matches = new Map<string, SymbolDefinition>();
|
||||
// Iterate all class-like candidates tracking the first unambiguous hit.
|
||||
// Zero-allocation fast path: the common case is exactly one class candidate,
|
||||
// so we avoid building a Map. A second hit with a different `nodeId` flips
|
||||
// `ambiguous` and short-circuits the loop. Diamond MRO convergence on the
|
||||
// same inherited method collapses to one hit because `nodeId` matches.
|
||||
//
|
||||
// firstDef === undefined → owner-scoped resolution found nothing
|
||||
// firstDef && !ambiguous → unambiguous answer
|
||||
// ambiguous → genuine homonym ambiguity — refuse to pick
|
||||
let firstDef: SymbolDefinition | undefined;
|
||||
let ambiguous = false;
|
||||
for (const candidate of typeResolved.candidates) {
|
||||
if (!CLASS_LIKE_TYPES.has(candidate.type)) continue;
|
||||
const def = canWalkMRO
|
||||
? lookupMethodByOwnerWithMRO(candidate.nodeId, methodName, heritageMap, ctx.symbols, language)
|
||||
: ctx.symbols.lookupMethodByOwner(candidate.nodeId, methodName);
|
||||
if (def) matches.set(def.nodeId, def);
|
||||
if (!def) continue;
|
||||
if (!firstDef) {
|
||||
firstDef = def;
|
||||
} else if (def.nodeId !== firstDef.nodeId) {
|
||||
ambiguous = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (matches.size !== 1) return undefined;
|
||||
const [def] = matches.values();
|
||||
return { def: def!, tier: typeResolved.tier };
|
||||
if (!firstDef || ambiguous) return undefined;
|
||||
return { def: firstDef, tier: typeResolved.tier };
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
5
gitnexus/test/fixtures/lang-resolution/cpp-diamond-inheritance/src/A.h
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/cpp-diamond-inheritance/src/A.h
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
#pragma once
|
||||
#include "Base.h"
|
||||
|
||||
class A : public Base {
|
||||
};
|
||||
5
gitnexus/test/fixtures/lang-resolution/cpp-diamond-inheritance/src/B.h
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/cpp-diamond-inheritance/src/B.h
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
#pragma once
|
||||
#include "Base.h"
|
||||
|
||||
class B : public Base {
|
||||
};
|
||||
6
gitnexus/test/fixtures/lang-resolution/cpp-diamond-inheritance/src/Base.h
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/cpp-diamond-inheritance/src/Base.h
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
class Base {
|
||||
public:
|
||||
int method() { return 42; }
|
||||
};
|
||||
6
gitnexus/test/fixtures/lang-resolution/cpp-diamond-inheritance/src/Derived.h
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/cpp-diamond-inheritance/src/Derived.h
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#pragma once
|
||||
#include "A.h"
|
||||
#include "B.h"
|
||||
|
||||
class Derived : public A, public B {
|
||||
};
|
||||
6
gitnexus/test/fixtures/lang-resolution/cpp-diamond-inheritance/src/app.cpp
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/cpp-diamond-inheritance/src/app.cpp
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#include "Derived.h"
|
||||
|
||||
void run() {
|
||||
Derived d;
|
||||
d.method();
|
||||
}
|
||||
10
gitnexus/test/fixtures/lang-resolution/csharp-interface-default-method/App.cs
vendored
Normal file
10
gitnexus/test/fixtures/lang-resolution/csharp-interface-default-method/App.cs
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
namespace InterfaceDefault;
|
||||
|
||||
public class App
|
||||
{
|
||||
public static void Run()
|
||||
{
|
||||
User user = new User("alice");
|
||||
user.Validate();
|
||||
}
|
||||
}
|
||||
11
gitnexus/test/fixtures/lang-resolution/csharp-interface-default-method/User.cs
vendored
Normal file
11
gitnexus/test/fixtures/lang-resolution/csharp-interface-default-method/User.cs
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
namespace InterfaceDefault;
|
||||
|
||||
public class User : IValidator
|
||||
{
|
||||
public string Name { get; }
|
||||
|
||||
public User(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
}
|
||||
6
gitnexus/test/fixtures/lang-resolution/csharp-interface-default-method/Validator.cs
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/csharp-interface-default-method/Validator.cs
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
namespace InterfaceDefault;
|
||||
|
||||
public interface IValidator
|
||||
{
|
||||
bool Validate() => true;
|
||||
}
|
||||
6
gitnexus/test/fixtures/lang-resolution/java-interface-default-method/App.java
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/java-interface-default-method/App.java
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
public class App {
|
||||
public static void run() {
|
||||
User user = new User("alice");
|
||||
user.validate();
|
||||
}
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/java-interface-default-method/User.java
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/java-interface-default-method/User.java
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
public class User implements Validator {
|
||||
private String name;
|
||||
|
||||
public User(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/java-interface-default-method/Validator.java
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/java-interface-default-method/Validator.java
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
public interface Validator {
|
||||
default boolean validate() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
6
gitnexus/test/fixtures/lang-resolution/kotlin-interface-default-method/src/App.kt
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/kotlin-interface-default-method/src/App.kt
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
package example
|
||||
|
||||
fun run() {
|
||||
val user = User("alice")
|
||||
user.validate()
|
||||
}
|
||||
3
gitnexus/test/fixtures/lang-resolution/kotlin-interface-default-method/src/User.kt
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/kotlin-interface-default-method/src/User.kt
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
package example
|
||||
|
||||
class User(val name: String) : Validator
|
||||
5
gitnexus/test/fixtures/lang-resolution/kotlin-interface-default-method/src/Validator.kt
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/kotlin-interface-default-method/src/Validator.kt
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package example
|
||||
|
||||
interface Validator {
|
||||
fun validate(): Boolean = true
|
||||
}
|
||||
6
gitnexus/test/fixtures/lang-resolution/python-multi-level-mro/app.py
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/python-multi-level-mro/app.py
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from child import Child
|
||||
|
||||
|
||||
def run() -> None:
|
||||
c = Child()
|
||||
c.gp_method()
|
||||
5
gitnexus/test/fixtures/lang-resolution/python-multi-level-mro/child.py
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/python-multi-level-mro/child.py
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from parent import Parent
|
||||
|
||||
|
||||
class Child(Parent):
|
||||
pass
|
||||
3
gitnexus/test/fixtures/lang-resolution/python-multi-level-mro/grandparent.py
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/python-multi-level-mro/grandparent.py
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
class Grandparent:
|
||||
def gp_method(self) -> str:
|
||||
return "grandparent"
|
||||
5
gitnexus/test/fixtures/lang-resolution/python-multi-level-mro/parent.py
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/python-multi-level-mro/parent.py
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from grandparent import Grandparent
|
||||
|
||||
|
||||
class Parent(Grandparent):
|
||||
pass
|
||||
16
gitnexus/test/fixtures/lang-resolution/rust-child-extends-parent/src/child.rs
vendored
Normal file
16
gitnexus/test/fixtures/lang-resolution/rust-child-extends-parent/src/child.rs
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
use crate::parent::Parent;
|
||||
|
||||
pub struct Child;
|
||||
|
||||
impl Child {
|
||||
// Direct impl method — MUST resolve via resolveMemberCall owner-scoped path.
|
||||
pub fn own_method(&self) -> &str {
|
||||
"child-own"
|
||||
}
|
||||
}
|
||||
|
||||
// Trait implementation — `trait_only` is provided by the trait's default impl
|
||||
// but is NOT reachable via direct `obj.trait_only()` in Rust without the trait
|
||||
// being in scope. The resolver correctly treats qualified-syntax MRO as opaque
|
||||
// to direct member calls.
|
||||
impl Parent for Child {}
|
||||
17
gitnexus/test/fixtures/lang-resolution/rust-child-extends-parent/src/main.rs
vendored
Normal file
17
gitnexus/test/fixtures/lang-resolution/rust-child-extends-parent/src/main.rs
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
mod child;
|
||||
mod parent;
|
||||
|
||||
use crate::child::Child;
|
||||
|
||||
fn run() {
|
||||
let c = Child;
|
||||
// Direct impl method — SHOULD resolve to Child::own_method.
|
||||
c.own_method();
|
||||
// Trait-inherited default — direct member-call SHOULD NOT resolve to
|
||||
// Parent::trait_only under Rust's qualified-syntax MRO strategy.
|
||||
c.trait_only();
|
||||
}
|
||||
|
||||
fn main() {
|
||||
run();
|
||||
}
|
||||
11
gitnexus/test/fixtures/lang-resolution/rust-child-extends-parent/src/parent.rs
vendored
Normal file
11
gitnexus/test/fixtures/lang-resolution/rust-child-extends-parent/src/parent.rs
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
// Trait "parent" — methods on a Rust trait are NOT reachable via direct
|
||||
// `obj.method()` syntax on structs that implement the trait unless the trait
|
||||
// itself is in scope. Our qualified-syntax MRO strategy reflects this: direct
|
||||
// member calls do NOT walk trait ancestry, so `c.trait_only()` below should
|
||||
// produce NO CALLS edge to `Parent::trait_only`.
|
||||
|
||||
pub trait Parent {
|
||||
fn trait_only(&self) -> &str {
|
||||
"parent-default"
|
||||
}
|
||||
}
|
||||
|
|
@ -1548,3 +1548,37 @@ describe('C++ Child extends Parent — inherited method resolution (SM-9)', () =
|
|||
expect(parentMethodCall!.source).toBe('run');
|
||||
});
|
||||
});
|
||||
|
||||
describe('C++ Derived : A, B — diamond inheritance via leftmost-base MRO (SM-11)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-diamond-inheritance'), () => {});
|
||||
}, 60000);
|
||||
|
||||
it('detects Base, A, B, and Derived classes', () => {
|
||||
const classes = getNodesByLabel(result, 'Class');
|
||||
expect(classes).toContain('Base');
|
||||
expect(classes).toContain('A');
|
||||
expect(classes).toContain('B');
|
||||
expect(classes).toContain('Derived');
|
||||
});
|
||||
|
||||
it('emits EXTENDS edges for both branches: A → Base, B → Base, Derived → A, Derived → B', () => {
|
||||
const extends_ = getRelationships(result, 'EXTENDS');
|
||||
const edges = edgeSet(extends_);
|
||||
expect(edges).toContain('A → Base');
|
||||
expect(edges).toContain('B → Base');
|
||||
expect(edges).toContain('Derived → A');
|
||||
expect(edges).toContain('Derived → B');
|
||||
});
|
||||
|
||||
it('resolves d.method() to Base::method via leftmost-base MRO walk', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const methodCall = calls.find(
|
||||
(c) => c.target === 'method' && c.targetFilePath.includes('Base.h'),
|
||||
);
|
||||
expect(methodCall).toBeDefined();
|
||||
expect(methodCall!.source).toBe('run');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1963,3 +1963,37 @@ describe('C# Child extends Parent — inherited method resolution (SM-9)', () =>
|
|||
expect(parentMethodCall!.source).toBe('Run');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SM-11: C# User : IValidator — interface default method via implements-split
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('C# User implements IValidator — interface default method (SM-11)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'csharp-interface-default-method'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects IValidator interface and User class', () => {
|
||||
expect(getNodesByLabel(result, 'Interface')).toContain('IValidator');
|
||||
expect(getNodesByLabel(result, 'Class')).toContain('User');
|
||||
});
|
||||
|
||||
it('emits IMPLEMENTS edge: User → IValidator', () => {
|
||||
const impls = getRelationships(result, 'IMPLEMENTS');
|
||||
expect(edgeSet(impls)).toContain('User → IValidator');
|
||||
});
|
||||
|
||||
it('resolves user.Validate() to IValidator.Validate via implements-split MRO', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const validateCall = calls.find(
|
||||
(c) => c.target === 'Validate' && c.targetFilePath.includes('Validator.cs'),
|
||||
);
|
||||
expect(validateCall).toBeDefined();
|
||||
expect(validateCall!.source).toBe('Run');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2133,3 +2133,37 @@ describe('Java Child extends Parent — inherited method resolution (SM-9)', ()
|
|||
expect(parentMethodCall!.source).toBe('run');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SM-11: Java User implements Validator — interface default method (Java 8+)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Java User implements Validator — interface default method (SM-11)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'java-interface-default-method'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects Validator interface and User class', () => {
|
||||
expect(getNodesByLabel(result, 'Interface')).toContain('Validator');
|
||||
expect(getNodesByLabel(result, 'Class')).toContain('User');
|
||||
});
|
||||
|
||||
it('emits IMPLEMENTS edge: User → Validator', () => {
|
||||
const impls = getRelationships(result, 'IMPLEMENTS');
|
||||
expect(edgeSet(impls)).toContain('User → Validator');
|
||||
});
|
||||
|
||||
it('resolves user.validate() to Validator.validate via implements-split MRO', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const validateCall = calls.find(
|
||||
(c) => c.target === 'validate' && c.targetFilePath.includes('Validator.java'),
|
||||
);
|
||||
expect(validateCall).toBeDefined();
|
||||
expect(validateCall!.source).toBe('run');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2026,3 +2026,37 @@ describe('Kotlin Child extends Parent — inherited method resolution (SM-9)', (
|
|||
expect(parentMethodCall!.source).toBe('run');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SM-11: Kotlin User : Validator — interface default method via implements-split
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Kotlin User implements Validator — interface default method (SM-11)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'kotlin-interface-default-method'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects Validator interface and User class', () => {
|
||||
expect(getNodesByLabel(result, 'Interface')).toContain('Validator');
|
||||
expect(getNodesByLabel(result, 'Class')).toContain('User');
|
||||
});
|
||||
|
||||
it('emits IMPLEMENTS edge: User → Validator', () => {
|
||||
const impls = getRelationships(result, 'IMPLEMENTS');
|
||||
expect(edgeSet(impls)).toContain('User → Validator');
|
||||
});
|
||||
|
||||
it('resolves user.validate() to Validator.validate via implements-split MRO', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const validateCall = calls.find(
|
||||
(c) => c.target === 'validate' && c.targetFilePath.includes('Validator.kt'),
|
||||
);
|
||||
expect(validateCall).toBeDefined();
|
||||
expect(validateCall!.source).toBe('run');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2146,3 +2146,33 @@ describe('Python Child extends Parent — inherited method resolution (SM-9)', (
|
|||
expect(parentMethodCall!.source).toBe('run');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Python Grandchild→Child→Parent — 3-level C3 MRO walk (SM-11)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(FIXTURES, 'python-multi-level-mro'), () => {});
|
||||
}, 60000);
|
||||
|
||||
it('detects Grandparent, Parent, and Child classes', () => {
|
||||
const classes = getNodesByLabel(result, 'Class');
|
||||
expect(classes).toContain('Grandparent');
|
||||
expect(classes).toContain('Parent');
|
||||
expect(classes).toContain('Child');
|
||||
});
|
||||
|
||||
it('emits EXTENDS chain: Child → Parent, Parent → Grandparent', () => {
|
||||
const extends_ = getRelationships(result, 'EXTENDS');
|
||||
expect(edgeSet(extends_)).toContain('Child → Parent');
|
||||
expect(edgeSet(extends_)).toContain('Parent → Grandparent');
|
||||
});
|
||||
|
||||
it('resolves c.gp_method() to Grandparent.gp_method via 3-level C3 walk', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const gpCall = calls.find(
|
||||
(c) => c.target === 'gp_method' && c.targetFilePath.includes('grandparent.py'),
|
||||
);
|
||||
expect(gpCall).toBeDefined();
|
||||
expect(gpCall!.source).toBe('run');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1857,3 +1857,46 @@ describe('Rust abstract dispatch (Repository trait)', () => {
|
|||
expect(names).toEqual(['find', 'save']);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SM-11: Rust Child struct — direct impl method resolution via D0
|
||||
//
|
||||
// Companion integration test for the unit-level Rust qualified-syntax tests
|
||||
// in symbol-table.test.ts. Validates end-to-end that Rust direct-impl methods
|
||||
// resolve through the owner-scoped D0 path (`resolveMemberCall`).
|
||||
//
|
||||
// NOTE on trait-inherited methods: Rust's qualified-syntax MRO strategy in
|
||||
// `lookupMethodByOwnerWithMRO` correctly returns null for trait-inherited
|
||||
// methods at the unit level. However, in the current pipeline, Rust trait
|
||||
// default methods are captured as `Function` nodes (not `Method` with
|
||||
// ownerId), so the owner-scoped index does not contain them. This means
|
||||
// direct `obj.trait_method()` calls currently fall through to D1-D4 fuzzy
|
||||
// widening rather than being correctly null-routed. Rust trait capture as
|
||||
// Method-with-ownerId is a Phase 5 (SM-16) fix — out of SM-11 scope.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Rust Child direct-impl method resolution (SM-11)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-child-extends-parent'), () => {});
|
||||
}, 60000);
|
||||
|
||||
it('detects Child struct and Parent trait', () => {
|
||||
const structs = getNodesByLabel(result, 'Struct');
|
||||
expect(structs).toContain('Child');
|
||||
const traits = getNodesByLabel(result, 'Trait');
|
||||
expect(traits).toContain('Parent');
|
||||
});
|
||||
|
||||
it('resolves c.own_method() to Child::own_method via D0 owner-scoped path', () => {
|
||||
// Direct impl method — D0 short-circuits to lookupMethodByOwner which
|
||||
// returns Child::own_method without falling through to D1-D4 fuzzy.
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const ownCall = calls.find(
|
||||
(c) =>
|
||||
c.target === 'own_method' && c.source === 'run' && c.targetFilePath.includes('child.rs'),
|
||||
);
|
||||
expect(ownCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1406,6 +1406,7 @@ describe('lookupMethodByOwnerWithMRO', () => {
|
|||
import {
|
||||
_resolveCallTargetForTesting,
|
||||
resolveMemberCall,
|
||||
type OverloadHints,
|
||||
} from '../../src/core/ingestion/call-processor.js';
|
||||
|
||||
describe('resolveMemberCall', () => {
|
||||
|
|
@ -1702,6 +1703,72 @@ describe('resolveMemberCall', () => {
|
|||
expect(result!.nodeId).toBe('method:Base:method');
|
||||
expect(result!.returnType).toBe('int');
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// L1: C# / Kotlin implements-split strategy through resolveMemberCall.
|
||||
// lookupMethodByOwnerWithMRO already has strategy-level coverage for these
|
||||
// languages; these tests add the resolveMemberCall layer (tier resolution
|
||||
// + class candidate iteration + MRO walk) on top.
|
||||
// -------------------------------------------------------------------------
|
||||
it('C#: walks implements-split to find inherited method via interface', () => {
|
||||
// C# uses implements-split MRO: class base chain walked first, then
|
||||
// interfaces. Here IService declares Save which is implemented by the
|
||||
// base class BaseService — MyService inherits Save through the class.
|
||||
ctx.symbols.add('src/iservice.cs', 'IService', 'interface:IService', 'Interface');
|
||||
ctx.symbols.add('src/base.cs', 'BaseService', 'class:BaseService', 'Class');
|
||||
ctx.symbols.add('src/my.cs', 'MyService', 'class:MyService', 'Class');
|
||||
ctx.symbols.add('src/base.cs', 'Save', 'method:BaseService:Save', 'Method', {
|
||||
returnType: 'void',
|
||||
ownerId: 'class:BaseService',
|
||||
});
|
||||
ctx.importMap.set('src/app.cs', new Set(['src/iservice.cs', 'src/base.cs', 'src/my.cs']));
|
||||
|
||||
const heritage: ExtractedHeritage[] = [
|
||||
{
|
||||
filePath: 'src/base.cs',
|
||||
className: 'BaseService',
|
||||
parentName: 'IService',
|
||||
kind: 'implements',
|
||||
},
|
||||
{ filePath: 'src/my.cs', className: 'MyService', parentName: 'BaseService', kind: 'extends' },
|
||||
];
|
||||
const map = buildHeritageMap(heritage, ctx);
|
||||
|
||||
const result = resolveMemberCall('MyService', 'Save', 'src/app.cs', ctx, map);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.nodeId).toBe('method:BaseService:Save');
|
||||
expect(result!.returnType).toBe('void');
|
||||
});
|
||||
|
||||
it('Kotlin: walks implements-split to find inherited method via interface', () => {
|
||||
// Kotlin shares the implements-split MRO strategy with Java/C#. A class
|
||||
// inheriting from an interface that provides a default method should
|
||||
// resolve `obj.method()` to the interface's implementation.
|
||||
ctx.symbols.add('src/validator.kt', 'Validator', 'interface:Validator', 'Interface');
|
||||
ctx.symbols.add('src/user.kt', 'User', 'class:User', 'Class');
|
||||
ctx.symbols.add('src/validator.kt', 'validate', 'method:Validator:validate', 'Method', {
|
||||
returnType: 'Boolean',
|
||||
ownerId: 'interface:Validator',
|
||||
});
|
||||
ctx.importMap.set('src/app.kt', new Set(['src/validator.kt', 'src/user.kt']));
|
||||
|
||||
const heritage: ExtractedHeritage[] = [
|
||||
{
|
||||
filePath: 'src/user.kt',
|
||||
className: 'User',
|
||||
parentName: 'Validator',
|
||||
kind: 'implements',
|
||||
},
|
||||
];
|
||||
const map = buildHeritageMap(heritage, ctx);
|
||||
|
||||
const result = resolveMemberCall('User', 'validate', 'src/app.kt', ctx, map);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.nodeId).toBe('method:Validator:validate');
|
||||
expect(result!.returnType).toBe('Boolean');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -1717,23 +1784,41 @@ describe('resolveCallTarget D0 skip conditions (SM-11)', () => {
|
|||
ctx = createResolutionContext();
|
||||
});
|
||||
|
||||
it('module alias: resolution succeeds when hasActiveModuleAlias triggers D0 skip', () => {
|
||||
// Python-style: `import auth; auth.User.save()`. The `receiverName='auth'`
|
||||
// matches a moduleAliasMap entry, which sets `hasActiveModuleAlias=true`
|
||||
// and bypasses the D0 fast path. The test verifies that D1-D4 still
|
||||
// produces the correct result in this skip scenario — a regression here
|
||||
// (e.g. D0 being called when it shouldn't) would silently pick a homonym
|
||||
// from another file, and an unintentional skip would cause resolution to
|
||||
// fail entirely.
|
||||
it('module alias: picks alias-scoped class over homonym (D0 actually bypassed)', () => {
|
||||
// Python-style: `import auth; auth.User.save()` where BOTH auth.py and
|
||||
// other.py define a `User` class with a `save` method. The test proves:
|
||||
//
|
||||
// 1. Without the alias: resolveMemberCall sees two homonym Users,
|
||||
// both own `save`, and correctly returns null (refuses to guess).
|
||||
// 2. With the alias: D0 is skipped via `hasActiveModuleAlias`, and
|
||||
// D1-D4 — respecting the alias-narrowed filteredCandidates — picks
|
||||
// the auth.py User.save method.
|
||||
//
|
||||
// A regression where D0 silently ran would produce null (ambiguous)
|
||||
// instead of the correct answer, so this test actually exercises the
|
||||
// skip path rather than just verifying a single-candidate happy path.
|
||||
ctx.symbols.add('src/auth.py', 'User', 'class:auth:User', 'Class');
|
||||
ctx.symbols.add('src/auth.py', 'save', 'method:auth:User:save', 'Method', {
|
||||
returnType: 'None',
|
||||
ownerId: 'class:auth:User',
|
||||
});
|
||||
ctx.importMap.set('src/app.py', new Set(['src/auth.py']));
|
||||
ctx.symbols.add('src/other.py', 'User', 'class:other:User', 'Class');
|
||||
ctx.symbols.add('src/other.py', 'save', 'method:other:User:save', 'Method', {
|
||||
returnType: 'None',
|
||||
ownerId: 'class:other:User',
|
||||
});
|
||||
ctx.importMap.set('src/app.py', new Set(['src/auth.py', 'src/other.py']));
|
||||
ctx.moduleAliasMap.set('src/app.py', new Map([['auth', 'src/auth.py']]));
|
||||
|
||||
const result = _resolveCallTargetForTesting(
|
||||
// Control: without alias narrowing, resolveMemberCall sees both Users
|
||||
// own `save` and correctly refuses to pick one.
|
||||
const ambiguous = resolveMemberCall('User', 'save', 'src/app.py', ctx);
|
||||
expect(ambiguous).toBeNull();
|
||||
|
||||
// With alias narrowing active, D0 is skipped and D1-D4 picks auth.py's
|
||||
// User.save because the alias block already narrowed filteredCandidates
|
||||
// to auth.py (and the D2 widening step is gated on `!aliasNarrowed`).
|
||||
const aliased = _resolveCallTargetForTesting(
|
||||
{
|
||||
calledName: 'save',
|
||||
callForm: 'member',
|
||||
|
|
@ -1744,26 +1829,8 @@ describe('resolveCallTarget D0 skip conditions (SM-11)', () => {
|
|||
ctx,
|
||||
);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.nodeId).toBe('method:auth:User:save');
|
||||
});
|
||||
|
||||
it('module alias: resolveMemberCall called directly still works (control)', () => {
|
||||
// Control case: calling resolveMemberCall directly (the path D0 would have
|
||||
// taken) produces the same result. Demonstrates that the skip is a safety
|
||||
// measure for the D2-widening interaction with alias narrowing, not because
|
||||
// resolveMemberCall itself is broken here.
|
||||
ctx.symbols.add('src/auth.py', 'User', 'class:auth:User', 'Class');
|
||||
ctx.symbols.add('src/auth.py', 'save', 'method:auth:User:save', 'Method', {
|
||||
returnType: 'None',
|
||||
ownerId: 'class:auth:User',
|
||||
});
|
||||
ctx.importMap.set('src/app.py', new Set(['src/auth.py']));
|
||||
|
||||
const result = resolveMemberCall('User', 'save', 'src/app.py', ctx);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.nodeId).toBe('method:auth:User:save');
|
||||
expect(aliased).not.toBeNull();
|
||||
expect(aliased!.nodeId).toBe('method:auth:User:save');
|
||||
});
|
||||
|
||||
it('overloadHints present: D0 bypassed, D1-D4 handles resolution', () => {
|
||||
|
|
@ -1783,13 +1850,7 @@ describe('resolveCallTarget D0 skip conditions (SM-11)', () => {
|
|||
|
||||
// Minimal stub; D1-D4 only calls tryOverloadDisambiguation when there are
|
||||
// multiple candidates, so an empty object is fine for single-candidate cases.
|
||||
const dummyHints = {} as unknown as Parameters<
|
||||
typeof _resolveCallTargetForTesting
|
||||
>[3] extends infer O
|
||||
? O extends { overloadHints?: infer H }
|
||||
? H
|
||||
: never
|
||||
: never;
|
||||
const dummyHints = {} as OverloadHints;
|
||||
|
||||
const result = _resolveCallTargetForTesting(
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue