fix: Phase 1 bug fixes — Go range semantics, typed_parameter, bracket depth

- Go single-var range correctly returns early for slices/maps (index, not element)
- Go single-var range on channels correctly resolves element type
- Added map_type and channel_type to extractGoElementTypeFromTypeNode
- Added isChannelType helper for channel detection before skip decision
- Added 'typed_parameter' to TYPED_PARAMETER_TYPES for Python annotated params
- Fixed bracket depth tracking in extractElementTypeFromString — only match
  selected closeChar at depth 0, return undefined for mismatched brackets
- Un-skipped 3 prematurely skipped tests (TS local const, Python List/Sequence)
- Added tests for map range, single-var range semantics, bracket edge cases
This commit is contained in:
Gergo Magyar
2026-03-16 21:42:35 +00:00
parent 00c90d476f
commit 96cbe8cdae
4 changed files with 103 additions and 16 deletions
@@ -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<string, string>,
declarationTypeNodes?: ReadonlyMap<string, SyntaxNode>,
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;
@@ -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;
@@ -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, List<User>> → String (nested angle brackets)', () => {
expect(extractElementTypeFromString('Map<String, List<User>>')).toBe('String');
});
it('mismatched close bracket at depth 0 → undefined', () => {
// openChar is '<' but first close at depth 0 is ']' — malformed
expect(extractElementTypeFromString('Array<int]')).toBeUndefined();
});
});
describe('edge cases — return undefined', () => {
it('empty string → undefined', () => {
expect(extractElementTypeFromString('')).toBeUndefined();
+33 -5
View File
@@ -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)', () => {