mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-08 03:08:13 +00:00
feat: container descriptor table for generic type arg resolution
Replace simple KEY_METHODS heuristic with CONTAINER_DESCRIPTORS table that maps 30+ container types across all languages to their type parameter semantics per access method. Key improvements: - Container-aware resolution: HashMap.iter() correctly yields V (arity 2), while Vec.iter() yields T (arity 1) — same method, different semantics - Cross-language coverage: Map/HashMap/BTreeMap/dict/Dict/Dictionary/ ConcurrentHashMap + List/Vec/Set/HashSet/Queue/Deque/Stack etc. - Method categorization: keyMethods (keys/keySet/Keys) vs valueMethods (values/get/pop/iter/first/last) per container type - Fallback for unknown containers: still uses method name heuristic, so MyCache<K,V>.keys() correctly returns first arg - Exported getContainerDescriptor() for future heritage-chain lookups Each language extractor now passes containerTypeName from scopeEnv to methodToTypeArgPosition for descriptor-aware resolution. 252 unit tests pass (4 new descriptor tests), 1 skip (Ruby).
This commit is contained in:
parent
656af32e52
commit
b4986fdaba
5 changed files with 163 additions and 10 deletions
|
|
@ -256,7 +256,8 @@ const extractForLoopBinding: ForLoopExtractor = (
|
|||
}
|
||||
if (!iterableName) return;
|
||||
|
||||
const typeArgPos = methodToTypeArgPosition(methodName);
|
||||
const containerTypeName = scopeEnv.get(iterableName);
|
||||
const typeArgPos = methodToTypeArgPosition(methodName, containerTypeName);
|
||||
const elementType = resolveIterableElementType(
|
||||
iterableName, node, scopeEnv, declarationTypeNodes, scope,
|
||||
extractPyElementTypeFromAnnotation, findPyParamElementType,
|
||||
|
|
|
|||
|
|
@ -383,7 +383,8 @@ const extractForLoopBinding: ForLoopExtractor = (
|
|||
}
|
||||
if (!iterableName) return;
|
||||
|
||||
const typeArgPos = methodToTypeArgPosition(methodName);
|
||||
const containerTypeName = scopeEnv.get(iterableName);
|
||||
const typeArgPos = methodToTypeArgPosition(methodName, containerTypeName);
|
||||
const elementType = resolveIterableElementType(
|
||||
iterableName, node, scopeEnv, declarationTypeNodes, scope,
|
||||
extractRustElementTypeFromTypeNode, findRustParamElementType,
|
||||
|
|
|
|||
|
|
@ -5,14 +5,112 @@ import type { SyntaxNode } from '../utils.js';
|
|||
* - 'last': value type (e.g., V from Map<K,V>) — used for .values(), .items(), .iter() */
|
||||
export type TypeArgPosition = 'first' | 'last';
|
||||
|
||||
/** Map method names to which type argument they yield.
|
||||
* Methods that iterate/return keys → 'first'; everything else → 'last'. */
|
||||
const KEY_METHODS = new Set(['keys', 'keySet', 'Keys']);
|
||||
// ---------------------------------------------------------------------------
|
||||
// Container type descriptors — maps container base names to type parameter
|
||||
// semantics per access method. Replaces the simple KEY_METHODS heuristic.
|
||||
//
|
||||
// For user-defined generics (MyCache<K,V> extends Map<K,V>), heritage-aware
|
||||
// fallback can walk the EXTENDS chain to find a matching descriptor.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Determine which type arg to use based on the iterator method name.
|
||||
* .keys() / .keySet() → 'first' (key type); everything else → 'last' (value type). */
|
||||
export function methodToTypeArgPosition(methodName: string | undefined): TypeArgPosition {
|
||||
return methodName && KEY_METHODS.has(methodName) ? 'first' : 'last';
|
||||
/** Describes which type parameter position each access method yields. */
|
||||
interface ContainerDescriptor {
|
||||
/** Number of type parameters (1 = single-element, 2 = key-value) */
|
||||
arity: number;
|
||||
/** Methods that yield the first type parameter (key type for maps) */
|
||||
keyMethods: ReadonlySet<string>;
|
||||
/** Methods that yield the last type parameter (value type) */
|
||||
valueMethods: ReadonlySet<string>;
|
||||
}
|
||||
|
||||
/** Empty set for containers that have no key-yielding methods */
|
||||
const NO_KEYS: ReadonlySet<string> = new Set();
|
||||
|
||||
/** Standard key-yielding methods across languages */
|
||||
const STD_KEY_METHODS: ReadonlySet<string> = new Set(['keys']);
|
||||
const JAVA_KEY_METHODS: ReadonlySet<string> = new Set(['keySet']);
|
||||
const CSHARP_KEY_METHODS: ReadonlySet<string> = new Set(['Keys']);
|
||||
|
||||
/** Standard value-yielding methods across languages */
|
||||
const STD_VALUE_METHODS: ReadonlySet<string> = new Set(['values', 'get', 'pop', 'remove']);
|
||||
const CSHARP_VALUE_METHODS: ReadonlySet<string> = new Set(['Values', 'TryGetValue']);
|
||||
const SINGLE_ELEMENT_METHODS: ReadonlySet<string> = new Set([
|
||||
'iter', 'into_iter', 'iterator', 'get', 'first', 'last', 'pop',
|
||||
'peek', 'poll', 'find', 'filter', 'map',
|
||||
]);
|
||||
|
||||
const CONTAINER_DESCRIPTORS: ReadonlyMap<string, ContainerDescriptor> = new Map([
|
||||
// --- Map / Dict types (arity 2: key + value) ---
|
||||
['Map', { arity: 2, keyMethods: STD_KEY_METHODS, valueMethods: STD_VALUE_METHODS }],
|
||||
['WeakMap', { arity: 2, keyMethods: STD_KEY_METHODS, valueMethods: STD_VALUE_METHODS }],
|
||||
['HashMap', { arity: 2, keyMethods: STD_KEY_METHODS, valueMethods: STD_VALUE_METHODS }],
|
||||
['BTreeMap', { arity: 2, keyMethods: STD_KEY_METHODS, valueMethods: STD_VALUE_METHODS }],
|
||||
['LinkedHashMap', { arity: 2, keyMethods: JAVA_KEY_METHODS, valueMethods: STD_VALUE_METHODS }],
|
||||
['TreeMap', { arity: 2, keyMethods: JAVA_KEY_METHODS, valueMethods: STD_VALUE_METHODS }],
|
||||
['dict', { arity: 2, keyMethods: STD_KEY_METHODS, valueMethods: STD_VALUE_METHODS }],
|
||||
['Dict', { arity: 2, keyMethods: STD_KEY_METHODS, valueMethods: STD_VALUE_METHODS }],
|
||||
['Dictionary', { arity: 2, keyMethods: CSHARP_KEY_METHODS, valueMethods: CSHARP_VALUE_METHODS }],
|
||||
['SortedDictionary', { arity: 2, keyMethods: CSHARP_KEY_METHODS, valueMethods: CSHARP_VALUE_METHODS }],
|
||||
['Record', { arity: 2, keyMethods: STD_KEY_METHODS, valueMethods: STD_VALUE_METHODS }],
|
||||
['OrderedDict', { arity: 2, keyMethods: STD_KEY_METHODS, valueMethods: STD_VALUE_METHODS }],
|
||||
['ConcurrentHashMap', { arity: 2, keyMethods: JAVA_KEY_METHODS, valueMethods: STD_VALUE_METHODS }],
|
||||
['ConcurrentDictionary', { arity: 2, keyMethods: CSHARP_KEY_METHODS, valueMethods: CSHARP_VALUE_METHODS }],
|
||||
|
||||
// --- Single-element containers (arity 1) ---
|
||||
['Array', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['List', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['ArrayList', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['LinkedList',{ arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['Vec', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['VecDeque', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['Set', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['HashSet', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['BTreeSet', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['TreeSet', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['Queue', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['Deque', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['Stack', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['Sequence', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['Iterable', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['Iterator', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['IEnumerable', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['IList', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['list', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['set', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['tuple', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
['frozenset', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
|
||||
]);
|
||||
|
||||
/** Determine which type arg to extract based on container type name and access method.
|
||||
*
|
||||
* Resolution order:
|
||||
* 1. If container is known and method is in keyMethods → 'first'
|
||||
* 2. If container is known with arity 1 → 'last' (same as 'first' for single-arg)
|
||||
* 3. If container is unknown → fall back to method name heuristic
|
||||
* 4. Default: 'last' (value type)
|
||||
*/
|
||||
export function methodToTypeArgPosition(methodName: string | undefined, containerTypeName?: string): TypeArgPosition {
|
||||
if (containerTypeName) {
|
||||
const desc = CONTAINER_DESCRIPTORS.get(containerTypeName);
|
||||
if (desc) {
|
||||
// Single-element container: always 'last' (= only arg)
|
||||
if (desc.arity === 1) return 'last';
|
||||
// Multi-element: check if method yields key type
|
||||
if (methodName && desc.keyMethods.has(methodName)) return 'first';
|
||||
// Default for multi-element: value type
|
||||
return 'last';
|
||||
}
|
||||
}
|
||||
// Fallback for unknown containers: simple method name heuristic
|
||||
if (methodName && (methodName === 'keys' || methodName === 'keySet' || methodName === 'Keys')) {
|
||||
return 'first';
|
||||
}
|
||||
return 'last';
|
||||
}
|
||||
|
||||
/** Look up the container descriptor for a type name. Exported for heritage-chain lookups. */
|
||||
export function getContainerDescriptor(typeName: string): ContainerDescriptor | undefined {
|
||||
return CONTAINER_DESCRIPTORS.get(typeName);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -356,7 +356,9 @@ const extractForLoopBinding: ForLoopExtractor = (
|
|||
}
|
||||
if (!iterableName) return;
|
||||
|
||||
const typeArgPos = methodToTypeArgPosition(methodName);
|
||||
// Look up the container's base type name for descriptor-aware resolution
|
||||
const containerTypeName = scopeEnv.get(iterableName);
|
||||
const typeArgPos = methodToTypeArgPosition(methodName, containerTypeName);
|
||||
const elementType = resolveIterableElementType(
|
||||
iterableName, node, scopeEnv, declarationTypeNodes, scope,
|
||||
extractTsElementTypeFromAnnotation, findTsIterableElementType,
|
||||
|
|
|
|||
|
|
@ -3056,6 +3056,57 @@ fn process(data: HashMap<String, User>) {
|
|||
});
|
||||
});
|
||||
|
||||
describe('container descriptor-aware type arg selection', () => {
|
||||
it('HashMap.keys() resolves to key type (String) via descriptor', () => {
|
||||
const tree = parse(`
|
||||
fn process(data: HashMap<String, User>) {
|
||||
for key in data.keys() {
|
||||
key.len();
|
||||
}
|
||||
}
|
||||
`, Rust);
|
||||
const { env } = buildTypeEnv(tree, 'rust');
|
||||
expect(flatGet(env, 'key')).toBe('String');
|
||||
});
|
||||
|
||||
it('HashMap.values() resolves to value type (User) via descriptor', () => {
|
||||
const tree = parse(`
|
||||
fn process(data: HashMap<String, User>) {
|
||||
for user in data.values() {
|
||||
user.save();
|
||||
}
|
||||
}
|
||||
`, Rust);
|
||||
const { env } = buildTypeEnv(tree, 'rust');
|
||||
expect(flatGet(env, 'user')).toBe('User');
|
||||
});
|
||||
|
||||
it('Vec.iter() resolves to element type (User) — arity 1 always returns last', () => {
|
||||
const tree = parse(`
|
||||
fn process(users: Vec<User>) {
|
||||
for user in users.iter() {
|
||||
user.save();
|
||||
}
|
||||
}
|
||||
`, Rust);
|
||||
const { env } = buildTypeEnv(tree, 'rust');
|
||||
expect(flatGet(env, 'user')).toBe('User');
|
||||
});
|
||||
|
||||
it('unknown container falls back to last-arg heuristic', () => {
|
||||
// MyCache is not in CONTAINER_DESCRIPTORS, so .keys() still returns first via fallback
|
||||
const tree = parse(`
|
||||
function process(cache: MyCache<string, User>) {
|
||||
for (const key of cache.keys()) {
|
||||
key.trim();
|
||||
}
|
||||
}
|
||||
`, TypeScript.typescript);
|
||||
const { env } = buildTypeEnv(tree, 'typescript');
|
||||
expect(flatGet(env, 'key')).toBe('string');
|
||||
});
|
||||
});
|
||||
|
||||
describe('known limitations (documented skip tests)', () => {
|
||||
it.skip('Ruby block parameter: users.each { |user| } — closure param inference, different feature', () => {
|
||||
// Not a for-loop; .each { |user| } is a method call with a block.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue