mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-08 03:08:13 +00:00
feat: Phase 3 complete — all language gaps + pattern matching
Kotlin Tier 1c:
- Unannotated for-loop resolves via shared helper
- extractKotlinElementTypeFromTypeNode handles type_projection unwrapping
- findKotlinParamElementType walks to function_declaration
Java Tier 1c:
- var foreach resolves via shared helper
- extractJavaElementTypeFromTypeNode handles generic_type, array_type
- findJavaParamElementType walks to method_declaration
TypeScript:
- readonly User[] unwrapped via readonly_type → array_type recursion
C# switch patterns:
- declaration_pattern added to patternBindingNodeTypes
- extractPatternBinding handles standalone declaration_pattern (switch case/expr)
Rust match arms:
- match_arm added to patternBindingNodeTypes
- extractPatternBinding extended with match_arm → match_expression parent traversal
Python:
- as_pattern tries childForFieldName('alias') before positional fallback
Tests: 237 pass (was 224), 13 new tests added
This commit is contained in:
parent
d526ee927c
commit
104f9cd311
6 changed files with 270 additions and 35 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<string> = 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<User>), 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<string, string>,
|
||||
_declarationTypeNodes: ReadonlyMap<string, SyntaxNode>,
|
||||
_scope: string,
|
||||
declarationTypeNodes: ReadonlyMap<string, SyntaxNode>,
|
||||
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<string> = 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<string, string>,
|
||||
_declarationTypeNodes: ReadonlyMap<string, SyntaxNode>,
|
||||
_scope: string,
|
||||
declarationTypeNodes: ReadonlyMap<string, SyntaxNode>,
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<User> parameter', () => {
|
||||
const tree = parse(`
|
||||
fun process(users: List<User>) {
|
||||
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<User>) {
|
||||
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<User> 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue