diff --git a/gitnexus/src/core/ingestion/type-extractors/csharp.ts b/gitnexus/src/core/ingestion/type-extractors/csharp.ts index 4936abbf1..afe2431dd 100644 --- a/gitnexus/src/core/ingestion/type-extractors/csharp.ts +++ b/gitnexus/src/core/ingestion/type-extractors/csharp.ts @@ -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, diff --git a/gitnexus/src/core/ingestion/type-extractors/jvm.ts b/gitnexus/src/core/ingestion/type-extractors/jvm.ts index 646d1a228..9ee3aa19d 100644 --- a/gitnexus/src/core/ingestion/type-extractors/jvm.ts +++ b/gitnexus/src/core/ingestion/type-extractors/jvm.ts @@ -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 = 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), 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, - _declarationTypeNodes: ReadonlyMap, - _scope: string, + declarationTypeNodes: ReadonlyMap, + 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 = 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, - _declarationTypeNodes: ReadonlyMap, - _scope: string, + declarationTypeNodes: ReadonlyMap, + 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. diff --git a/gitnexus/src/core/ingestion/type-extractors/python.ts b/gitnexus/src/core/ingestion/type-extractors/python.ts index e0e0c535b..f971f48cc 100644 --- a/gitnexus/src/core/ingestion/type-extractors/python.ts +++ b/gitnexus/src/core/ingestion/type-extractors/python.ts @@ -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; diff --git a/gitnexus/src/core/ingestion/type-extractors/rust.ts b/gitnexus/src/core/ingestion/type-extractors/rust.ts index 40768668c..71f869342 100644 --- a/gitnexus/src/core/ingestion/type-extractors/rust.ts +++ b/gitnexus/src/core/ingestion/type-extractors/rust.ts @@ -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, diff --git a/gitnexus/src/core/ingestion/type-extractors/typescript.ts b/gitnexus/src/core/ingestion/type-extractors/typescript.ts index 8ec93a894..b3e7706e4 100644 --- a/gitnexus/src/core/ingestion/type-extractors/typescript.ts +++ b/gitnexus/src/core/ingestion/type-extractors/typescript.ts @@ -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; diff --git a/gitnexus/test/unit/type-env.test.ts b/gitnexus/test/unit/type-env.test.ts index 1a2269996..7e1056e41 100644 --- a/gitnexus/test/unit/type-env.test.ts +++ b/gitnexus/test/unit/type-env.test.ts @@ -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 parameter', () => { + const tree = parse(` +fun process(users: List) { + 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) { + 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 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(); + }); + }); });