feat(SM-11): extract resolveMemberCall from resolveCallTarget

- Create resolveMemberCall(ownerType, methodName, currentFile, ctx, heritageMap?)
  that uses owner-scoped + MRO resolution only (no fuzzy lookup)
- resolveCallTarget delegates member calls (D0 path) to resolveMemberCall
- walkMixedChain uses resolveMemberCall for owner-scoped member-call resolution
- Add 7 unit tests for resolveMemberCall covering direct, inherited, MRO,
  null cases, and confidence tier assertions
- Export resolveMemberCall for external use

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/3b7889a9-5f2f-4572-8904-45084210f10d
This commit is contained in:
copilot-swe-agent[bot] 2026-04-08 22:37:33 +00:00 committed by GitHub
parent ab0ebee497
commit 6695c8aae6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 170 additions and 18 deletions

View file

@ -17,6 +17,7 @@
"commander": "^12.0.0",
"cors": "^2.8.5",
"express": "^4.19.2",
"gitnexus-shared": "file:../gitnexus-shared",
"glob": "^11.0.0",
"graphology": "^0.25.4",
"graphology-indices": "^0.17.0",

View file

@ -1365,33 +1365,29 @@ const resolveCallTarget = (
// belong to the wrong class (e.g. super.save() should hit the parent's save,
// not the child's own save method in the same file).
if (call.callForm === 'member' && call.receiverTypeName) {
// D0. MRO fast path: when heritageMap is available, try owner-scoped + MRO
// lookup before falling back to the expensive D2 fuzzy widening.
// This short-circuits the lookupFuzzy call for every cross-file member call.
// D0. Delegate to resolveMemberCall (SM-11): owner-scoped + MRO lookup
// before falling back to the expensive D1-D4 fuzzy widening.
// Skip conditions:
// (a) overloadHints or preComputedArgTypes present — the MRO lookup may
// pick the wrong overload for same-return-type overloads since it
// does not consider argument types. D2-D4+E handles those correctly.
// does not consider argument types. D1-D4+E handles those correctly.
// (b) A module alias on call.receiverName is active for this file — the
// alias block above already narrowed `filteredCandidates` to a
// specific file (e.g. Python `import auth; auth.user.save()`).
// resolveMethodByOwner re-resolves `receiverTypeName` from scratch
// via `ctx.resolve`, which ignores that narrowing and could pick a
// homonymous class from the wrong file. Fall through to D1-D4 which
// respects the alias-filtered candidate pool.
// specific file. resolveMemberCall re-resolves `receiverTypeName`
// from scratch via `ctx.resolve`, which ignores that narrowing and
// could pick a homonymous class from the wrong file. Fall through to
// D1-D4 which respects the alias-filtered candidate pool.
const hasActiveModuleAlias =
!!call.receiverName && ctx.moduleAliasMap?.get(currentFile)?.has(call.receiverName) === true;
if (!overloadHints && !preComputedArgTypes && !hasActiveModuleAlias) {
const mroResult = resolveMethodByOwner(
const memberResult = resolveMemberCall(
call.receiverTypeName,
call.calledName,
currentFile,
ctx,
heritageMap,
);
if (mroResult) {
return toResolveResult(mroResult, tiered.tier);
}
if (memberResult) return memberResult;
}
// D1. Resolve the receiver type
@ -1671,6 +1667,46 @@ const resolveMethodByOwner = (
return ctx.symbols.lookupMethodByOwner(classDef.nodeId, methodName);
};
// ---------------------------------------------------------------------------
// SM-11: Owner-scoped + MRO member-call resolution (no fuzzy lookup)
// ---------------------------------------------------------------------------
/**
* Resolve a member call using owner-scoped + MRO resolution only (no fuzzy lookup).
* Used for `obj.method()` calls where the receiver type is known.
*
* Delegates to {@link resolveMethodByOwner} which performs an O(1) owner-scoped
* method lookup and, when a {@link HeritageMap} is provided, walks the MRO chain
* via {@link lookupMethodByOwnerWithMRO}.
*
* {@link resolveCallTarget} delegates here for member calls before falling back
* to the more expensive fuzzy-widening path (D1-D4).
*
* @param ownerType - The receiver's type name (e.g. 'User')
* @param methodName - The method being called (e.g. 'save')
* @param currentFile - File path of the call site
* @param ctx - Resolution context
* @param heritageMap - Optional heritage map for MRO-aware ancestor walking
*/
export const resolveMemberCall = (
ownerType: string,
methodName: string,
currentFile: string,
ctx: ResolutionContext,
heritageMap?: HeritageMap,
): ResolveResult | null => {
const methodDef = resolveMethodByOwner(ownerType, methodName, currentFile, ctx, heritageMap);
if (!methodDef) return null;
// Determine confidence tier from how the owner type was resolved.
// ctx.resolve is per-file cached so this second call (resolveMethodByOwner
// already called it internally) is essentially free.
const ownerResolved = ctx.resolve(ownerType, currentFile);
const tier = ownerResolved?.tier ?? 'global';
return toResolveResult(methodDef, tier);
};
// ---------------------------------------------------------------------------
// MRO-aware method resolution via HeritageMap (SM-9)
// ---------------------------------------------------------------------------
@ -1860,13 +1896,12 @@ const walkMixedChain = (
currentType = fieldResolved.typeName;
continue;
}
// Fast path: O(1) owner-scoped method lookup via methodByOwner index.
// Avoids fuzzy lookup when the owner type is known and the method is unambiguous.
// SM-11: delegate to resolveMemberCall for owner-scoped + MRO resolution.
// Note: CALLS edges for intermediate chain steps are NOT emitted here — walkMixedChain
// only threads types. CALLS edges come from the outer per-call-expression loop in processCalls.
const methodDef = resolveMethodByOwner(currentType, step.name, filePath, ctx, heritageMap);
if (methodDef?.returnType) {
const fastRetType = extractReturnTypeName(methodDef.returnType);
const memberResult = resolveMemberCall(currentType, step.name, filePath, ctx, heritageMap);
if (memberResult?.returnType) {
const fastRetType = extractReturnTypeName(memberResult.returnType);
if (fastRetType) {
currentType = fastRetType;
continue;

View file

@ -1398,3 +1398,119 @@ describe('lookupMethodByOwnerWithMRO', () => {
expect(result!.nodeId).toBe('method:User:getName');
});
});
// ---------------------------------------------------------------------------
// resolveMemberCall — SM-11: owner-scoped + MRO member-call resolution
// ---------------------------------------------------------------------------
import { resolveMemberCall } from '../../src/core/ingestion/call-processor.js';
describe('resolveMemberCall', () => {
let ctx: ResolutionContext;
beforeEach(() => {
ctx = createResolutionContext();
});
it('resolves direct method on owner type', () => {
ctx.symbols.add('src/user.ts', 'User', 'class:User', 'Class');
ctx.symbols.add('src/user.ts', 'save', 'method:User:save', 'Method', {
returnType: 'void',
ownerId: 'class:User',
});
ctx.importMap.set('src/app.ts', new Set(['src/user.ts']));
const result = resolveMemberCall('User', 'save', 'src/app.ts', ctx);
expect(result).not.toBeNull();
expect(result!.nodeId).toBe('method:User:save');
expect(result!.returnType).toBe('void');
expect(result!.confidence).toBeGreaterThan(0);
});
it('resolves inherited method via MRO walk', () => {
ctx.symbols.add('src/parent.java', 'Parent', 'class:Parent', 'Class');
ctx.symbols.add('src/child.java', 'Child', 'class:Child', 'Class');
ctx.symbols.add('src/parent.java', 'validate', 'method:Parent:validate', 'Method', {
returnType: 'boolean',
ownerId: 'class:Parent',
});
ctx.importMap.set('src/app.java', new Set(['src/child.java', 'src/parent.java']));
const heritage: ExtractedHeritage[] = [
{ filePath: 'src/child.java', className: 'Child', parentName: 'Parent', kind: 'extends' },
];
const map = buildHeritageMap(heritage, ctx);
const result = resolveMemberCall('Child', 'validate', 'src/app.java', ctx, map);
expect(result).not.toBeNull();
expect(result!.nodeId).toBe('method:Parent:validate');
expect(result!.returnType).toBe('boolean');
});
it('returns null for unknown owner type', () => {
const result = resolveMemberCall('NonExistent', 'save', 'src/app.ts', ctx);
expect(result).toBeNull();
});
it('returns null for unknown method on known owner', () => {
ctx.symbols.add('src/user.ts', 'User', 'class:User', 'Class');
ctx.importMap.set('src/app.ts', new Set(['src/user.ts']));
const result = resolveMemberCall('User', 'nonExistentMethod', 'src/app.ts', ctx);
expect(result).toBeNull();
});
it('returns result with correct confidence tier for same-file resolution', () => {
ctx.symbols.add('src/app.ts', 'User', 'class:User', 'Class');
ctx.symbols.add('src/app.ts', 'save', 'method:User:save', 'Method', {
returnType: 'void',
ownerId: 'class:User',
});
const result = resolveMemberCall('User', 'save', 'src/app.ts', ctx);
expect(result).not.toBeNull();
expect(result!.confidence).toBe(0.95); // same-file tier
expect(result!.reason).toBe('same-file');
});
it('returns result with import-scoped tier for cross-file resolution', () => {
ctx.symbols.add('src/user.ts', 'User', 'class:User', 'Class');
ctx.symbols.add('src/user.ts', 'save', 'method:User:save', 'Method', {
returnType: 'void',
ownerId: 'class:User',
});
ctx.importMap.set('src/app.ts', new Set(['src/user.ts']));
const result = resolveMemberCall('User', 'save', 'src/app.ts', ctx);
expect(result).not.toBeNull();
expect(result!.confidence).toBe(0.9); // import-scoped tier
expect(result!.reason).toBe('import-resolved');
});
it('resolves with heritage map across C3 MRO chain (Python)', () => {
ctx.symbols.add('src/a.py', 'A', 'class:A', 'Class');
ctx.symbols.add('src/b.py', 'B', 'class:B', 'Class');
ctx.symbols.add('src/c.py', 'C', 'class:C', 'Class');
ctx.symbols.add('src/a.py', 'foo', 'method:A:foo', 'Method', {
returnType: 'str',
ownerId: 'class:A',
});
ctx.importMap.set('src/main.py', new Set(['src/a.py', 'src/b.py', 'src/c.py']));
const heritage: ExtractedHeritage[] = [
{ filePath: 'src/c.py', className: 'C', parentName: 'B', kind: 'extends' },
{ filePath: 'src/b.py', className: 'B', parentName: 'A', kind: 'extends' },
];
const map = buildHeritageMap(heritage, ctx);
const result = resolveMemberCall('C', 'foo', 'src/main.py', ctx, map);
expect(result).not.toBeNull();
expect(result!.nodeId).toBe('method:A:foo');
expect(result!.returnType).toBe('str');
});
});