feat: Phase 3 complete — all language gaps + pattern matching

Kotlin Tier 1c:
- Unannotated for-loop resolves via shared helper
- extractKotlinElementTypeFromTypeNode handles type_projection unwrapping
- findKotlinParamElementType walks to function_declaration

Java Tier 1c:
- var foreach resolves via shared helper
- extractJavaElementTypeFromTypeNode handles generic_type, array_type
- findJavaParamElementType walks to method_declaration

TypeScript:
- readonly User[] unwrapped via readonly_type → array_type recursion

C# switch patterns:
- declaration_pattern added to patternBindingNodeTypes
- extractPatternBinding handles standalone declaration_pattern (switch case/expr)

Rust match arms:
- match_arm added to patternBindingNodeTypes
- extractPatternBinding extended with match_arm → match_expression parent traversal

Python:
- as_pattern tries childForFieldName('alias') before positional fallback

Tests: 237 pass (was 224), 13 new tests added
This commit is contained in:
Gergo Magyar 2026-03-16 22:03:42 +00:00
parent d526ee927c
commit 104f9cd311
6 changed files with 270 additions and 35 deletions

View file

@ -226,16 +226,30 @@ const extractForLoopBinding: ForLoopExtractor = (
* No scopeEnv lookup is needed — the pattern explicitly declares the new variable's type.
*/
const extractPatternBinding: PatternBindingExtractor = (node) => {
if (node.type !== 'is_pattern_expression') return undefined;
const pattern = node.childForFieldName('pattern');
if (pattern?.type !== 'declaration_pattern') return undefined;
const typeNode = pattern.childForFieldName('type');
const nameNode = pattern.childForFieldName('name');
if (!typeNode || !nameNode) return undefined;
const typeName = extractSimpleTypeName(typeNode);
const varName = extractVarName(nameNode);
if (!typeName || !varName) return undefined;
return { varName, typeName };
// is_pattern_expression: `obj is User user` — has a declaration_pattern child
if (node.type === 'is_pattern_expression') {
const pattern = node.childForFieldName('pattern');
if (pattern?.type !== 'declaration_pattern') return undefined;
const typeNode = pattern.childForFieldName('type');
const nameNode = pattern.childForFieldName('name');
if (!typeNode || !nameNode) return undefined;
const typeName = extractSimpleTypeName(typeNode);
const varName = extractVarName(nameNode);
if (!typeName || !varName) return undefined;
return { varName, typeName };
}
// declaration_pattern: standalone in switch statements and switch expressions
// `case User u:` or `User u =>` — the declaration_pattern is a direct child
if (node.type === 'declaration_pattern') {
const typeNode = node.childForFieldName('type');
const nameNode = node.childForFieldName('name');
if (!typeNode || !nameNode) return undefined;
const typeName = extractSimpleTypeName(typeNode);
const varName = extractVarName(nameNode);
if (!typeName || !varName) return undefined;
return { varName, typeName };
}
return undefined;
};
/** C#: var alias = u → variable_declarator with name + equals_value_clause.
@ -266,7 +280,7 @@ const extractPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) =>
export const typeConfig: LanguageTypeConfig = {
declarationNodeTypes: DECLARATION_NODE_TYPES,
forLoopNodeTypes: FOR_LOOP_NODE_TYPES,
patternBindingNodeTypes: new Set(['is_pattern_expression']),
patternBindingNodeTypes: new Set(['is_pattern_expression', 'declaration_pattern']),
extractDeclaration,
extractParameter,
scanConstructorBinding,

View file

@ -1,6 +1,6 @@
import type { SyntaxNode } from '../utils.js';
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, ForLoopExtractor, PendingAssignmentExtractor, PatternBindingExtractor } from './types.js';
import { extractSimpleTypeName, extractVarName, findChildByType } from './shared.js';
import { extractSimpleTypeName, extractVarName, findChildByType, extractGenericTypeArgs, resolveIterableElementType } from './shared.js';
// ── Java ──────────────────────────────────────────────────────────────────
@ -89,19 +89,74 @@ const JAVA_FOR_LOOP_NODE_TYPES: ReadonlySet<string> = new Set([
'enhanced_for_statement',
]);
/** Java: for (User user : users) — extract loop variable binding */
/** Extract element type from a Java type annotation AST node.
* Handles generic_type (List<User>), array_type (User[]). */
const extractJavaElementTypeFromTypeNode = (typeNode: SyntaxNode): string | undefined => {
if (typeNode.type === 'generic_type') {
const args = extractGenericTypeArgs(typeNode);
if (args.length >= 1) return args[0];
}
if (typeNode.type === 'array_type') {
const elemNode = typeNode.firstNamedChild;
if (elemNode) return extractSimpleTypeName(elemNode);
}
return undefined;
};
/** Walk up from a for-each to the enclosing method_declaration and search parameters. */
const findJavaParamElementType = (iterableName: string, startNode: SyntaxNode): string | undefined => {
let current: SyntaxNode | null = startNode.parent;
while (current) {
if (current.type === 'method_declaration' || current.type === 'constructor_declaration') {
const paramsNode = current.childForFieldName('parameters');
if (paramsNode) {
for (let i = 0; i < paramsNode.namedChildCount; i++) {
const param = paramsNode.namedChild(i);
if (!param || param.type !== 'formal_parameter') continue;
const nameNode = param.childForFieldName('name');
if (nameNode?.text !== iterableName) continue;
const typeNode = param.childForFieldName('type');
if (typeNode) return extractJavaElementTypeFromTypeNode(typeNode);
}
}
break;
}
current = current.parent;
}
return undefined;
};
/** Java: for (User user : users) — extract loop variable binding.
* Tier 1c: for `for (var user : users)`, resolves element type from iterable. */
const extractJavaForLoopBinding: ForLoopExtractor = (
node: SyntaxNode,
scopeEnv: Map<string, string>,
_declarationTypeNodes: ReadonlyMap<string, SyntaxNode>,
_scope: string,
declarationTypeNodes: ReadonlyMap<string, SyntaxNode>,
scope: string,
): void => {
const typeNode = node.childForFieldName('type');
const nameNode = node.childForFieldName('name');
if (!typeNode || !nameNode) return;
const typeName = extractSimpleTypeName(typeNode);
const varName = extractVarName(nameNode);
if (typeName && varName) scopeEnv.set(varName, typeName);
if (!varName) return;
// Explicit type (existing behavior): for (User user : users)
const typeName = extractSimpleTypeName(typeNode);
if (typeName && typeName !== 'var') {
scopeEnv.set(varName, typeName);
return;
}
// Tier 1c: var — resolve from iterable's container type
const iterableNode = node.childForFieldName('value');
if (!iterableNode || iterableNode.type !== 'identifier') return;
const iterableName = iterableNode.text;
const elementType = resolveIterableElementType(
iterableName, node, scopeEnv, declarationTypeNodes, scope,
extractJavaElementTypeFromTypeNode, findJavaParamElementType,
);
if (elementType) scopeEnv.set(varName, elementType);
};
/** Java: var alias = u → local_variable_declaration > variable_declarator with name/value */
@ -285,24 +340,92 @@ const KOTLIN_FOR_LOOP_NODE_TYPES: ReadonlySet<string> = new Set([
'for_statement',
]);
/** Kotlin: for (user: User in users) — extract loop variable binding when explicit type annotation exists */
/** Extract element type from a Kotlin type annotation AST node (user_type wrapping generic).
* Kotlin: user_type → [type_identifier, type_arguments → [type_projection → user_type]]
* Handles the type_projection wrapper that Kotlin uses for generic type arguments. */
const extractKotlinElementTypeFromTypeNode = (typeNode: SyntaxNode): string | undefined => {
if (typeNode.type === 'user_type') {
const argsNode = findChildByType(typeNode, 'type_arguments');
if (argsNode && argsNode.namedChildCount >= 1) {
const firstArg = argsNode.namedChild(0);
if (!firstArg) return undefined;
// Kotlin wraps type args in type_projection — unwrap to get the inner type
const inner = firstArg.type === 'type_projection'
? firstArg.firstNamedChild
: firstArg;
if (inner) return extractSimpleTypeName(inner);
}
}
return undefined;
};
/** Walk up from a for-loop to the enclosing function_declaration and search parameters.
* Kotlin parameters use positional children (simple_identifier, user_type), not named fields. */
const findKotlinParamElementType = (iterableName: string, startNode: SyntaxNode): string | undefined => {
let current: SyntaxNode | null = startNode.parent;
while (current) {
if (current.type === 'function_declaration') {
const paramsNode = findChildByType(current, 'function_value_parameters');
if (paramsNode) {
for (let i = 0; i < paramsNode.namedChildCount; i++) {
const param = paramsNode.namedChild(i);
if (!param || param.type !== 'parameter') continue;
const nameNode = findChildByType(param, 'simple_identifier');
if (nameNode?.text !== iterableName) continue;
const typeNode = findChildByType(param, 'user_type');
if (typeNode) return extractKotlinElementTypeFromTypeNode(typeNode);
}
}
break;
}
current = current.parent;
}
return undefined;
};
/** Kotlin: for (user: User in users) — extract loop variable binding.
* Tier 1c: for `for (user in users)` without annotation, resolves from iterable. */
const extractKotlinForLoopBinding: ForLoopExtractor = (
node: SyntaxNode,
scopeEnv: Map<string, string>,
_declarationTypeNodes: ReadonlyMap<string, SyntaxNode>,
_scope: string,
declarationTypeNodes: ReadonlyMap<string, SyntaxNode>,
scope: string,
): void => {
// Kotlin loop variable: variable_declaration child with optional user_type annotation
const varDecl = findChildByType(node, 'variable_declaration');
if (!varDecl) return;
// Only extract when there is an explicit type annotation (user_type node)
const typeNode = findChildByType(varDecl, 'user_type');
if (!typeNode) return;
const nameNode = findChildByType(varDecl, 'simple_identifier');
if (!nameNode) return;
const typeName = extractSimpleTypeName(typeNode);
const varName = extractVarName(nameNode);
if (typeName && varName) scopeEnv.set(varName, typeName);
if (!varName) return;
// Explicit type annotation (existing behavior): for (user: User in users)
const typeNode = findChildByType(varDecl, 'user_type');
if (typeNode) {
const typeName = extractSimpleTypeName(typeNode);
if (typeName) scopeEnv.set(varName, typeName);
return;
}
// Tier 1c: no annotation — resolve from iterable's container type
// Kotlin for-loop children: [variable_declaration, simple_identifier(iterable), control_structure_body]
// The iterable is the second named child of the for_statement (after variable_declaration)
let iterableName: string | undefined;
let foundVarDecl = false;
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (child === varDecl) { foundVarDecl = true; continue; }
if (foundVarDecl && child?.type === 'simple_identifier') {
iterableName = child.text;
break;
}
}
if (!iterableName) return;
const elementType = resolveIterableElementType(
iterableName, node, scopeEnv, declarationTypeNodes, scope,
extractKotlinElementTypeFromTypeNode, findKotlinParamElementType,
);
if (elementType) scopeEnv.set(varName, elementType);
};
/** Kotlin: val alias = u → property_declaration or variable_declaration.

View file

@ -283,14 +283,13 @@ const extractPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) =>
const extractPatternBinding: PatternBindingExtractor = (node, scopeEnv) => {
if (node.type !== 'as_pattern') return undefined;
// as_pattern children (positional, no named fields in this tree-sitter version):
// child 0: case_pattern (wrapping class_pattern) or class_pattern directly
// child 1: identifier (the bound variable name, e.g. "u")
// Note: `alias` field returns null at runtime despite node-types.json listing it.
// as_pattern: `case User() as u:` — binds matched value to a name.
// Try named field first (future grammar versions may expose it), fall back to positional.
if (node.namedChildCount < 2) return undefined;
const patternChild = node.namedChild(0);
const varNameNode = node.namedChild(node.namedChildCount - 1);
const varNameNode = node.childForFieldName('alias')
?? node.namedChild(node.namedChildCount - 1);
if (!patternChild || !varNameNode) return undefined;
if (varNameNode.type !== 'identifier') return undefined;

View file

@ -215,10 +215,21 @@ const extractPatternBinding: PatternBindingExtractor = (
declarationTypeNodes,
scope,
) => {
if (node.type !== 'let_condition') return undefined;
let patternNode: SyntaxNode | null = null;
let valueNode: SyntaxNode | null = null;
const patternNode = node.childForFieldName('pattern');
const valueNode = node.childForFieldName('value');
if (node.type === 'let_condition') {
patternNode = node.childForFieldName('pattern');
valueNode = node.childForFieldName('value');
} else if (node.type === 'match_arm') {
// match_arm → pattern is in the 'pattern' field
// source variable is in the parent match_expression's 'value' field
patternNode = node.childForFieldName('pattern');
const matchExpr = node.parent?.parent; // match_arm → match_block → match_expression
if (matchExpr?.type === 'match_expression') {
valueNode = matchExpr.childForFieldName('value');
}
}
if (!patternNode || !valueNode) return undefined;
// Only handle tuple_struct_pattern: Some(x) or Ok(x)
@ -370,7 +381,7 @@ const extractForLoopBinding: ForLoopExtractor = (
export const typeConfig: LanguageTypeConfig = {
declarationNodeTypes: DECLARATION_NODE_TYPES,
forLoopNodeTypes: FOR_LOOP_NODE_TYPES,
patternBindingNodeTypes: new Set(['let_condition']),
patternBindingNodeTypes: new Set(['let_condition', 'match_arm']),
extractDeclaration,
extractInitializer,
extractParameter,

View file

@ -214,6 +214,12 @@ const extractTsElementTypeFromAnnotation = (typeAnnotation: SyntaxNode): string
? (typeAnnotation.firstNamedChild ?? typeAnnotation)
: typeAnnotation;
// readonly User[] — readonly_type wraps array_type: unwrap and recurse
if (inner.type === 'readonly_type') {
const wrapped = inner.firstNamedChild;
if (wrapped) return extractTsElementTypeFromAnnotation(wrapped);
}
// User[] — array_type: first named child is the element type
if (inner.type === 'array_type') {
const elem = inner.firstNamedChild;

View file

@ -2613,6 +2613,18 @@ def process():
// for array_type), but the for-loop extractor uses AST walking to resolve the element type.
expect(flatGet(env, 'user')).toBe('User');
});
it('infers loop variable from readonly User[] parameter', () => {
const tree = parse(`
function process(users: readonly User[]) {
for (const user of users) {
user.save();
}
}
`, TypeScript.typescript);
const { env } = buildTypeEnv(tree, 'typescript');
expect(flatGet(env, 'user')).toBe('User');
});
});
describe('for-loop element type inference (Tier 1c) — Python', () => {
@ -2845,4 +2857,74 @@ class Foo {
expect(flatGet(env, 'user')).toBeUndefined();
});
});
describe('for-loop element type inference (Tier 1c) — Kotlin', () => {
it('infers loop variable from unannotated for with List<User> parameter', () => {
const tree = parse(`
fun process(users: List<User>) {
for (user in users) {
user.save()
}
}
`, Kotlin);
const { env } = buildTypeEnv(tree, 'kotlin');
expect(flatGet(env, 'user')).toBe('User');
});
it('still resolves explicit type annotation (regression)', () => {
const tree = parse(`
fun process(users: List<User>) {
for (user: User in users) {
user.save()
}
}
`, Kotlin);
const { env } = buildTypeEnv(tree, 'kotlin');
expect(flatGet(env, 'user')).toBe('User');
});
it('does not infer type when iterable has no annotation', () => {
const tree = parse(`
fun process() {
val users = getUsers()
for (user in users) {
user.save()
}
}
`, Kotlin);
const { env } = buildTypeEnv(tree, 'kotlin');
expect(flatGet(env, 'user')).toBeUndefined();
});
});
describe('for-loop element type inference (Tier 1c) — Java', () => {
it('still resolves explicit type enhanced-for (regression)', () => {
const tree = parse(`
class Foo {
void process(List<User> users) {
for (User user : users) {
user.save();
}
}
}
`, Java);
const { env } = buildTypeEnv(tree, 'java');
expect(flatGet(env, 'user')).toBe('User');
});
it('does not infer type when iterable has no annotation', () => {
const tree = parse(`
class Foo {
void process() {
var users = getUsers();
for (var user : users) {
user.save();
}
}
}
`, Java);
const { env } = buildTypeEnv(tree, 'java');
expect(flatGet(env, 'user')).toBeUndefined();
});
});
});