feat: Phase 3 partial — Rust for-loop + C# var foreach Tier 1c

- Rust: add extractForLoopBinding with for_expression support
  - Handles &users, &mut users via reference_expression unwrapping
  - extractRustElementTypeFromTypeNode: generic_type, reference_type, slice/array
  - findRustParamElementType: AST walk with reference/mut pattern unwrapping
  - 4 unit tests (Vec<User>, &[User], range expr negative, no-annotation negative)

- C#: upgrade foreach to handle var (implicit_type) via Tier 1c
  - extractCSharpElementTypeFromTypeNode: generic_name, array_type, nullable_type
  - findCSharpParamElementType: AST walk to method_declaration parameters
  - 3 unit tests (var foreach, explicit type regression, no-annotation negative)
This commit is contained in:
Gergo Magyar 2026-03-16 21:56:19 +00:00
parent 1a90112495
commit d526ee927c
3 changed files with 270 additions and 11 deletions

View file

@ -1,6 +1,6 @@
import type { SyntaxNode } from '../utils.js';
import type { ConstructorBindingScanner, ForLoopExtractor, LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, PendingAssignmentExtractor, PatternBindingExtractor } from './types.js';
import { extractSimpleTypeName, extractVarName, findChildByType, unwrapAwait } from './shared.js';
import { extractSimpleTypeName, extractVarName, findChildByType, unwrapAwait, extractGenericTypeArgs, resolveIterableElementType } from './shared.js';
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
'local_declaration_statement',
@ -131,22 +131,84 @@ const FOR_LOOP_NODE_TYPES: ReadonlySet<string> = new Set([
'foreach_statement',
]);
/** C#: foreach (User user in users) — extract loop variable binding */
/** Extract element type from a C# type annotation AST node.
* Handles generic_name (List<User>), array_type (User[]), nullable_type (?). */
const extractCSharpElementTypeFromTypeNode = (typeNode: SyntaxNode): string | undefined => {
// generic_name: List<User>, IEnumerable<User> — C# uses generic_name (not generic_type)
if (typeNode.type === 'generic_name') {
const argList = findChildByType(typeNode, 'type_argument_list');
if (argList && argList.namedChildCount >= 1) {
const firstArg = argList.namedChild(0);
if (firstArg) return extractSimpleTypeName(firstArg);
}
}
// array_type: User[]
if (typeNode.type === 'array_type') {
const elemNode = typeNode.firstNamedChild;
if (elemNode) return extractSimpleTypeName(elemNode);
}
// nullable_type: unwrap and recurse (List<User>? → List<User> → User)
if (typeNode.type === 'nullable_type') {
const inner = typeNode.firstNamedChild;
if (inner) return extractCSharpElementTypeFromTypeNode(inner);
}
return undefined;
};
/** Walk up from a foreach to the enclosing method and search parameters. */
const findCSharpParamElementType = (iterableName: string, startNode: SyntaxNode): string | undefined => {
let current: SyntaxNode | null = startNode.parent;
while (current) {
if (current.type === 'method_declaration' || current.type === 'local_function_statement') {
const paramsNode = current.childForFieldName('parameters');
if (paramsNode) {
for (let i = 0; i < paramsNode.namedChildCount; i++) {
const param = paramsNode.namedChild(i);
if (!param || param.type !== 'parameter') continue;
const nameNode = param.childForFieldName('name');
if (nameNode?.text !== iterableName) continue;
const typeNode = param.childForFieldName('type');
if (typeNode) return extractCSharpElementTypeFromTypeNode(typeNode);
}
}
break;
}
current = current.parent;
}
return undefined;
};
/** C#: foreach (User user in users) — extract loop variable binding.
* Tier 1c: for `foreach (var user in users)`, resolves element type from iterable. */
const extractForLoopBinding: 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');
// The loop variable name is in the 'left' field in tree-sitter-c-sharp
const nameNode = node.childForFieldName('left');
if (!typeNode || !nameNode) return;
// Skip 'var' — type would need to be inferred from the collection element type
if (typeNode.type === 'implicit_type' && typeNode.text === 'var') return;
const typeName = extractSimpleTypeName(typeNode);
const varName = extractVarName(nameNode);
if (typeName && varName) scopeEnv.set(varName, typeName);
if (!varName) return;
// Explicit type (existing behavior): foreach (User user in users)
if (!(typeNode.type === 'implicit_type' && typeNode.text === 'var')) {
const typeName = extractSimpleTypeName(typeNode);
if (typeName) scopeEnv.set(varName, typeName);
return;
}
// Tier 1c: implicit type (var) — resolve from iterable's container type
const rightNode = node.childForFieldName('right');
if (!rightNode || rightNode.type !== 'identifier') return;
const iterableName = rightNode.text;
const elementType = resolveIterableElementType(
iterableName, node, scopeEnv, declarationTypeNodes, scope,
extractCSharpElementTypeFromTypeNode, findCSharpParamElementType,
);
if (elementType) scopeEnv.set(varName, elementType);
};
/**

View file

@ -1,6 +1,6 @@
import type { SyntaxNode } from '../utils.js';
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, PendingAssignmentExtractor, PatternBindingExtractor } from './types.js';
import { extractSimpleTypeName, extractVarName, hasTypeAnnotation, unwrapAwait, extractGenericTypeArgs } from './shared.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';
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
'let_declaration',
@ -269,13 +269,113 @@ const extractPatternBinding: PatternBindingExtractor = (
return { varName: innerVar, typeName: typeArgs[argIndex] };
};
// --- For-loop Tier 1c ---
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
if (typeNode.type === 'generic_type') {
const args = extractGenericTypeArgs(typeNode);
if (args.length >= 1) return args[0];
}
// 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);
}
// array_type: [User; N] — element is the first child
if (typeNode.type === 'array_type') {
const elemNode = typeNode.firstNamedChild;
if (elemNode) return extractSimpleTypeName(elemNode);
}
// slice_type: [User] — element is the first child
if (typeNode.type === 'slice_type') {
const elemNode = typeNode.firstNamedChild;
if (elemNode) return extractSimpleTypeName(elemNode);
}
return undefined;
};
/** 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 => {
let current: SyntaxNode | null = startNode.parent;
while (current) {
if (current.type === 'function_item') {
const paramsNode = current.childForFieldName('parameters');
if (paramsNode) {
for (let i = 0; i < paramsNode.namedChildCount; i++) {
const param = paramsNode.namedChild(i);
if (!param || param.type !== 'parameter') continue;
const nameNode = param.childForFieldName('pattern');
if (!nameNode) continue;
// Unwrap reference patterns: &users, &mut users
let identNode = nameNode;
if (identNode.type === 'reference_pattern') {
identNode = identNode.lastNamedChild ?? identNode;
}
if (identNode.type === 'mut_pattern') {
identNode = identNode.firstNamedChild ?? identNode;
}
if (identNode.text !== iterableName) continue;
const typeNode = param.childForFieldName('type');
if (typeNode) return extractRustElementTypeFromTypeNode(typeNode);
}
}
break;
}
current = current.parent;
}
return undefined;
};
/** Rust: for user in &users where users has a known container type.
* Unwraps reference_expression (&users, &mut users) to get the iterable name. */
const extractForLoopBinding: ForLoopExtractor = (
node: SyntaxNode,
scopeEnv: Map<string, string>,
declarationTypeNodes: ReadonlyMap<string, SyntaxNode>,
scope: string,
): void => {
if (node.type !== 'for_expression') return;
const patternNode = node.childForFieldName('pattern');
const valueNode = node.childForFieldName('value');
if (!patternNode || !valueNode) return;
// Extract iterable name — may be &users, &mut users, or plain users
let iterableName: 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;
}
if (!iterableName) return;
const elementType = resolveIterableElementType(
iterableName, node, scopeEnv, declarationTypeNodes, scope,
extractRustElementTypeFromTypeNode, findRustParamElementType,
);
if (!elementType) return;
const loopVarName = extractVarName(patternNode);
if (loopVarName) scopeEnv.set(loopVarName, elementType);
};
export const typeConfig: LanguageTypeConfig = {
declarationNodeTypes: DECLARATION_NODE_TYPES,
forLoopNodeTypes: FOR_LOOP_NODE_TYPES,
patternBindingNodeTypes: new Set(['let_condition']),
extractDeclaration,
extractInitializer,
extractParameter,
scanConstructorBinding,
extractForLoopBinding,
extractPendingAssignment,
extractPatternBinding,
};

