diff --git a/gitnexus/src/core/ingestion/type-extractors/go.ts b/gitnexus/src/core/ingestion/type-extractors/go.ts index 869c35337..fbbfd408a 100644 --- a/gitnexus/src/core/ingestion/type-extractors/go.ts +++ b/gitnexus/src/core/ingestion/type-extractors/go.ts @@ -203,10 +203,36 @@ const extractGoElementTypeFromTypeNode = (typeNode: SyntaxNode): string | undefi const elemNode = typeNode.childForFieldName('element'); if (elemNode) return extractSimpleTypeName(elemNode); } + // map_type: map[string]User — value field is the element type (for range, second var gets value) + if (typeNode.type === 'map_type') { + const valueNode = typeNode.childForFieldName('value'); + if (valueNode) return extractSimpleTypeName(valueNode); + } + // channel_type: chan User — the type argument is the element type + if (typeNode.type === 'channel_type') { + const valueNode = typeNode.childForFieldName('value') ?? typeNode.lastNamedChild; + if (valueNode) return extractSimpleTypeName(valueNode); + } // Fallback: text-based extraction ([]User → User, User[] → User) return extractElementTypeFromString(typeNode.text); }; +/** Check if a Go type node represents a channel type. Used to determine + * whether single-var range yields the element (channels) vs index (slices/maps). */ +const isChannelType = ( + iterableName: string, + scopeEnv: ReadonlyMap, + declarationTypeNodes?: ReadonlyMap, + scope?: string, +): boolean => { + if (declarationTypeNodes && scope) { + const typeNode = declarationTypeNodes.get(`${scope}\0${iterableName}`); + if (typeNode) return typeNode.type === 'channel_type'; + } + const t = scopeEnv.get(iterableName); + return !!t && t.startsWith('chan '); +}; + /** * Walk up the AST from a for-statement to find the enclosing function declaration, * then search its parameters for one named `iterableName`. @@ -301,20 +327,34 @@ const extractForLoopBinding: ForLoopExtractor = ( if (!elementType) return; - // The loop variable(s) are in the `left` field. For `_, user` this is an - // expression_list with two identifiers; we take the second (the value). - // For a single `user := range users` we take the first identifier. + // The loop variable(s) are in the `left` field. + // Go range semantics: + // Slice/Array/String: single-var → INDEX (int); two-var → (index, element) + // Map: single-var → KEY; two-var → (key, value) + // Channel: single-var → ELEMENT (channels have no index) const leftNode = rangeClause.childForFieldName('left'); if (!leftNode) return; let loopVarNode: SyntaxNode | null = null; if (leftNode.type === 'expression_list') { - // `_, user` — take second named child (index=0 is `_`, index=1 is the element) - loopVarNode = leftNode.namedChildCount >= 2 - ? leftNode.namedChild(1) - : leftNode.namedChild(0); + if (leftNode.namedChildCount >= 2) { + // Two-var form: `_, user` or `i, user` — second variable gets element/value type + loopVarNode = leftNode.namedChild(1); + } else { + // Single-var in expression_list — yields INDEX for slices/maps, ELEMENT for channels + if (isChannelType(iterableName, scopeEnv, declarationTypeNodes, scope)) { + loopVarNode = leftNode.namedChild(0); + } else { + return; // index-only range on slice/map — skip + } + } } else { - loopVarNode = leftNode; + // Plain identifier (single-var form without expression_list) + if (isChannelType(iterableName, scopeEnv, declarationTypeNodes, scope)) { + loopVarNode = leftNode; + } else { + return; // index-only range on slice/map — skip + } } if (!loopVarNode) return; diff --git a/gitnexus/src/core/ingestion/type-extractors/shared.ts b/gitnexus/src/core/ingestion/type-extractors/shared.ts index 0b2303dc7..8925775a3 100644 --- a/gitnexus/src/core/ingestion/type-extractors/shared.ts +++ b/gitnexus/src/core/ingestion/type-extractors/shared.ts @@ -141,6 +141,7 @@ export const TYPED_PARAMETER_TYPES = new Set([ 'optional_parameter', // TS: (x?: Foo) 'formal_parameter', // Java/Kotlin 'parameter', // C#/Rust/Go/Python/Swift + 'typed_parameter', // Python: def f(x: Foo) — distinct from 'parameter' in tree-sitter-python 'parameter_declaration', // C/C++ void f(Type name) 'simple_parameter', // PHP function(Foo $x) 'property_promotion_parameter', // PHP 8.0+ constructor promotion: __construct(private Foo $x) @@ -394,15 +395,18 @@ export function extractElementTypeFromString(typeStr: string): string | undefine // Walk bracket-balanced from the character after the opening bracket to find // the matching close bracket, tracking depth for nested brackets. + // All bracket types (<, >, [, ]) contribute to depth uniformly, but only the + // selected closeChar can match at depth 0 (prevents cross-bracket miscounting). let depth = 0; const start = openIdx + 1; for (let i = start; i < typeStr.length; i++) { const ch = typeStr[i]; - if (ch === openChar || ch === '<' || ch === '[') { + if (ch === '<' || ch === '[') { depth++; - } else if (ch === closeChar || ch === '>' || ch === ']') { + } else if (ch === '>' || ch === ']') { if (depth === 0) { - // Found the matching close bracket — extract and validate first arg. + // At depth 0 — only match if it is our selected close bracket. + if (ch !== closeChar) return undefined; // mismatched bracket = malformed const inner = typeStr.slice(start, i).trim(); const firstArg = extractFirstArg(inner); return firstArg && /^\w+$/.test(firstArg) ? firstArg : undefined; diff --git a/gitnexus/test/unit/extract-element-type-from-string.test.ts b/gitnexus/test/unit/extract-element-type-from-string.test.ts index b1f80e8da..29dc31e75 100644 --- a/gitnexus/test/unit/extract-element-type-from-string.test.ts +++ b/gitnexus/test/unit/extract-element-type-from-string.test.ts @@ -97,6 +97,21 @@ describe('extractElementTypeFromString', () => { }); }); + describe('cross-bracket nesting (bracket depth fix)', () => { + it('Dict[str, List[int]] → str (square-bracket outer, nested inner)', () => { + expect(extractElementTypeFromString('Dict[str, List[int]]')).toBe('str'); + }); + + it('Map> → String (nested angle brackets)', () => { + expect(extractElementTypeFromString('Map>')).toBe('String'); + }); + + it('mismatched close bracket at depth 0 → undefined', () => { + // openChar is '<' but first close at depth 0 is ']' — malformed + expect(extractElementTypeFromString('Array { it('empty string → undefined', () => { expect(extractElementTypeFromString('')).toBeUndefined(); diff --git a/gitnexus/test/unit/type-env.test.ts b/gitnexus/test/unit/type-env.test.ts index 36f34143c..2bfd25091 100644 --- a/gitnexus/test/unit/type-env.test.ts +++ b/gitnexus/test/unit/type-env.test.ts @@ -2599,7 +2599,7 @@ def process(): expect(flatGet(env, 'user')).toBeUndefined(); }); - it.skip('infers loop variable from a locally declared const with User[] annotation', () => { + it('infers loop variable from a locally declared const with User[] annotation', () => { const tree = parse(` function process() { const users: User[] = getUsers(); @@ -2616,7 +2616,7 @@ def process(): }); describe('for-loop element type inference (Tier 1c) — Python', () => { - it.skip('infers loop variable type from List[User] parameter annotation', () => { + it('infers loop variable type from List[User] parameter annotation', () => { const tree = parse(` def process(users: List[User]): for user in users: @@ -2626,7 +2626,7 @@ def process(users: List[User]): expect(flatGet(env, 'user')).toBe('User'); }); - it.skip('infers loop variable type from Sequence[User] annotation style', () => { + it('infers loop variable type from Sequence[User] annotation style', () => { const tree = parse(` def process(users: Sequence[User]): for user in users: @@ -2678,7 +2678,7 @@ func process(users []User) { expect(flatGet(env, 'user')).toBe('User'); }); - it('infers loop variable from single-var range form (user := range users)', () => { + it('does NOT infer element type for single-var slice range (yields index, not element)', () => { const tree = parse(` package main func process(users []User) { @@ -2688,7 +2688,35 @@ func process(users []User) { } `, Go); const { env } = buildTypeEnv(tree, 'go'); - expect(flatGet(env, 'user')).toBe('User'); + // In Go, `for v := range slice` gives the INDEX (int), not the element. + expect(flatGet(env, 'user')).toBeUndefined(); + }); + + it('infers loop variable from map range (_, v := range myMap)', () => { + const tree = parse(` +package main +func process(myMap map[string]User) { + for _, v := range myMap { + v.Save() + } +} + `, Go); + const { env } = buildTypeEnv(tree, 'go'); + expect(flatGet(env, 'v')).toBe('User'); + }); + + it('does NOT infer element type for single-var map range (yields key, not value)', () => { + const tree = parse(` +package main +func process(myMap map[string]User) { + for k := range myMap { + _ = k + } +} + `, Go); + const { env } = buildTypeEnv(tree, 'go'); + // Single-var map range gives the KEY, not the value + expect(flatGet(env, 'k')).toBeUndefined(); }); it('does not infer type for C-style for loops (no range_clause)', () => {