mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-07 02:58:02 +00:00
[cli] Replace owner-filtered method lookups in type-env (#736)
* refactor(type-env): use owner method lookup * test(type-env): cover owner lookup edge cases * test(type-env): cover inherited overload ambiguity --------- Co-authored-by: 许恩宁 <xuenning@qiyi.com>
This commit is contained in:
parent
3388ae16d7
commit
83b5bec293
2 changed files with 418 additions and 18 deletions
|
|
@ -459,19 +459,19 @@ const SKIP_SUBTREE_TYPES = new Set([
|
|||
]);
|
||||
|
||||
const CLASS_LIKE_TYPES = new Set(['Class', 'Struct', 'Interface']);
|
||||
type ClassDefRef = { nodeId: string; type: string; filePath: string };
|
||||
|
||||
const lookupClassDefsByName = (
|
||||
symbolTable: SymbolTable,
|
||||
name: string,
|
||||
allowedTypes: ReadonlySet<string> = CLASS_LIKE_TYPES,
|
||||
): Array<{ nodeId: string; type: string }> =>
|
||||
symbolTable.lookupClassByName(name).filter((d) => allowedTypes.has(d.type));
|
||||
): ClassDefRef[] => symbolTable.lookupClassByName(name).filter((d) => allowedTypes.has(d.type));
|
||||
|
||||
/** Memoize class definition lookups during fixpoint iteration.
|
||||
* SymbolTable is immutable during type resolution, so results never change.
|
||||
* Eliminates redundant array allocations + filter scans across iterations. */
|
||||
const createClassDefCache = (symbolTable?: SymbolTable) => {
|
||||
const cache = new Map<string, Array<{ nodeId: string; type: string }>>();
|
||||
const cache = new Map<string, ClassDefRef[]>();
|
||||
return (typeName: string) => {
|
||||
let result = cache.get(typeName);
|
||||
if (result === undefined) {
|
||||
|
|
@ -561,7 +561,7 @@ export const isSubclassOf = (
|
|||
const walkParentChain = <T>(
|
||||
typeName: string,
|
||||
parentMap: ReadonlyMap<string, readonly string[]> | undefined,
|
||||
getClassDefs: (name: string) => Array<{ nodeId: string; type: string }>,
|
||||
getClassDefs: (name: string) => ClassDefRef[],
|
||||
lookupOnClass: (nodeId: string) => T | undefined,
|
||||
): T | undefined => {
|
||||
if (!parentMap) return undefined;
|
||||
|
|
@ -597,7 +597,7 @@ const resolveFieldType = (
|
|||
field: string,
|
||||
scopeEnv: ReadonlyMap<string, string>,
|
||||
symbolTable?: SymbolTable,
|
||||
getClassDefs?: (typeName: string) => Array<{ nodeId: string; type: string }>,
|
||||
getClassDefs?: (typeName: string) => ClassDefRef[],
|
||||
parentMap?: ReadonlyMap<string, readonly string[]>,
|
||||
): string | undefined => {
|
||||
if (!symbolTable) return undefined;
|
||||
|
|
@ -619,14 +619,14 @@ const resolveFieldType = (
|
|||
|
||||
/** Resolve a method's return type given a receiver variable and method name.
|
||||
* Uses SymbolTable to find class nodeIds for the receiver's type, then
|
||||
* looks up the method via lookupFuzzyCallable filtered by ownerId.
|
||||
* looks up the method via owner-scoped lookupMethodByOwner.
|
||||
* Falls back to MRO parent chain walking if direct lookup fails (Phase 11A). */
|
||||
const resolveMethodReturnType = (
|
||||
receiver: string,
|
||||
method: string,
|
||||
scopeEnv: ReadonlyMap<string, string>,
|
||||
symbolTable?: SymbolTable,
|
||||
getClassDefs?: (typeName: string) => Array<{ nodeId: string; type: string }>,
|
||||
getClassDefs?: (typeName: string) => ClassDefRef[],
|
||||
parentMap?: ReadonlyMap<string, readonly string[]>,
|
||||
): string | undefined => {
|
||||
if (!symbolTable) return undefined;
|
||||
|
|
@ -642,21 +642,29 @@ const resolveMethodReturnType = (
|
|||
const classDefs = lookup(receiverType);
|
||||
if (classDefs.length === 0) return undefined;
|
||||
// Direct lookup first
|
||||
const classNodeIds = new Set(classDefs.map((d) => d.nodeId));
|
||||
const methods = symbolTable
|
||||
.lookupFuzzyCallable(method)
|
||||
.filter((d) => d.ownerId && classNodeIds.has(d.ownerId));
|
||||
const directMethodLookups = classDefs.map((d) => ({
|
||||
classDef: d,
|
||||
methodDef: symbolTable.lookupMethodByOwner(d.nodeId, method),
|
||||
}));
|
||||
const hasAmbiguousDirectLookup = directMethodLookups.some(({ classDef, methodDef }) => {
|
||||
if (methodDef) return false;
|
||||
return symbolTable
|
||||
.lookupExactAll(classDef.filePath, method)
|
||||
.some((d) => d.ownerId === classDef.nodeId);
|
||||
});
|
||||
if (hasAmbiguousDirectLookup) return undefined;
|
||||
const methods = directMethodLookups
|
||||
.map(({ methodDef }) => methodDef)
|
||||
.filter((d): d is NonNullable<typeof d> => d !== undefined);
|
||||
if (methods.length === 1 && methods[0].returnType) {
|
||||
return extractReturnTypeName(methods[0].returnType);
|
||||
}
|
||||
// MRO parent chain walking on miss
|
||||
if (methods.length === 0) {
|
||||
const inherited = walkParentChain(receiverType, parentMap, lookup, (nodeId) => {
|
||||
const parentMethods = symbolTable
|
||||
.lookupFuzzyCallable(method)
|
||||
.filter((d) => d.ownerId === nodeId);
|
||||
if (parentMethods.length !== 1 || !parentMethods[0].returnType) return undefined;
|
||||
return extractReturnTypeName(parentMethods[0].returnType);
|
||||
const parentMethod = symbolTable.lookupMethodByOwner(nodeId, method);
|
||||
if (!parentMethod?.returnType) return undefined;
|
||||
return extractReturnTypeName(parentMethod.returnType);
|
||||
});
|
||||
return inherited;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { buildTypeEnv, type TypeEnvironment } from '../../src/core/ingestion/type-env.js';
|
||||
import type { SymbolDefinition, SymbolTable } from '../../src/core/ingestion/symbol-table.js';
|
||||
import {
|
||||
createSymbolTable,
|
||||
type SymbolDefinition,
|
||||
type SymbolTable,
|
||||
} from '../../src/core/ingestion/symbol-table.js';
|
||||
import {
|
||||
stripNullable,
|
||||
extractSimpleTypeName,
|
||||
|
|
@ -2445,6 +2449,394 @@ function process(user: User) {
|
|||
const typeEnv = buildTypeEnv(tree, 'typescript', { symbolTable });
|
||||
expect(flatGet(typeEnv, 'addr')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('method return type resolution uses lookupMethodByOwner-backed class defs', () => {
|
||||
const tree = parse(
|
||||
`
|
||||
function process(repo: Repo) {
|
||||
const profile = repo.getProfile();
|
||||
}
|
||||
`,
|
||||
TypeScript.typescript,
|
||||
);
|
||||
const lookupFuzzyCallable = vi.fn(() => []);
|
||||
const symbolTable = createMockSymbolTable({
|
||||
lookupClassByName: (name: string) =>
|
||||
name === 'Repo' ? [createClassDef('Repo', 'Class', 'models.ts')] : [],
|
||||
lookupMethodByOwner: (ownerNodeId: string, methodName: string) =>
|
||||
ownerNodeId === 'class:Repo' && methodName === 'getProfile'
|
||||
? {
|
||||
nodeId: 'method:Repo:getProfile',
|
||||
filePath: 'models.ts',
|
||||
type: 'Method',
|
||||
ownerId: 'class:Repo',
|
||||
returnType: 'Profile',
|
||||
}
|
||||
: undefined,
|
||||
lookupFuzzyCallable,
|
||||
});
|
||||
const typeEnv = buildTypeEnv(tree, 'typescript', { symbolTable });
|
||||
expect(flatGet(typeEnv, 'profile')).toBe('Profile');
|
||||
expect(lookupFuzzyCallable).not.toHaveBeenCalledWith('getProfile');
|
||||
});
|
||||
|
||||
it('inherited method return type resolution uses lookupMethodByOwner on parent owners', () => {
|
||||
const tree = parse(
|
||||
`
|
||||
function process(repo: Repo) {
|
||||
const profile = repo.getProfile();
|
||||
}
|
||||
`,
|
||||
TypeScript.typescript,
|
||||
);
|
||||
const lookupFuzzyCallable = vi.fn(() => []);
|
||||
const symbolTable = createMockSymbolTable({
|
||||
lookupClassByName: (name: string) => {
|
||||
if (name === 'Repo') return [createClassDef('Repo', 'Class', 'models.ts')];
|
||||
if (name === 'BaseRepo') return [createClassDef('BaseRepo', 'Class', 'base.ts')];
|
||||
return [];
|
||||
},
|
||||
lookupMethodByOwner: (ownerNodeId: string, methodName: string) =>
|
||||
ownerNodeId === 'class:BaseRepo' && methodName === 'getProfile'
|
||||
? {
|
||||
nodeId: 'method:BaseRepo:getProfile',
|
||||
filePath: 'base.ts',
|
||||
type: 'Method',
|
||||
ownerId: 'class:BaseRepo',
|
||||
returnType: 'Profile',
|
||||
}
|
||||
: undefined,
|
||||
lookupFuzzyCallable,
|
||||
});
|
||||
const typeEnv = buildTypeEnv(tree, 'typescript', {
|
||||
symbolTable,
|
||||
parentMap: new Map([['Repo', ['BaseRepo']]]),
|
||||
});
|
||||
expect(flatGet(typeEnv, 'profile')).toBe('Profile');
|
||||
expect(lookupFuzzyCallable).not.toHaveBeenCalledWith('getProfile');
|
||||
});
|
||||
|
||||
it('method return type resolution handles multiple class defs when only one owner has the method', () => {
|
||||
const tree = parse(
|
||||
`
|
||||
function process(repo: Repo) {
|
||||
const profile = repo.getProfile();
|
||||
}
|
||||
`,
|
||||
TypeScript.typescript,
|
||||
);
|
||||
const lookupFuzzyCallable = vi.fn(() => []);
|
||||
const symbolTable = createMockSymbolTable({
|
||||
lookupClassByName: (name: string) =>
|
||||
name === 'Repo'
|
||||
? [
|
||||
createClassDef('Repo', 'Class', 'models-a.ts'),
|
||||
{
|
||||
...createClassDef('Repo', 'Class', 'models-b.ts'),
|
||||
nodeId: 'class:Repo:partial',
|
||||
},
|
||||
]
|
||||
: [],
|
||||
lookupMethodByOwner: (ownerNodeId: string, methodName: string) =>
|
||||
ownerNodeId === 'class:Repo:partial' && methodName === 'getProfile'
|
||||
? {
|
||||
nodeId: 'method:Repo:getProfile',
|
||||
filePath: 'models-b.ts',
|
||||
type: 'Method',
|
||||
ownerId: 'class:Repo:partial',
|
||||
returnType: 'Profile',
|
||||
}
|
||||
: undefined,
|
||||
lookupExactAll: () => [],
|
||||
lookupFuzzyCallable,
|
||||
});
|
||||
const typeEnv = buildTypeEnv(tree, 'typescript', { symbolTable });
|
||||
expect(flatGet(typeEnv, 'profile')).toBe('Profile');
|
||||
expect(lookupFuzzyCallable).not.toHaveBeenCalledWith('getProfile');
|
||||
});
|
||||
|
||||
it('method return type resolution with multiple class defs falls back to MRO when direct owners miss', () => {
|
||||
const tree = parse(
|
||||
`
|
||||
function process(repo: Repo) {
|
||||
const profile = repo.getProfile();
|
||||
}
|
||||
`,
|
||||
TypeScript.typescript,
|
||||
);
|
||||
const lookupFuzzyCallable = vi.fn(() => []);
|
||||
const symbolTable = createMockSymbolTable({
|
||||
lookupClassByName: (name: string) => {
|
||||
if (name === 'Repo') {
|
||||
return [
|
||||
createClassDef('Repo', 'Class', 'models-a.ts'),
|
||||
{ ...createClassDef('Repo', 'Class', 'models-b.ts'), nodeId: 'class:Repo:partial' },
|
||||
];
|
||||
}
|
||||
if (name === 'BaseRepo') return [createClassDef('BaseRepo', 'Class', 'base.ts')];
|
||||
return [];
|
||||
},
|
||||
lookupMethodByOwner: (ownerNodeId: string, methodName: string) =>
|
||||
ownerNodeId === 'class:BaseRepo' && methodName === 'getProfile'
|
||||
? {
|
||||
nodeId: 'method:BaseRepo:getProfile',
|
||||
filePath: 'base.ts',
|
||||
type: 'Method',
|
||||
ownerId: 'class:BaseRepo',
|
||||
returnType: 'Profile',
|
||||
}
|
||||
: undefined,
|
||||
lookupExactAll: () => [],
|
||||
lookupFuzzyCallable,
|
||||
});
|
||||
const typeEnv = buildTypeEnv(tree, 'typescript', {
|
||||
symbolTable,
|
||||
parentMap: new Map([['Repo', ['BaseRepo']]]),
|
||||
});
|
||||
expect(flatGet(typeEnv, 'profile')).toBe('Profile');
|
||||
expect(lookupFuzzyCallable).not.toHaveBeenCalledWith('getProfile');
|
||||
});
|
||||
|
||||
it('method return type resolution stays unresolved when multiple class defs each define the method', () => {
|
||||
const tree = parse(
|
||||
`
|
||||
function process(repo: Repo) {
|
||||
const profile = repo.getProfile();
|
||||
}
|
||||
`,
|
||||
TypeScript.typescript,
|
||||
);
|
||||
const lookupFuzzyCallable = vi.fn(() => []);
|
||||
const symbolTable = createMockSymbolTable({
|
||||
lookupClassByName: (name: string) =>
|
||||
name === 'Repo'
|
||||
? [
|
||||
createClassDef('Repo', 'Class', 'models-a.ts'),
|
||||
{
|
||||
...createClassDef('Repo', 'Class', 'models-b.ts'),
|
||||
nodeId: 'class:Repo:partial',
|
||||
},
|
||||
]
|
||||
: [],
|
||||
lookupMethodByOwner: (ownerNodeId: string, methodName: string) => {
|
||||
if (methodName !== 'getProfile') return undefined;
|
||||
if (ownerNodeId === 'class:Repo') {
|
||||
return {
|
||||
nodeId: 'method:Repo:getProfile#a',
|
||||
filePath: 'models-a.ts',
|
||||
type: 'Method',
|
||||
ownerId: 'class:Repo',
|
||||
returnType: 'Profile',
|
||||
};
|
||||
}
|
||||
if (ownerNodeId === 'class:Repo:partial') {
|
||||
return {
|
||||
nodeId: 'method:Repo:getProfile#b',
|
||||
filePath: 'models-b.ts',
|
||||
type: 'Method',
|
||||
ownerId: 'class:Repo:partial',
|
||||
returnType: 'Profile',
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
lookupExactAll: () => [],
|
||||
lookupFuzzyCallable,
|
||||
});
|
||||
const typeEnv = buildTypeEnv(tree, 'typescript', { symbolTable });
|
||||
expect(flatGet(typeEnv, 'profile')).toBeUndefined();
|
||||
expect(lookupFuzzyCallable).not.toHaveBeenCalledWith('getProfile');
|
||||
});
|
||||
|
||||
it('method return type resolution preserves same-return overload success', () => {
|
||||
const tree = parse(
|
||||
`
|
||||
function process(repo: Repo) {
|
||||
const profile = repo.getProfile();
|
||||
}
|
||||
`,
|
||||
TypeScript.typescript,
|
||||
);
|
||||
const lookupFuzzyCallable = vi.fn(() => []);
|
||||
const symbolTable = createMockSymbolTable({
|
||||
lookupClassByName: (name: string) =>
|
||||
name === 'Repo' ? [createClassDef('Repo', 'Class', 'models.ts')] : [],
|
||||
lookupMethodByOwner: (ownerNodeId: string, methodName: string) =>
|
||||
ownerNodeId === 'class:Repo' && methodName === 'getProfile'
|
||||
? {
|
||||
nodeId: 'method:Repo:getProfile#1',
|
||||
filePath: 'models.ts',
|
||||
type: 'Method',
|
||||
ownerId: 'class:Repo',
|
||||
returnType: 'Profile',
|
||||
}
|
||||
: undefined,
|
||||
lookupExactAll: (filePath: string, name: string) =>
|
||||
filePath === 'models.ts' && name === 'getProfile'
|
||||
? [
|
||||
{
|
||||
nodeId: 'method:Repo:getProfile#1',
|
||||
filePath: 'models.ts',
|
||||
type: 'Method',
|
||||
ownerId: 'class:Repo',
|
||||
returnType: 'Profile',
|
||||
},
|
||||
{
|
||||
nodeId: 'method:Repo:getProfile#2',
|
||||
filePath: 'models.ts',
|
||||
type: 'Method',
|
||||
ownerId: 'class:Repo',
|
||||
returnType: 'Profile',
|
||||
},
|
||||
]
|
||||
: [],
|
||||
lookupFuzzyCallable,
|
||||
});
|
||||
const typeEnv = buildTypeEnv(tree, 'typescript', { symbolTable });
|
||||
expect(flatGet(typeEnv, 'profile')).toBe('Profile');
|
||||
expect(lookupFuzzyCallable).not.toHaveBeenCalledWith('getProfile');
|
||||
});
|
||||
|
||||
it('method return type resolution stays unresolved for ambiguous overloads with differing returns', () => {
|
||||
const tree = parse(
|
||||
`
|
||||
function process(repo: Repo) {
|
||||
const profile = repo.getProfile();
|
||||
}
|
||||
`,
|
||||
TypeScript.typescript,
|
||||
);
|
||||
const lookupFuzzyCallable = vi.fn(() => []);
|
||||
const symbolTable = createMockSymbolTable({
|
||||
lookupClassByName: (name: string) => {
|
||||
if (name === 'Repo') return [createClassDef('Repo', 'Class', 'models.ts')];
|
||||
if (name === 'BaseRepo') return [createClassDef('BaseRepo', 'Class', 'base.ts')];
|
||||
return [];
|
||||
},
|
||||
lookupMethodByOwner: (ownerNodeId: string, methodName: string) =>
|
||||
ownerNodeId === 'class:BaseRepo' && methodName === 'getProfile'
|
||||
? {
|
||||
nodeId: 'method:BaseRepo:getProfile',
|
||||
filePath: 'base.ts',
|
||||
type: 'Method',
|
||||
ownerId: 'class:BaseRepo',
|
||||
returnType: 'Profile',
|
||||
}
|
||||
: undefined,
|
||||
lookupExactAll: (filePath: string, name: string) =>
|
||||
filePath === 'models.ts' && name === 'getProfile'
|
||||
? [
|
||||
{
|
||||
nodeId: 'method:Repo:getProfile#1',
|
||||
filePath: 'models.ts',
|
||||
type: 'Method',
|
||||
ownerId: 'class:Repo',
|
||||
returnType: 'User',
|
||||
},
|
||||
{
|
||||
nodeId: 'method:Repo:getProfile#2',
|
||||
filePath: 'models.ts',
|
||||
type: 'Method',
|
||||
ownerId: 'class:Repo',
|
||||
returnType: 'Admin',
|
||||
},
|
||||
]
|
||||
: [],
|
||||
lookupFuzzyCallable,
|
||||
});
|
||||
const typeEnv = buildTypeEnv(tree, 'typescript', {
|
||||
symbolTable,
|
||||
parentMap: new Map([['Repo', ['BaseRepo']]]),
|
||||
});
|
||||
expect(flatGet(typeEnv, 'profile')).toBeUndefined();
|
||||
expect(lookupFuzzyCallable).not.toHaveBeenCalledWith('getProfile');
|
||||
});
|
||||
|
||||
it('inherited method return type resolution preserves same-return overload success on parent owners', () => {
|
||||
const tree = parse(
|
||||
`
|
||||
function process(repo: Repo) {
|
||||
const profile = repo.getProfile();
|
||||
}
|
||||
`,
|
||||
TypeScript.typescript,
|
||||
);
|
||||
const lookupFuzzyCallable = vi.fn(() => []);
|
||||
const symbolTable = createMockSymbolTable({
|
||||
lookupClassByName: (name: string) => {
|
||||
if (name === 'Repo') return [createClassDef('Repo', 'Class', 'models.ts')];
|
||||
if (name === 'BaseRepo') return [createClassDef('BaseRepo', 'Class', 'base.ts')];
|
||||
return [];
|
||||
},
|
||||
lookupMethodByOwner: (ownerNodeId: string, methodName: string) =>
|
||||
ownerNodeId === 'class:BaseRepo' && methodName === 'getProfile'
|
||||
? {
|
||||
nodeId: 'method:BaseRepo:getProfile#1',
|
||||
filePath: 'base.ts',
|
||||
type: 'Method',
|
||||
ownerId: 'class:BaseRepo',
|
||||
returnType: 'Profile',
|
||||
}
|
||||
: undefined,
|
||||
lookupExactAll: (filePath: string, name: string) =>
|
||||
filePath === 'base.ts' && name === 'getProfile'
|
||||
? [
|
||||
{
|
||||
nodeId: 'method:BaseRepo:getProfile#1',
|
||||
filePath: 'base.ts',
|
||||
type: 'Method',
|
||||
ownerId: 'class:BaseRepo',
|
||||
returnType: 'Profile',
|
||||
},
|
||||
{
|
||||
nodeId: 'method:BaseRepo:getProfile#2',
|
||||
filePath: 'base.ts',
|
||||
type: 'Method',
|
||||
ownerId: 'class:BaseRepo',
|
||||
returnType: 'Profile',
|
||||
},
|
||||
]
|
||||
: [],
|
||||
lookupFuzzyCallable,
|
||||
});
|
||||
const typeEnv = buildTypeEnv(tree, 'typescript', {
|
||||
symbolTable,
|
||||
parentMap: new Map([['Repo', ['BaseRepo']]]),
|
||||
});
|
||||
expect(flatGet(typeEnv, 'profile')).toBe('Profile');
|
||||
expect(lookupFuzzyCallable).not.toHaveBeenCalledWith('getProfile');
|
||||
});
|
||||
|
||||
it('inherited method return type resolution stays unresolved for ambiguous overloads on parent owners', () => {
|
||||
const tree = parse(
|
||||
`
|
||||
function process(repo: Repo) {
|
||||
const profile = repo.getProfile();
|
||||
}
|
||||
`,
|
||||
TypeScript.typescript,
|
||||
);
|
||||
const symbolTable = createSymbolTable();
|
||||
symbolTable.add('models.ts', 'Repo', 'class:Repo', 'Class');
|
||||
symbolTable.add('base.ts', 'BaseRepo', 'class:BaseRepo', 'Class');
|
||||
symbolTable.add('base.ts', 'getProfile', 'method:BaseRepo:getProfile#1', 'Method', {
|
||||
ownerId: 'class:BaseRepo',
|
||||
parameterCount: 1,
|
||||
returnType: 'User',
|
||||
});
|
||||
symbolTable.add('base.ts', 'getProfile', 'method:BaseRepo:getProfile#2', 'Method', {
|
||||
ownerId: 'class:BaseRepo',
|
||||
parameterCount: 2,
|
||||
returnType: 'Admin',
|
||||
});
|
||||
const lookupFuzzyCallable = vi.spyOn(symbolTable, 'lookupFuzzyCallable');
|
||||
const typeEnv = buildTypeEnv(tree, 'typescript', {
|
||||
symbolTable,
|
||||
parentMap: new Map([['Repo', ['BaseRepo']]]),
|
||||
});
|
||||
expect(flatGet(typeEnv, 'profile')).toBeUndefined();
|
||||
expect(lookupFuzzyCallable).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Python walrus operator type inference', () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue