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:
Gergo Magyar 2026-04-09 07:50:11 +01:00
parent c159743cb1
commit 2e628c71c3
29 changed files with 501 additions and 51 deletions

View file

@ -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 };
};
// ---------------------------------------------------------------------------

View file

@ -0,0 +1,5 @@
#pragma once
#include "Base.h"
class A : public Base {
};

View file

@ -0,0 +1,5 @@
#pragma once
#include "Base.h"
class B : public Base {
};

View file

@ -0,0 +1,6 @@
#pragma once
class Base {
public:
int method() { return 42; }
};

View file

@ -0,0 +1,6 @@
#pragma once
#include "A.h"
#include "B.h"
class Derived : public A, public B {
};

View file

@ -0,0 +1,6 @@
#include "Derived.h"
void run() {
Derived d;
d.method();
}

View file

@ -0,0 +1,10 @@
namespace InterfaceDefault;
public class App
{
public static void Run()
{
User user = new User("alice");
user.Validate();
}
}

View file

@ -0,0 +1,11 @@
namespace InterfaceDefault;
public class User : IValidator
{
public string Name { get; }
public User(string name)
{
Name = name;
}
}

View file

@ -0,0 +1,6 @@
namespace InterfaceDefault;
public interface IValidator
{
bool Validate() => true;
}

View file

@ -0,0 +1,6 @@
public class App {
public static void run() {
User user = new User("alice");
user.validate();
}
}

View file

@ -0,0 +1,7 @@
public class User implements Validator {
private String name;
public User(String name) {
this.name = name;
}
}

View file

@ -0,0 +1,5 @@
public interface Validator {
default boolean validate() {
return true;
}
}

View file

@ -0,0 +1,6 @@
package example
fun run() {
val user = User("alice")
user.validate()
}

View file

@ -0,0 +1,3 @@
package example
class User(val name: String) : Validator

View file

@ -0,0 +1,5 @@
package example
interface Validator {
fun validate(): Boolean = true
}

View file

@ -0,0 +1,6 @@
from child import Child
def run() -> None:
c = Child()
c.gp_method()

View file

@ -0,0 +1,5 @@
from parent import Parent
class Child(Parent):
pass

View file

@ -0,0 +1,3 @@
class Grandparent:
def gp_method(self) -> str:
return "grandparent"

View file

@ -0,0 +1,5 @@
from grandparent import Grandparent
class Parent(Grandparent):
pass

View 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 {}

View 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();
}

View 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"
}
}

View file

@ -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');
});
});

View file

@ -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');
});
});

View file

@ -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');
});
});

View file

@ -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');
});
});

View file

@ -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');
});
});

View file

@ -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();
});
});

View file

@ -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(
{