mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-24 00:51:53 +00:00
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:
parent
ab0ebee497
commit
6695c8aae6
3 changed files with 170 additions and 18 deletions
1
gitnexus/package-lock.json
generated
1
gitnexus/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue