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:
@@ -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();
|
||||
|
||||
@@ -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)', () => {
|
||||
|
||||
Reference in New Issue
Block a user