mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-08 03:08:13 +00:00
feat: resolve 4 known limitation skip tests + method-aware type arg selection
Unskip 4 of 5 type-env known limitations with full integration test coverage:
1. TS destructured for-of: handle array_pattern by binding last named child
to element type. Fix Map<K,V> to return last generic arg (value type).
2. Python dict.items() loop: handle `call` iterables + `pattern_list` left
side. Fix dict[K,V] extraction via type_parameter with last-arg heuristic.
Unwrap `type` wrapper in extractPyElementTypeFromAnnotation.
3. TS instanceof narrowing: add extractPatternBinding for binary_expression
with positional child access. First-writer-wins (not block-scoped).
4. Rust .iter() for-loops: handle call_expression in for_expression value
node by extracting receiver from field_expression.
Method-aware type arg resolution:
- Add TypeArgPosition ('first'|'last') to resolveIterableElementType
- .keys()/.keySet()/.Keys → first type arg (key); all else → last (value)
- Thread position through all 3 strategy callbacks in TS/Rust/Python
- Add predefined_type to extractSimpleTypeName for TS primitives (string etc)
New fixtures: rust-iter-for-loop, typescript-destructured-for-of,
typescript-instanceof-narrowing, python-dict-items-loop.
248 unit tests pass (6 new), 1 skip (Ruby block params).
This commit is contained in:
parent
5f12ffccf2
commit
656af32e52
20 changed files with 500 additions and 77 deletions
|
|
@ -1,6 +1,6 @@
|
|||
import type { SyntaxNode } from '../utils.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, PendingAssignmentExtractor, PatternBindingExtractor, ForLoopExtractor } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName, extractElementTypeFromString, extractGenericTypeArgs, resolveIterableElementType } from './shared.js';
|
||||
import { extractSimpleTypeName, extractVarName, extractElementTypeFromString, extractGenericTypeArgs, resolveIterableElementType, methodToTypeArgPosition, type TypeArgPosition } from './shared.js';
|
||||
|
||||
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
|
||||
'assignment',
|
||||
|
|
@ -150,18 +150,35 @@ const PY_FUNCTION_NODE_TYPES = new Set([
|
|||
* generic_type → extractGenericTypeArgs → first arg
|
||||
* Falls back to text-based extraction.
|
||||
*/
|
||||
const extractPyElementTypeFromAnnotation = (typeNode: SyntaxNode): string | undefined => {
|
||||
const extractPyElementTypeFromAnnotation = (typeNode: SyntaxNode, pos: TypeArgPosition = 'last'): string | undefined => {
|
||||
// Unwrap 'type' wrapper node to get to the actual type (e.g., type > generic_type)
|
||||
const inner = typeNode.type === 'type' ? (typeNode.firstNamedChild ?? typeNode) : typeNode;
|
||||
|
||||
// Python subscript: List[User], Sequence[User] — use raw text
|
||||
if (typeNode.type === 'subscript') {
|
||||
return extractElementTypeFromString(typeNode.text);
|
||||
if (inner.type === 'subscript') {
|
||||
return extractElementTypeFromString(inner.text, pos);
|
||||
}
|
||||
// generic_type (less common in Python but present in some grammars)
|
||||
if (typeNode.type === 'generic_type') {
|
||||
const args = extractGenericTypeArgs(typeNode);
|
||||
if (args.length >= 1) return args[0];
|
||||
// generic_type: dict[str, User] — tree-sitter-python uses type_parameter child
|
||||
if (inner.type === 'generic_type') {
|
||||
// Try standard extractGenericTypeArgs first (handles type_arguments)
|
||||
const args = extractGenericTypeArgs(inner);
|
||||
if (args.length >= 1) return pos === 'first' ? args[0] : args[args.length - 1];
|
||||
// Fallback: look for type_parameter child (tree-sitter-python specific)
|
||||
for (let i = 0; i < inner.namedChildCount; i++) {
|
||||
const child = inner.namedChild(i);
|
||||
if (child?.type === 'type_parameter') {
|
||||
if (pos === 'first') {
|
||||
const firstArg = child.firstNamedChild;
|
||||
if (firstArg) return extractSimpleTypeName(firstArg);
|
||||
} else {
|
||||
const lastArg = child.lastNamedChild;
|
||||
if (lastArg) return extractSimpleTypeName(lastArg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fallback: raw text extraction (handles User[], [User], etc.)
|
||||
return extractElementTypeFromString(typeNode.text);
|
||||
return extractElementTypeFromString(inner.text, pos);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -173,7 +190,7 @@ const extractPyElementTypeFromAnnotation = (typeNode: SyntaxNode): string | unde
|
|||
* `typed_parameter` may not expose the name as a `name` field — falls back to
|
||||
* checking the first identifier-type named child.
|
||||
*/
|
||||
const findPyParamElementType = (iterableName: string, startNode: SyntaxNode): string | undefined => {
|
||||
const findPyParamElementType = (iterableName: string, startNode: SyntaxNode, pos: TypeArgPosition = 'last'): string | undefined => {
|
||||
let current: SyntaxNode | null = startNode.parent;
|
||||
while (current) {
|
||||
if (current.type === 'function_definition') {
|
||||
|
|
@ -191,7 +208,7 @@ const findPyParamElementType = (iterableName: string, startNode: SyntaxNode): st
|
|||
const typeAnnotation = param.childForFieldName('type')
|
||||
?? (param.namedChildCount >= 2 ? param.namedChild(param.namedChildCount - 1) : null);
|
||||
if (typeAnnotation && typeAnnotation !== nameNode) {
|
||||
return extractPyElementTypeFromAnnotation(typeAnnotation);
|
||||
return extractPyElementTypeFromAnnotation(typeAnnotation, pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -220,20 +237,46 @@ const extractForLoopBinding: ForLoopExtractor = (
|
|||
): void => {
|
||||
if (node.type !== 'for_statement') return;
|
||||
|
||||
// The iterable is the `right` field of the for_statement.
|
||||
// The iterable is the `right` field — may be identifier or call (data.items()/keys()/values()).
|
||||
const rightNode = node.childForFieldName('right');
|
||||
if (!rightNode || rightNode.type !== 'identifier') return;
|
||||
const iterableName = rightNode.text;
|
||||
let iterableName: string | undefined;
|
||||
let methodName: string | undefined;
|
||||
if (rightNode?.type === 'identifier') {
|
||||
iterableName = rightNode.text;
|
||||
} else if (rightNode?.type === 'call') {
|
||||
// data.items() → call > function: attribute > identifier('data') + identifier('items')
|
||||
const fn = rightNode.childForFieldName('function');
|
||||
if (fn?.type === 'attribute') {
|
||||
const obj = fn.firstNamedChild;
|
||||
if (obj?.type === 'identifier') iterableName = obj.text;
|
||||
// Extract method name: items, keys, values
|
||||
const method = fn.lastNamedChild;
|
||||
if (method?.type === 'identifier' && method !== obj) methodName = method.text;
|
||||
}
|
||||
}
|
||||
if (!iterableName) return;
|
||||
|
||||
const typeArgPos = methodToTypeArgPosition(methodName);
|
||||
const elementType = resolveIterableElementType(
|
||||
iterableName, node, scopeEnv, declarationTypeNodes, scope,
|
||||
extractPyElementTypeFromAnnotation, findPyParamElementType,
|
||||
typeArgPos,
|
||||
);
|
||||
if (!elementType) return;
|
||||
|
||||
// The loop variable is the `left` field — a plain identifier.
|
||||
// The loop variable is the `left` field — identifier or pattern_list.
|
||||
const leftNode = node.childForFieldName('left');
|
||||
if (!leftNode) return;
|
||||
|
||||
// Handle tuple unpacking: for key, value in data.items()
|
||||
if (leftNode.type === 'pattern_list') {
|
||||
const lastChild = leftNode.lastNamedChild;
|
||||
if (lastChild?.type === 'identifier') {
|
||||
scopeEnv.set(lastChild.text, elementType);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const loopVarName = extractVarName(leftNode);
|
||||
if (loopVarName) scopeEnv.set(loopVarName, elementType);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { SyntaxNode } from '../utils.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, PendingAssignmentExtractor, PatternBindingExtractor, ForLoopExtractor } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName, hasTypeAnnotation, unwrapAwait, extractGenericTypeArgs, resolveIterableElementType } from './shared.js';
|
||||
import { extractSimpleTypeName, extractVarName, hasTypeAnnotation, unwrapAwait, extractGenericTypeArgs, resolveIterableElementType, methodToTypeArgPosition, type TypeArgPosition } from './shared.js';
|
||||
|
||||
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
|
||||
'let_declaration',
|
||||
|
|
@ -291,16 +291,16 @@ const FOR_LOOP_NODE_TYPES: ReadonlySet<string> = new Set(['for_expression']);
|
|||
/** Extract element type from a Rust type annotation AST node.
|
||||
* Handles: generic_type (Vec<User>), reference_type (&[User]), array_type ([User; N]),
|
||||
* slice_type ([User]). For call-graph purposes, strips references (&User → User). */
|
||||
const extractRustElementTypeFromTypeNode = (typeNode: SyntaxNode): string | undefined => {
|
||||
// generic_type: Vec<User>, HashSet<User> — extract first type argument
|
||||
const extractRustElementTypeFromTypeNode = (typeNode: SyntaxNode, pos: TypeArgPosition = 'last'): string | undefined => {
|
||||
// generic_type: Vec<User>, HashMap<K, V> — extract type arg based on position
|
||||
if (typeNode.type === 'generic_type') {
|
||||
const args = extractGenericTypeArgs(typeNode);
|
||||
if (args.length >= 1) return args[0];
|
||||
if (args.length >= 1) return pos === 'first' ? args[0] : args[args.length - 1];
|
||||
}
|
||||
// reference_type: &[User] or &Vec<User> — unwrap the reference and recurse
|
||||
if (typeNode.type === 'reference_type') {
|
||||
const inner = typeNode.lastNamedChild;
|
||||
if (inner) return extractRustElementTypeFromTypeNode(inner);
|
||||
if (inner) return extractRustElementTypeFromTypeNode(inner, pos);
|
||||
}
|
||||
// array_type: [User; N] — element is the first child
|
||||
if (typeNode.type === 'array_type') {
|
||||
|
|
@ -317,7 +317,7 @@ const extractRustElementTypeFromTypeNode = (typeNode: SyntaxNode): string | unde
|
|||
|
||||
/** Walk up from a for-loop to the enclosing function_item and search parameters
|
||||
* for one named `iterableName`. Returns the element type from its annotation. */
|
||||
const findRustParamElementType = (iterableName: string, startNode: SyntaxNode): string | undefined => {
|
||||
const findRustParamElementType = (iterableName: string, startNode: SyntaxNode, pos: TypeArgPosition = 'last'): string | undefined => {
|
||||
let current: SyntaxNode | null = startNode.parent;
|
||||
while (current) {
|
||||
if (current.type === 'function_item') {
|
||||
|
|
@ -338,7 +338,7 @@ const findRustParamElementType = (iterableName: string, startNode: SyntaxNode):
|
|||
}
|
||||
if (identNode.text !== iterableName) continue;
|
||||
const typeNode = param.childForFieldName('type');
|
||||
if (typeNode) return extractRustElementTypeFromTypeNode(typeNode);
|
||||
if (typeNode) return extractRustElementTypeFromTypeNode(typeNode, pos);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
|
@ -362,19 +362,32 @@ const extractForLoopBinding: ForLoopExtractor = (
|
|||
const valueNode = node.childForFieldName('value');
|
||||
if (!patternNode || !valueNode) return;
|
||||
|
||||
// Extract iterable name — may be &users, &mut users, or plain users
|
||||
// Extract iterable name + method — may be &users, users, or users.iter()/keys()/values()
|
||||
let iterableName: string | undefined;
|
||||
let methodName: string | undefined;
|
||||
if (valueNode.type === 'reference_expression') {
|
||||
const inner = valueNode.lastNamedChild;
|
||||
if (inner?.type === 'identifier') iterableName = inner.text;
|
||||
} else if (valueNode.type === 'identifier') {
|
||||
iterableName = valueNode.text;
|
||||
} else if (valueNode.type === 'call_expression') {
|
||||
// users.iter() → call_expression > function: field_expression > identifier + field_identifier
|
||||
const fieldExpr = valueNode.childForFieldName('function');
|
||||
if (fieldExpr?.type === 'field_expression') {
|
||||
const obj = fieldExpr.firstNamedChild;
|
||||
if (obj?.type === 'identifier') iterableName = obj.text;
|
||||
// Extract method name: iter, keys, values, into_iter, etc.
|
||||
const field = fieldExpr.lastNamedChild;
|
||||
if (field?.type === 'field_identifier') methodName = field.text;
|
||||
}
|
||||
}
|
||||
if (!iterableName) return;
|
||||
|
||||
const typeArgPos = methodToTypeArgPosition(methodName);
|
||||
const elementType = resolveIterableElementType(
|
||||
iterableName, node, scopeEnv, declarationTypeNodes, scope,
|
||||
extractRustElementTypeFromTypeNode, findRustParamElementType,
|
||||
typeArgPos,
|
||||
);
|
||||
if (!elementType) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,20 @@
|
|||
import type { SyntaxNode } from '../utils.js';
|
||||
|
||||
/** Which type argument to extract from a multi-arg generic container.
|
||||
* - 'first': key type (e.g., K from Map<K,V>) — used for .keys(), .keySet()
|
||||
* - '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']);
|
||||
|
||||
/** 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';
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared 3-strategy fallback for resolving the element type of a container variable.
|
||||
* Used by all for-loop extractors to resolve the loop variable's type from the iterable.
|
||||
|
|
@ -11,6 +26,7 @@ import type { SyntaxNode } from '../utils.js';
|
|||
*
|
||||
* @param extractFromTypeNode Language-specific function to extract element type from AST node
|
||||
* @param findParamElementType Optional language-specific AST walk to find parameter type
|
||||
* @param typeArgPos Which generic type arg to extract: 'first' for keys, 'last' for values (default)
|
||||
*/
|
||||
export function resolveIterableElementType(
|
||||
iterableName: string,
|
||||
|
|
@ -18,23 +34,24 @@ export function resolveIterableElementType(
|
|||
scopeEnv: ReadonlyMap<string, string>,
|
||||
declarationTypeNodes: ReadonlyMap<string, SyntaxNode>,
|
||||
scope: string,
|
||||
extractFromTypeNode: (typeNode: SyntaxNode) => string | undefined,
|
||||
findParamElementType?: (name: string, startNode: SyntaxNode) => string | undefined,
|
||||
extractFromTypeNode: (typeNode: SyntaxNode, pos?: TypeArgPosition) => string | undefined,
|
||||
findParamElementType?: (name: string, startNode: SyntaxNode, pos?: TypeArgPosition) => string | undefined,
|
||||
typeArgPos: TypeArgPosition = 'last',
|
||||
): string | undefined {
|
||||
// Strategy 1: declarationTypeNodes AST node
|
||||
const typeNode = declarationTypeNodes.get(`${scope}\0${iterableName}`);
|
||||
if (typeNode) {
|
||||
const t = extractFromTypeNode(typeNode);
|
||||
const t = extractFromTypeNode(typeNode, typeArgPos);
|
||||
if (t) return t;
|
||||
}
|
||||
// Strategy 2: scopeEnv string → extractElementTypeFromString
|
||||
const iterableType = scopeEnv.get(iterableName);
|
||||
if (iterableType) {
|
||||
const el = extractElementTypeFromString(iterableType);
|
||||
const el = extractElementTypeFromString(iterableType, typeArgPos);
|
||||
if (el) return el;
|
||||
}
|
||||
// Strategy 3: AST walk to function parameters
|
||||
if (findParamElementType) return findParamElementType(iterableName, node);
|
||||
if (findParamElementType) return findParamElementType(iterableName, node, typeArgPos);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
|
@ -131,8 +148,9 @@ export const extractSimpleTypeName = (typeNode: SyntaxNode): string | undefined
|
|||
if (inner) return extractSimpleTypeName(inner);
|
||||
}
|
||||
|
||||
// PHP primitive_type (string, int, float, bool)
|
||||
if (typeNode.type === 'primitive_type') {
|
||||
// Primitive/predefined types: string, int, float, bool, number, unknown, any
|
||||
// PHP: primitive_type; TS/JS: predefined_type
|
||||
if (typeNode.type === 'primitive_type' || typeNode.type === 'predefined_type') {
|
||||
return typeNode.text;
|
||||
}
|
||||
|
||||
|
|
@ -383,11 +401,11 @@ function extractFirstArg(args: string): string {
|
|||
* - vector<User> → User (C++ container)
|
||||
* - Vec<User> → User (Rust container)
|
||||
*
|
||||
* For multi-argument generics (Map<K, V>), only the first type argument is
|
||||
* returned. Returns undefined when the extracted type is not a simple word
|
||||
* (e.g., nested generics as element types).
|
||||
* For multi-argument generics (Map<K, V>), returns the first or last type arg
|
||||
* based on `pos` ('first' for keys, 'last' for values — default 'last').
|
||||
* Returns undefined when the extracted type is not a simple word.
|
||||
*/
|
||||
export function extractElementTypeFromString(typeStr: string): string | undefined {
|
||||
export function extractElementTypeFromString(typeStr: string, pos: TypeArgPosition = 'last'): string | undefined {
|
||||
if (!typeStr || typeStr.length === 0 || typeStr.length > 2048) return undefined;
|
||||
|
||||
// 1. Array suffix: User[] → User
|
||||
|
|
@ -437,6 +455,7 @@ export function extractElementTypeFromString(typeStr: string): string | undefine
|
|||
// selected closeChar can match at depth 0 (prevents cross-bracket miscounting).
|
||||
let depth = 0;
|
||||
const start = openIdx + 1;
|
||||
let lastCommaIdx = -1; // Track last top-level comma for 'last' position
|
||||
for (let i = start; i < typeStr.length; i++) {
|
||||
const ch = typeStr[i];
|
||||
if (ch === '<' || ch === '[') {
|
||||
|
|
@ -445,15 +464,23 @@ export function extractElementTypeFromString(typeStr: string): string | undefine
|
|||
if (depth === 0) {
|
||||
// At depth 0 — only match if it is our selected close bracket.
|
||||
if (ch !== closeChar) return undefined; // mismatched bracket = malformed
|
||||
if (pos === 'last' && lastCommaIdx >= 0) {
|
||||
// Return last arg (text after last comma)
|
||||
const lastArg = typeStr.slice(lastCommaIdx + 1, i).trim();
|
||||
return lastArg && /^\w+$/.test(lastArg) ? lastArg : undefined;
|
||||
}
|
||||
const inner = typeStr.slice(start, i).trim();
|
||||
const firstArg = extractFirstArg(inner);
|
||||
return firstArg && /^\w+$/.test(firstArg) ? firstArg : undefined;
|
||||
}
|
||||
depth--;
|
||||
} else if (ch === ',' && depth === 0) {
|
||||
// Top-level comma before the matching close bracket — take the text before it.
|
||||
const arg = typeStr.slice(start, i).trim();
|
||||
return arg && /^\w+$/.test(arg) ? arg : undefined;
|
||||
if (pos === 'first') {
|
||||
// Return first arg (text before first comma)
|
||||
const arg = typeStr.slice(start, i).trim();
|
||||
return arg && /^\w+$/.test(arg) ? arg : undefined;
|
||||
}
|
||||
lastCommaIdx = i;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { SyntaxNode } from '../utils.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, ReturnTypeExtractor, PendingAssignmentExtractor, ForLoopExtractor } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName, hasTypeAnnotation, unwrapAwait, extractCalleeName, extractElementTypeFromString, extractGenericTypeArgs, resolveIterableElementType } from './shared.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, ReturnTypeExtractor, PendingAssignmentExtractor, ForLoopExtractor, PatternBindingExtractor } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName, hasTypeAnnotation, unwrapAwait, extractCalleeName, extractElementTypeFromString, extractGenericTypeArgs, resolveIterableElementType, methodToTypeArgPosition, type TypeArgPosition } from './shared.js';
|
||||
|
||||
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
|
||||
'lexical_declaration',
|
||||
|
|
@ -208,7 +208,7 @@ const TS_FUNCTION_NODE_TYPES = new Set([
|
|||
* type_annotation ": Array<User>" → generic_type → extractGenericTypeArgs → "User"
|
||||
* Falls back to text-based extraction via extractElementTypeFromString.
|
||||
*/
|
||||
const extractTsElementTypeFromAnnotation = (typeAnnotation: SyntaxNode): string | undefined => {
|
||||
const extractTsElementTypeFromAnnotation = (typeAnnotation: SyntaxNode, pos: TypeArgPosition = 'last'): string | undefined => {
|
||||
// Unwrap type_annotation (the node text includes ': ' prefix)
|
||||
const inner = typeAnnotation.type === 'type_annotation'
|
||||
? (typeAnnotation.firstNamedChild ?? typeAnnotation)
|
||||
|
|
@ -217,7 +217,7 @@ const extractTsElementTypeFromAnnotation = (typeAnnotation: SyntaxNode): string
|
|||
// readonly User[] — readonly_type wraps array_type: unwrap and recurse
|
||||
if (inner.type === 'readonly_type') {
|
||||
const wrapped = inner.firstNamedChild;
|
||||
if (wrapped) return extractTsElementTypeFromAnnotation(wrapped);
|
||||
if (wrapped) return extractTsElementTypeFromAnnotation(wrapped, pos);
|
||||
}
|
||||
|
||||
// User[] — array_type: first named child is the element type
|
||||
|
|
@ -226,15 +226,16 @@ const extractTsElementTypeFromAnnotation = (typeAnnotation: SyntaxNode): string
|
|||
if (elem) return extractSimpleTypeName(elem);
|
||||
}
|
||||
|
||||
// Array<User>, ReadonlyArray<User> — generic_type
|
||||
// Array<User>, Map<string, User> — generic_type
|
||||
// pos determines which type arg: 'first' for keys, 'last' for values
|
||||
if (inner.type === 'generic_type') {
|
||||
const args = extractGenericTypeArgs(inner);
|
||||
if (args.length >= 1) return args[0];
|
||||
if (args.length >= 1) return pos === 'first' ? args[0] : args[args.length - 1];
|
||||
}
|
||||
|
||||
// Fallback: strip ': ' prefix from type_annotation text and use string extraction
|
||||
const rawText = inner.text;
|
||||
return extractElementTypeFromString(rawText);
|
||||
return extractElementTypeFromString(rawText, pos);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -246,6 +247,7 @@ const findTsLocalDeclElementType = (
|
|||
iterableName: string,
|
||||
blockNode: SyntaxNode,
|
||||
beforeNode: SyntaxNode,
|
||||
pos: TypeArgPosition = 'last',
|
||||
): string | undefined => {
|
||||
for (let i = 0; i < blockNode.namedChildCount; i++) {
|
||||
const stmt = blockNode.namedChild(i);
|
||||
|
|
@ -260,7 +262,7 @@ const findTsLocalDeclElementType = (
|
|||
const nameNode = decl.childForFieldName('name');
|
||||
if (nameNode?.text !== iterableName) continue;
|
||||
const typeAnnotation = decl.childForFieldName('type');
|
||||
if (typeAnnotation) return extractTsElementTypeFromAnnotation(typeAnnotation);
|
||||
if (typeAnnotation) return extractTsElementTypeFromAnnotation(typeAnnotation, pos);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
|
|
@ -272,7 +274,7 @@ const findTsLocalDeclElementType = (
|
|||
* for a variable named `iterableName` with a container type annotation.
|
||||
* Returns the element type extracted from the annotation, or undefined.
|
||||
*/
|
||||
const findTsIterableElementType = (iterableName: string, startNode: SyntaxNode): string | undefined => {
|
||||
const findTsIterableElementType = (iterableName: string, startNode: SyntaxNode, pos: TypeArgPosition = 'last'): string | undefined => {
|
||||
let current: SyntaxNode | null = startNode.parent;
|
||||
// Capture the immediate statement_block parent to search local declarations
|
||||
const blockNode = current?.type === 'statement_block' ? current : null;
|
||||
|
|
@ -289,13 +291,13 @@ const findTsIterableElementType = (iterableName: string, startNode: SyntaxNode):
|
|||
const patternNode = param.childForFieldName('pattern') ?? param.childForFieldName('name');
|
||||
if (patternNode?.text === iterableName) {
|
||||
const typeAnnotation = param.childForFieldName('type');
|
||||
if (typeAnnotation) return extractTsElementTypeFromAnnotation(typeAnnotation);
|
||||
if (typeAnnotation) return extractTsElementTypeFromAnnotation(typeAnnotation, pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Search local declarations in the function body (statement_block)
|
||||
if (blockNode) {
|
||||
const result = findTsLocalDeclElementType(iterableName, blockNode, startNode);
|
||||
const result = findTsLocalDeclElementType(iterableName, blockNode, startNode, pos);
|
||||
if (result) return result;
|
||||
}
|
||||
break; // stop at the nearest function boundary
|
||||
|
|
@ -336,21 +338,47 @@ const extractForLoopBinding: ForLoopExtractor = (
|
|||
}
|
||||
if (!isForOf) return;
|
||||
|
||||
// The iterable is the `right` field of the for_in_statement.
|
||||
// The iterable is the `right` field — may be identifier or call_expression.
|
||||
const rightNode = node.childForFieldName('right');
|
||||
if (!rightNode || rightNode.type !== 'identifier') return;
|
||||
const iterableName = rightNode.text;
|
||||
let iterableName: string | undefined;
|
||||
let methodName: string | undefined;
|
||||
if (rightNode?.type === 'identifier') {
|
||||
iterableName = rightNode.text;
|
||||
} else if (rightNode?.type === 'call_expression') {
|
||||
// entries.values() → call_expression > function: member_expression > object + property
|
||||
const fn = rightNode.childForFieldName('function');
|
||||
if (fn?.type === 'member_expression') {
|
||||
const obj = fn.childForFieldName('object');
|
||||
const prop = fn.childForFieldName('property');
|
||||
if (obj?.type === 'identifier') iterableName = obj.text;
|
||||
if (prop?.type === 'property_identifier') methodName = prop.text;
|
||||
}
|
||||
}
|
||||
if (!iterableName) return;
|
||||
|
||||
const typeArgPos = methodToTypeArgPosition(methodName);
|
||||
const elementType = resolveIterableElementType(
|
||||
iterableName, node, scopeEnv, declarationTypeNodes, scope,
|
||||
extractTsElementTypeFromAnnotation, findTsIterableElementType,
|
||||
typeArgPos,
|
||||
);
|
||||
if (!elementType) return;
|
||||
|
||||
// The loop variable is the `left` field. It may be wrapped in a variable_declarator.
|
||||
// The loop variable is the `left` field.
|
||||
const leftNode = node.childForFieldName('left');
|
||||
if (!leftNode) return;
|
||||
|
||||
// Handle destructured for-of: for (const [k, v] of entries)
|
||||
// AST: left = array_pattern directly (no variable_declarator wrapper)
|
||||
// Bind the LAST identifier to the element type (value in [key, value] patterns)
|
||||
if (leftNode.type === 'array_pattern') {
|
||||
const lastChild = leftNode.lastNamedChild;
|
||||
if (lastChild?.type === 'identifier') {
|
||||
scopeEnv.set(lastChild.text, elementType);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let loopVarNode: SyntaxNode | null = leftNode;
|
||||
// `const user` parses as: left → variable_declarator containing an identifier named `user`
|
||||
if (loopVarNode.type === 'variable_declarator') {
|
||||
|
|
@ -377,9 +405,25 @@ const extractPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) =>
|
|||
return undefined;
|
||||
};
|
||||
|
||||
/** TS instanceof narrowing: `x instanceof User` → bind x to User.
|
||||
* Only works when x has no prior type binding (e.g. x: unknown, untyped params).
|
||||
* Typed params (x: Animal) are blocked by the !scopeEnv.has() guard in buildTypeEnv.
|
||||
* Uses first-writer-wins, same as Rust match arm bindings. */
|
||||
const extractPatternBinding: PatternBindingExtractor = (node) => {
|
||||
if (node.type !== 'binary_expression') return undefined;
|
||||
const op = node.children.find(c => !c.isNamed && c.text === 'instanceof');
|
||||
if (!op) return undefined;
|
||||
// binary_expression children are positional — no left/right fields
|
||||
const left = node.namedChild(0);
|
||||
const right = node.namedChild(1);
|
||||
if (left?.type !== 'identifier' || right?.type !== 'identifier') return undefined;
|
||||
return { varName: left.text, typeName: right.text };
|
||||
};
|
||||
|
||||
export const typeConfig: LanguageTypeConfig = {
|
||||
declarationNodeTypes: DECLARATION_NODE_TYPES,
|
||||
forLoopNodeTypes: FOR_LOOP_NODE_TYPES,
|
||||
patternBindingNodeTypes: new Set(['binary_expression']),
|
||||
extractDeclaration,
|
||||
extractParameter,
|
||||
extractInitializer,
|
||||
|
|
@ -387,4 +431,5 @@ export const typeConfig: LanguageTypeConfig = {
|
|||
extractReturnType,
|
||||
extractForLoopBinding,
|
||||
extractPendingAssignment,
|
||||
extractPatternBinding,
|
||||
};
|
||||
|
|
|
|||
5
gitnexus/test/fixtures/lang-resolution/python-dict-items-loop/app.py
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/python-dict-items-loop/app.py
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from user import User
|
||||
|
||||
def process(data: dict[str, User]):
|
||||
for key, user in data.items():
|
||||
user.save()
|
||||
6
gitnexus/test/fixtures/lang-resolution/python-dict-items-loop/repo.py
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/python-dict-items-loop/repo.py
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
class Repo:
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
|
||||
def save(self):
|
||||
pass
|
||||
6
gitnexus/test/fixtures/lang-resolution/python-dict-items-loop/user.py
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/python-dict-items-loop/user.py
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
class User:
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
|
||||
def save(self):
|
||||
pass
|
||||
18
gitnexus/test/fixtures/lang-resolution/rust-iter-for-loop/src/main.rs
vendored
Normal file
18
gitnexus/test/fixtures/lang-resolution/rust-iter-for-loop/src/main.rs
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
mod user;
|
||||
mod repo;
|
||||
use crate::user::User;
|
||||
use crate::repo::Repo;
|
||||
|
||||
fn process_users(users: Vec<User>) {
|
||||
for user in users.iter() {
|
||||
user.save();
|
||||
}
|
||||
}
|
||||
|
||||
fn process_repos(repos: Vec<Repo>) {
|
||||
for repo in repos.into_iter() {
|
||||
repo.save();
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
7
gitnexus/test/fixtures/lang-resolution/rust-iter-for-loop/src/repo.rs
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/rust-iter-for-loop/src/repo.rs
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
pub struct Repo {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl Repo {
|
||||
pub fn save(&self) {}
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/rust-iter-for-loop/src/user.rs
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/rust-iter-for-loop/src/user.rs
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
pub struct User {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub fn save(&self) {}
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/typescript-destructured-for-of/src/app.ts
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/typescript-destructured-for-of/src/app.ts
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { User } from './user';
|
||||
|
||||
function processEntries(entries: Map<string, User>) {
|
||||
for (const [key, user] of entries) {
|
||||
user.save();
|
||||
}
|
||||
}
|
||||
4
gitnexus/test/fixtures/lang-resolution/typescript-destructured-for-of/src/repo.ts
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/typescript-destructured-for-of/src/repo.ts
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export class Repo {
|
||||
constructor(public name: string) {}
|
||||
save() {}
|
||||
}
|
||||
4
gitnexus/test/fixtures/lang-resolution/typescript-destructured-for-of/src/user.ts
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/typescript-destructured-for-of/src/user.ts
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export class User {
|
||||
constructor(public name: string) {}
|
||||
save() {}
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/typescript-instanceof-narrowing/src/app.ts
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/typescript-instanceof-narrowing/src/app.ts
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { User } from './user';
|
||||
|
||||
function process(x) {
|
||||
if (x instanceof User) {
|
||||
x.save();
|
||||
}
|
||||
}
|
||||
4
gitnexus/test/fixtures/lang-resolution/typescript-instanceof-narrowing/src/repo.ts
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/typescript-instanceof-narrowing/src/repo.ts
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export class Repo {
|
||||
constructor(public name: string) {}
|
||||
save() {}
|
||||
}
|
||||
4
gitnexus/test/fixtures/lang-resolution/typescript-instanceof-narrowing/src/user.ts
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/typescript-instanceof-narrowing/src/user.ts
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export class User {
|
||||
constructor(public name: string) {}
|
||||
save() {}
|
||||
}
|
||||
|
|
@ -1056,3 +1056,38 @@ describe('Python chained method call resolution', () => {
|
|||
expect(repoSave).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// for key, user in data.items() — dict.items() call iterable + tuple unpacking
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Python dict.items() for-loop resolution', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'python-dict-items-loop'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects User class with save method', () => {
|
||||
expect(getNodesByLabel(result, 'Class')).toContain('User');
|
||||
});
|
||||
|
||||
it('resolves user.save() via dict.items() loop to User#save', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const userSave = calls.find(c =>
|
||||
c.target === 'save' && c.source === 'process' && c.targetFilePath?.includes('user.py'),
|
||||
);
|
||||
expect(userSave).toBeDefined();
|
||||
});
|
||||
|
||||
it('does NOT resolve user.save() to Repo#save (negative)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const wrongSave = calls.find(c =>
|
||||
c.target === 'save' && c.source === 'process' && c.targetFilePath?.includes('repo.py'),
|
||||
);
|
||||
expect(wrongSave).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1177,3 +1177,49 @@ describe('Rust match arm type resolution', () => {
|
|||
expect(wrongSave).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// for user in users.iter() — call_expression iterable resolution
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Rust .iter() for-loop call_expression resolution', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'rust-iter-for-loop'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects User and Repo structs with save functions', () => {
|
||||
expect(getNodesByLabel(result, 'Struct')).toContain('User');
|
||||
expect(getNodesByLabel(result, 'Struct')).toContain('Repo');
|
||||
const saveFns = getNodesByLabel(result, 'Function').filter(f => f === 'save');
|
||||
expect(saveFns.length).toBe(2);
|
||||
});
|
||||
|
||||
it('resolves user.save() via users.iter() to User#save', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const userSave = calls.find(c =>
|
||||
c.target === 'save' && c.source === 'process_users' && c.targetFilePath?.includes('user.rs'),
|
||||
);
|
||||
expect(userSave).toBeDefined();
|
||||
});
|
||||
|
||||
it('resolves repo.save() via repos.into_iter() to Repo#save', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const repoSave = calls.find(c =>
|
||||
c.target === 'save' && c.source === 'process_repos' && c.targetFilePath?.includes('repo.rs'),
|
||||
);
|
||||
expect(repoSave).toBeDefined();
|
||||
});
|
||||
|
||||
it('does NOT cross-resolve user.save() to Repo#save (negative)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const wrongSave = calls.find(c =>
|
||||
c.target === 'save' && c.source === 'process_users' && c.targetFilePath?.includes('repo.rs'),
|
||||
);
|
||||
expect(wrongSave).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1471,3 +1471,73 @@ describe('TypeScript readonly array for-loop resolution (Tier 1c)', () => {
|
|||
expect(wrong).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// for (const [key, user] of entries) — destructured for-of resolution
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('TS destructured for-of Map resolution', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'typescript-destructured-for-of'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects User class with save method', () => {
|
||||
expect(getNodesByLabel(result, 'Class')).toContain('User');
|
||||
});
|
||||
|
||||
it('resolves user.save() in destructured for-of to User#save', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const userSave = calls.find(c =>
|
||||
c.target === 'save' && c.source === 'processEntries' && c.targetFilePath?.includes('user'),
|
||||
);
|
||||
expect(userSave).toBeDefined();
|
||||
});
|
||||
|
||||
it('does NOT resolve user.save() to Repo#save (negative)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const wrongSave = calls.find(c =>
|
||||
c.target === 'save' && c.source === 'processEntries' && c.targetFilePath?.includes('repo'),
|
||||
);
|
||||
expect(wrongSave).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// if (x instanceof User) { x.save() } — instanceof narrowing resolution
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('TS instanceof narrowing resolution', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'typescript-instanceof-narrowing'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects User class with save method', () => {
|
||||
expect(getNodesByLabel(result, 'Class')).toContain('User');
|
||||
});
|
||||
|
||||
it('resolves x.save() after instanceof to User#save', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const userSave = calls.find(c =>
|
||||
c.target === 'save' && c.source === 'process' && c.targetFilePath?.includes('user'),
|
||||
);
|
||||
expect(userSave).toBeDefined();
|
||||
});
|
||||
|
||||
it('does NOT resolve x.save() to Repo#save (negative)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const wrongSave = calls.find(c =>
|
||||
c.target === 'save' && c.source === 'process' && c.targetFilePath?.includes('repo'),
|
||||
);
|
||||
expect(wrongSave).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2928,28 +2928,25 @@ class Foo {
|
|||
});
|
||||
});
|
||||
|
||||
// These tests document capabilities we intentionally do not support yet.
|
||||
// Each skip test has assertions that should pass once the feature is implemented.
|
||||
// When a skip test starts passing, remove .skip and update the corresponding issue.
|
||||
describe('known limitations (documented skip tests)', () => {
|
||||
it.skip('TS destructured for-of: for (const [k, v] of entries) — requires tuple destructuring', () => {
|
||||
// extractVarName returns undefined for array_pattern; also needs
|
||||
// resolveIterableElementType to return tuple types and ForLoopExtractor
|
||||
// type signature to support multi-variable binding
|
||||
describe('previously-skipped limitations (now resolved)', () => {
|
||||
it('TS destructured for-of: for (const [k, v] of entries) — last-child heuristic', () => {
|
||||
// array_pattern handled by binding last named child to element type.
|
||||
// Map<string, User> resolves to 'User' via last generic type arg.
|
||||
const tree = parse(`
|
||||
function process(entries: Map<string, User>) {
|
||||
for (const [key, user] of entries) {
|
||||
user.save();
|
||||
}
|
||||
}
|
||||
`, TypeScript);
|
||||
`, TypeScript.typescript);
|
||||
const { env } = buildTypeEnv(tree, 'typescript');
|
||||
expect(flatGet(env, 'user')).toBe('User');
|
||||
});
|
||||
|
||||
it.skip('Python tuple unpacking: for key, value in dict.items() — requires pattern_list + method call iterable', () => {
|
||||
// Left side is pattern_list, not identifier (needs Group A: tuple destructuring).
|
||||
// Right side is call_expression .items() (needs Group C: method call iterable resolution).
|
||||
it('Python tuple unpacking: for key, value in dict.items() — call iterable + pattern_list', () => {
|
||||
// call iterable: data.items() → extract receiver 'data' for type lookup.
|
||||
// pattern_list: bind last named child to element type.
|
||||
// dict[str, User] resolves to 'User' via last generic type arg.
|
||||
const tree = parse(`
|
||||
def process(data: dict[str, User]):
|
||||
for key, user in data.items():
|
||||
|
|
@ -2959,27 +2956,24 @@ def process(data: dict[str, User]):
|
|||
expect(flatGet(env, 'user')).toBe('User');
|
||||
});
|
||||
|
||||
it.skip('TS instanceof narrowing: if (x instanceof User) { x.save() } — needs block-level scoping', () => {
|
||||
// Narrows existing variable within a block, does not introduce a new one.
|
||||
// Requires scope stack infrastructure (push/pop on block entry/exit) and
|
||||
// type guard recognition. TS parses instanceof as binary_expression, not
|
||||
// a dedicated node type.
|
||||
it('TS instanceof narrowing: if (x instanceof User) — first-writer-wins, not block-scoped', () => {
|
||||
// Binds x to User via extractPatternBinding on binary_expression.
|
||||
// Only works when x has no prior type binding in scopeEnv.
|
||||
// True block-level scoping (overwriting existing bindings) is Phase 5.
|
||||
const tree = parse(`
|
||||
function process(x: unknown) {
|
||||
function process(x) {
|
||||
if (x instanceof User) {
|
||||
x.save();
|
||||
}
|
||||
}
|
||||
`, TypeScript);
|
||||
`, TypeScript.typescript);
|
||||
const { env } = buildTypeEnv(tree, 'typescript');
|
||||
// x should be narrowed to User inside the if-block
|
||||
expect(flatGet(env, 'x')).toBe('User');
|
||||
});
|
||||
|
||||
it.skip('Rust for with .iter(): for user in users.iter() — needs method call iterable resolution', () => {
|
||||
// Iterable is a call_expression, not an identifier. In idiomatic Rust,
|
||||
// .iter()/.into_iter()/.iter_mut() is the dominant iteration pattern —
|
||||
// the plain identifier form (which IS supported) is less common.
|
||||
it('Rust for with .iter(): for user in users.iter() — call_expression iterable', () => {
|
||||
// Extracts receiver from call_expression > field_expression > identifier.
|
||||
// .iter()/.into_iter()/.iter_mut() is the dominant Rust iteration pattern.
|
||||
const tree = parse(`
|
||||
fn process(users: Vec<User>) {
|
||||
for user in users.iter() {
|
||||
|
|
@ -2990,12 +2984,83 @@ fn process(users: Vec<User>) {
|
|||
const { env } = buildTypeEnv(tree, 'rust');
|
||||
expect(flatGet(env, 'user')).toBe('User');
|
||||
});
|
||||
});
|
||||
|
||||
describe('method-aware type arg selection (.keys() vs .values())', () => {
|
||||
it('TS for-of map.values() resolves to value type (User)', () => {
|
||||
const tree = parse(`
|
||||
function process(data: Map<string, User>) {
|
||||
for (const user of data.values()) {
|
||||
user.save();
|
||||
}
|
||||
}
|
||||
`, TypeScript.typescript);
|
||||
const { env } = buildTypeEnv(tree, 'typescript');
|
||||
expect(flatGet(env, 'user')).toBe('User');
|
||||
});
|
||||
|
||||
it('TS for-of map.keys() resolves to key type (string)', () => {
|
||||
const tree = parse(`
|
||||
function process(data: Map<string, User>) {
|
||||
for (const key of data.keys()) {
|
||||
key.trim();
|
||||
}
|
||||
}
|
||||
`, TypeScript.typescript);
|
||||
const { env } = buildTypeEnv(tree, 'typescript');
|
||||
expect(flatGet(env, 'key')).toBe('string');
|
||||
});
|
||||
|
||||
it('Python for key in data.keys() resolves to key type (str)', () => {
|
||||
const tree = parse(`
|
||||
def process(data: dict[str, User]):
|
||||
for key in data.keys():
|
||||
key.strip()
|
||||
`, Python);
|
||||
const { env } = buildTypeEnv(tree, 'python');
|
||||
expect(flatGet(env, 'key')).toBe('str');
|
||||
});
|
||||
|
||||
it('Python for user in data.values() resolves to value type (User)', () => {
|
||||
const tree = parse(`
|
||||
def process(data: dict[str, User]):
|
||||
for user in data.values():
|
||||
user.save()
|
||||
`, Python);
|
||||
const { env } = buildTypeEnv(tree, 'python');
|
||||
expect(flatGet(env, 'user')).toBe('User');
|
||||
});
|
||||
|
||||
it('Rust for key in map.keys() resolves to key type (String)', () => {
|
||||
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('Rust for user in map.values() resolves to value type (User)', () => {
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
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.
|
||||
// Requires closure parameter inference — a different feature category
|
||||
// applicable to Ruby, Swift closures, Kotlin lambdas, and Java lambdas.
|
||||
// Depends on method call resolution (Group C) as prerequisite.
|
||||
const tree = parse(`
|
||||
def process(users)
|
||||
users.each { |user| user.save }
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue