From d526ee927ca8de5a985cebb6d5bd13857a362f8d Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Mon, 16 Mar 2026 21:56:19 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Phase=203=20partial=20=E2=80=94=20Rust?= =?UTF-8?q?=20for-loop=20+=20C#=20var=20foreach=20Tier=201c?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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], 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) --- .../core/ingestion/type-extractors/csharp.ts | 80 ++++++++++++-- .../core/ingestion/type-extractors/rust.ts | 104 +++++++++++++++++- gitnexus/test/unit/type-env.test.ts | 97 ++++++++++++++++ 3 files changed, 270 insertions(+), 11 deletions(-) diff --git a/gitnexus/src/core/ingestion/type-extractors/csharp.ts b/gitnexus/src/core/ingestion/type-extractors/csharp.ts index 2bab31dd4..4936abbf1 100644 --- a/gitnexus/src/core/ingestion/type-extractors/csharp.ts +++ b/gitnexus/src/core/ingestion/type-extractors/csharp.ts @@ -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 = new Set([ 'local_declaration_statement', @@ -131,22 +131,84 @@ const FOR_LOOP_NODE_TYPES: ReadonlySet = 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), array_type (User[]), nullable_type (?). */ +const extractCSharpElementTypeFromTypeNode = (typeNode: SyntaxNode): string | undefined => { + // generic_name: List, IEnumerable — 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? → List → 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, - _declarationTypeNodes: ReadonlyMap, - _scope: string, + declarationTypeNodes: ReadonlyMap, + 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); }; /** diff --git a/gitnexus/src/core/ingestion/type-extractors/rust.ts b/gitnexus/src/core/ingestion/type-extractors/rust.ts index 4d65e1d3f..40768668c 100644 --- a/gitnexus/src/core/ingestion/type-extractors/rust.ts +++ b/gitnexus/src/core/ingestion/type-extractors/rust.ts @@ -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 = 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 = new Set(['for_expression']); + +/** Extract element type from a Rust type annotation AST node. + * Handles: generic_type (Vec), 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, HashSet — 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 — 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, + declarationTypeNodes: ReadonlyMap, + 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, }; diff --git a/gitnexus/test/unit/type-env.test.ts b/gitnexus/test/unit/type-env.test.ts index 2bfd25091..1a2269996 100644 --- a/gitnexus/test/unit/type-env.test.ts +++ b/gitnexus/test/unit/type-env.test.ts @@ -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 parameter (for user in &users)', () => { + const tree = parse(` +fn process(users: Vec) { + 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 parameter', () => { + const tree = parse(` +using System.Collections.Generic; +class Foo { + void Process(List 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 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(); + }); + }); });