View file

@ -2748,4 +2748,101 @@ func process() {
expect(flatGet(env, 'user')).toBeUndefined();
});
});
describe('for-loop element type inference (Tier 1c) — Rust', () => {
it('infers loop variable from Vec<User> parameter (for user in &users)', () => {
const tree = parse(`
fn process(users: Vec<User>) {
for user in &users {
user.save();
}
}
`, Rust);
const { env } = buildTypeEnv(tree, 'rust');
expect(flatGet(env, 'user')).toBe('User');
});
it('infers loop variable from &[User] slice parameter', () => {
const tree = parse(`
fn process(users: &[User]) {
for user in users {
user.save();
}
}
`, Rust);
const { env } = buildTypeEnv(tree, 'rust');
expect(flatGet(env, 'user')).toBe('User');
});
it('does not infer type for range expression (0..10)', () => {
const tree = parse(`
fn process() {
for i in 0..10 {
println!("{}", i);
}
}
`, Rust);
const { env } = buildTypeEnv(tree, 'rust');
expect(flatGet(env, 'i')).toBeUndefined();
});
it('does not infer type when iterable has no annotation', () => {
const tree = parse(`
fn process() {
let users = get_users();
for user in &users {
user.save();
}
}
`, Rust);
const { env } = buildTypeEnv(tree, 'rust');
expect(flatGet(env, 'user')).toBeUndefined();
});
});
describe('for-loop element type inference (Tier 1c) — C#', () => {
it('infers loop variable from var foreach with List<User> parameter', () => {
const tree = parse(`
using System.Collections.Generic;
class Foo {
void Process(List<User> users) {
foreach (var user in users) {
user.Save();
}
}
}
`, CSharp);
const { env } = buildTypeEnv(tree, 'csharp');
expect(flatGet(env, 'user')).toBe('User');
});
it('still resolves explicit type foreach (regression)', () => {
const tree = parse(`
class Foo {
void Process(List<User> users) {
foreach (User user in users) {
user.Save();
}
}
}
`, CSharp);
const { env } = buildTypeEnv(tree, 'csharp');
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();
foreach (var user in users) {
user.Save();
}
}
}
`, CSharp);
const { env } = buildTypeEnv(tree, 'csharp');
expect(flatGet(env, 'user')).toBeUndefined();
});
});
});