mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-08 03:08:13 +00:00
feat: method-aware for-loop extractors + integration tests for all languages
Upgrade 4 existing extractors + create 3 new ones for full cross-language coverage of call_expression iterables and container descriptor resolution: Upgraded (add call expr iterable + methodToTypeArgPosition): - Java: method_invocation (data.keySet(), data.values()) - Kotlin: navigation_expression + call_expression (data.keys, data.values()) - C#: member_access_expression + invocation_expression (data.Keys, data.Values) - Go: TypeArgPosition threading for Go 1.18+ generics New for-loop extractors: - C++: for_range_loop with auto& unwrapping, template_type + qualified_identifier (std::vector<User>) extraction, explicit vs auto type handling - PHP: foreach_statement with simple/key-value/by-reference forms, PHPDoc @param priority over AST array type - Ruby: for-in with YARD @param type resolution via comment parsing Integration test fixtures + tests for all 6 languages: - java-map-keys-values (Map.values() + List iteration) - kotlin-map-keys-values (HashMap.values + List iteration) - csharp-dictionary-keys-values (Dictionary.Values foreach) - cpp-range-for (auto& + const auto& range-based for) - php-foreach-loop (foreach with PHPDoc @param User[]) - ruby-for-in-loop (for-in with YARD @param Array<User>) Bugs fixed during integration testing: - C++: qualified_identifier (std::vector) not unwrapped to template_type - PHP: extractParameter overwrote PHPDoc-derived types with bare 'array' 252 unit tests pass, 201 integration tests pass across 6 languages.
This commit is contained in:
parent
b4986fdaba
commit
d05aa9ef6f
30 changed files with 874 additions and 57 deletions
|
|
@ -1,10 +1,9 @@
|
|||
import type { SyntaxNode } from '../utils.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, PendingAssignmentExtractor } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName } from './shared.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, PendingAssignmentExtractor, ForLoopExtractor } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName, resolveIterableElementType, methodToTypeArgPosition, type TypeArgPosition } from './shared.js';
|
||||
|
||||
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
|
||||
'declaration',
|
||||
'for_range_loop',
|
||||
]);
|
||||
|
||||
/** C++: Type x = ...; Type* x; Type& x; */
|
||||
|
|
@ -183,11 +182,158 @@ const extractPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) =>
|
|||
return { lhs, rhs: value.text };
|
||||
};
|
||||
|
||||
// --- For-loop Tier 1c ---
|
||||
|
||||
const FOR_LOOP_NODE_TYPES: ReadonlySet<string> = new Set(['for_range_loop']);
|
||||
|
||||
/** Extract template type arguments from a C++ template_type node.
|
||||
* C++ template_type uses template_argument_list (not type_arguments), and each
|
||||
* argument is a type_descriptor with a 'type' field containing the type_specifier. */
|
||||
const extractCppTemplateTypeArgs = (templateTypeNode: SyntaxNode): string[] => {
|
||||
const argsNode = templateTypeNode.childForFieldName('arguments');
|
||||
if (!argsNode || argsNode.type !== 'template_argument_list') return [];
|
||||
const result: string[] = [];
|
||||
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
||||
let argNode = argsNode.namedChild(i);
|
||||
if (!argNode) continue;
|
||||
// type_descriptor wraps the actual type specifier in a 'type' field
|
||||
if (argNode.type === 'type_descriptor') {
|
||||
const inner = argNode.childForFieldName('type');
|
||||
if (inner) argNode = inner;
|
||||
}
|
||||
const name = extractSimpleTypeName(argNode);
|
||||
if (name) result.push(name);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/** Extract element type from a C++ type annotation AST node.
|
||||
* Handles: template_type (vector<User>, map<string, User>),
|
||||
* pointer/reference types (User*, User&). */
|
||||
const extractCppElementTypeFromTypeNode = (typeNode: SyntaxNode, pos: TypeArgPosition = 'last'): string | undefined => {
|
||||
// template_type: vector<User>, map<string, User> — extract type arg based on position
|
||||
if (typeNode.type === 'template_type') {
|
||||
const args = extractCppTemplateTypeArgs(typeNode);
|
||||
if (args.length >= 1) return pos === 'first' ? args[0] : args[args.length - 1];
|
||||
}
|
||||
// reference/pointer types: unwrap and recurse (vector<User>& → vector<User>)
|
||||
if (typeNode.type === 'reference_type' || typeNode.type === 'pointer_type'
|
||||
|| typeNode.type === 'type_descriptor') {
|
||||
const inner = typeNode.lastNamedChild;
|
||||
if (inner) return extractCppElementTypeFromTypeNode(inner, pos);
|
||||
}
|
||||
// qualified/scoped types: std::vector<User> → unwrap to template_type child
|
||||
if (typeNode.type === 'qualified_identifier' || typeNode.type === 'scoped_type_identifier') {
|
||||
const inner = typeNode.lastNamedChild;
|
||||
if (inner) return extractCppElementTypeFromTypeNode(inner, pos);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** Walk up from a for-range-loop to the enclosing function_definition and search parameters
|
||||
* for one named `iterableName`. Returns the element type from its annotation. */
|
||||
const findCppParamElementType = (iterableName: string, startNode: SyntaxNode, pos: TypeArgPosition = 'last'): string | undefined => {
|
||||
let current: SyntaxNode | null = startNode.parent;
|
||||
while (current) {
|
||||
if (current.type === 'function_definition') {
|
||||
const declarator = current.childForFieldName('declarator');
|
||||
// function_definition > declarator (function_declarator) > parameters (parameter_list)
|
||||
const paramsNode = declarator?.childForFieldName('parameters');
|
||||
if (paramsNode) {
|
||||
for (let i = 0; i < paramsNode.namedChildCount; i++) {
|
||||
const param = paramsNode.namedChild(i);
|
||||
if (!param || param.type !== 'parameter_declaration') continue;
|
||||
const paramDeclarator = param.childForFieldName('declarator');
|
||||
if (!paramDeclarator) continue;
|
||||
// Unwrap reference/pointer declarators: vector<User>& users → &users
|
||||
let identNode = paramDeclarator;
|
||||
if (identNode.type === 'reference_declarator' || identNode.type === 'pointer_declarator') {
|
||||
identNode = identNode.firstNamedChild ?? identNode;
|
||||
}
|
||||
if (identNode.text !== iterableName) continue;
|
||||
const typeNode = param.childForFieldName('type');
|
||||
if (typeNode) return extractCppElementTypeFromTypeNode(typeNode, pos);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** C++: for (auto& user : users) — extract loop variable binding.
|
||||
* Handles explicit types (for (User& user : users)) and auto (for (auto& user : users)).
|
||||
* For auto, resolves element type from the iterable's container type. */
|
||||
const extractForLoopBinding: ForLoopExtractor = (
|
||||
node: SyntaxNode,
|
||||
scopeEnv: Map<string, string>,
|
||||
declarationTypeNodes: ReadonlyMap<string, SyntaxNode>,
|
||||
scope: string,
|
||||
): void => {
|
||||
if (node.type !== 'for_range_loop') return;
|
||||
|
||||
const typeNode = node.childForFieldName('type');
|
||||
const declaratorNode = node.childForFieldName('declarator');
|
||||
const rightNode = node.childForFieldName('right');
|
||||
if (!typeNode || !declaratorNode || !rightNode) return;
|
||||
|
||||
// Unwrap reference/pointer declarator to get the loop variable name
|
||||
let nameNode = declaratorNode;
|
||||
if (nameNode.type === 'reference_declarator' || nameNode.type === 'pointer_declarator') {
|
||||
nameNode = nameNode.firstNamedChild ?? nameNode;
|
||||
}
|
||||
const varName = extractVarName(nameNode);
|
||||
if (!varName) return;
|
||||
|
||||
// Check if the type is auto/placeholder — if not, use the explicit type directly
|
||||
const isAuto = typeNode.type === 'placeholder_type_specifier'
|
||||
|| typeNode.text === 'auto'
|
||||
|| typeNode.text === 'const auto'
|
||||
|| typeNode.text === 'decltype(auto)';
|
||||
|
||||
if (!isAuto) {
|
||||
// Explicit type: for (User& user : users) — extract directly
|
||||
const typeName = extractSimpleTypeName(typeNode);
|
||||
if (typeName) scopeEnv.set(varName, typeName);
|
||||
return;
|
||||
}
|
||||
|
||||
// auto/const auto/auto& — resolve from the iterable's container type
|
||||
// Extract iterable name + optional method
|
||||
let iterableName: string | undefined;
|
||||
let methodName: string | undefined;
|
||||
if (rightNode.type === 'identifier') {
|
||||
iterableName = rightNode.text;
|
||||
} else if (rightNode.type === 'call_expression') {
|
||||
// users.begin() is NOT used in range-for, but container.items() etc. might be
|
||||
const fieldExpr = rightNode.childForFieldName('function');
|
||||
if (fieldExpr?.type === 'field_expression') {
|
||||
const obj = fieldExpr.firstNamedChild;
|
||||
if (obj?.type === 'identifier') iterableName = obj.text;
|
||||
const field = fieldExpr.lastNamedChild;
|
||||
if (field?.type === 'field_identifier') methodName = field.text;
|
||||
}
|
||||
}
|
||||
if (!iterableName) return;
|
||||
|
||||
const containerTypeName = scopeEnv.get(iterableName);
|
||||
const typeArgPos = methodToTypeArgPosition(methodName, containerTypeName);
|
||||
const elementType = resolveIterableElementType(
|
||||
iterableName, node, scopeEnv, declarationTypeNodes, scope,
|
||||
extractCppElementTypeFromTypeNode, findCppParamElementType,
|
||||
typeArgPos,
|
||||
);
|
||||
if (elementType) scopeEnv.set(varName, elementType);
|
||||
};
|
||||
|
||||
export const typeConfig: LanguageTypeConfig = {
|
||||
declarationNodeTypes: DECLARATION_NODE_TYPES,
|
||||
forLoopNodeTypes: FOR_LOOP_NODE_TYPES,
|
||||
extractDeclaration,
|
||||
extractParameter,
|
||||
extractInitializer,
|
||||
scanConstructorBinding,
|
||||
extractForLoopBinding,
|
||||
extractPendingAssignment,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { SyntaxNode } from '../utils.js';
|
||||
import type { ConstructorBindingScanner, ForLoopExtractor, LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, PendingAssignmentExtractor, PatternBindingExtractor } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName, findChildByType, unwrapAwait, extractGenericTypeArgs, resolveIterableElementType } from './shared.js';
|
||||
import { extractSimpleTypeName, extractVarName, findChildByType, unwrapAwait, extractGenericTypeArgs, resolveIterableElementType, methodToTypeArgPosition, type TypeArgPosition } from './shared.js';
|
||||
|
||||
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
|
||||
'local_declaration_statement',
|
||||
|
|
@ -132,14 +132,21 @@ const FOR_LOOP_NODE_TYPES: ReadonlySet<string> = new Set([
|
|||
]);
|
||||
|
||||
/** Extract element type from a C# type annotation AST node.
|
||||
* Handles generic_name (List<User>), array_type (User[]), nullable_type (?). */
|
||||
const extractCSharpElementTypeFromTypeNode = (typeNode: SyntaxNode): string | undefined => {
|
||||
// generic_name: List<User>, IEnumerable<User> — C# uses generic_name (not generic_type)
|
||||
* Handles generic_name (List<User>), array_type (User[]), nullable_type (?).
|
||||
* `pos` selects which type arg: 'first' for keys, 'last' for values (default). */
|
||||
const extractCSharpElementTypeFromTypeNode = (typeNode: SyntaxNode, pos: TypeArgPosition = 'last'): string | undefined => {
|
||||
// generic_name: List<User>, IEnumerable<User>, Dictionary<string, User>
|
||||
// C# uses generic_name (not generic_type)
|
||||
if (typeNode.type === 'generic_name') {
|
||||
const argList = findChildByType(typeNode, 'type_argument_list');
|
||||
if (argList && argList.namedChildCount >= 1) {
|
||||
const firstArg = argList.namedChild(0);
|
||||
if (firstArg) return extractSimpleTypeName(firstArg);
|
||||
if (pos === 'first') {
|
||||
const firstArg = argList.namedChild(0);
|
||||
if (firstArg) return extractSimpleTypeName(firstArg);
|
||||
} else {
|
||||
const lastArg = argList.namedChild(argList.namedChildCount - 1);
|
||||
if (lastArg) return extractSimpleTypeName(lastArg);
|
||||
}
|
||||
}
|
||||
}
|
||||
// array_type: User[]
|
||||
|
|
@ -150,13 +157,13 @@ const extractCSharpElementTypeFromTypeNode = (typeNode: SyntaxNode): string | un
|
|||
// nullable_type: unwrap and recurse (List<User>? → List<User> → User)
|
||||
if (typeNode.type === 'nullable_type') {
|
||||
const inner = typeNode.firstNamedChild;
|
||||
if (inner) return extractCSharpElementTypeFromTypeNode(inner);
|
||||
if (inner) return extractCSharpElementTypeFromTypeNode(inner, pos);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** Walk up from a foreach to the enclosing method and search parameters. */
|
||||
const findCSharpParamElementType = (iterableName: string, startNode: SyntaxNode): string | undefined => {
|
||||
const findCSharpParamElementType = (iterableName: string, startNode: SyntaxNode, pos: TypeArgPosition = 'last'): string | undefined => {
|
||||
let current: SyntaxNode | null = startNode.parent;
|
||||
while (current) {
|
||||
if (current.type === 'method_declaration' || current.type === 'local_function_statement') {
|
||||
|
|
@ -168,7 +175,7 @@ const findCSharpParamElementType = (iterableName: string, startNode: SyntaxNode)
|
|||
const nameNode = param.childForFieldName('name');
|
||||
if (nameNode?.text !== iterableName) continue;
|
||||
const typeNode = param.childForFieldName('type');
|
||||
if (typeNode) return extractCSharpElementTypeFromTypeNode(typeNode);
|
||||
if (typeNode) return extractCSharpElementTypeFromTypeNode(typeNode, pos);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
|
@ -201,12 +208,35 @@ const extractForLoopBinding: ForLoopExtractor = (
|
|||
|
||||
// Tier 1c: implicit type (var) — resolve from iterable's container type
|
||||
const rightNode = node.childForFieldName('right');
|
||||
if (!rightNode || rightNode.type !== 'identifier') return;
|
||||
const iterableName = rightNode.text;
|
||||
let iterableName: string | undefined;
|
||||
let methodName: string | undefined;
|
||||
|
||||
if (rightNode?.type === 'identifier') {
|
||||
iterableName = rightNode.text;
|
||||
} else if (rightNode?.type === 'member_access_expression') {
|
||||
// C# property access: data.Keys, data.Values → member_access_expression
|
||||
const obj = rightNode.childForFieldName('expression');
|
||||
const prop = rightNode.childForFieldName('name');
|
||||
if (obj?.type === 'identifier') iterableName = obj.text;
|
||||
if (prop?.type === 'identifier') methodName = prop.text;
|
||||
} else if (rightNode?.type === 'invocation_expression') {
|
||||
// C# method call: data.Select(...) → invocation_expression > member_access_expression
|
||||
const fn = rightNode.firstNamedChild;
|
||||
if (fn?.type === 'member_access_expression') {
|
||||
const obj = fn.childForFieldName('expression');
|
||||
const prop = fn.childForFieldName('name');
|
||||
if (obj?.type === 'identifier') iterableName = obj.text;
|
||||
if (prop?.type === 'identifier') methodName = prop.text;
|
||||
}
|
||||
}
|
||||
if (!iterableName) return;
|
||||
|
||||
const containerTypeName = scopeEnv.get(iterableName);
|
||||
const typeArgPos = methodToTypeArgPosition(methodName, containerTypeName);
|
||||
const elementType = resolveIterableElementType(
|
||||
iterableName, node, scopeEnv, declarationTypeNodes, scope,
|
||||
extractCSharpElementTypeFromTypeNode, findCSharpParamElementType,
|
||||
typeArgPos,
|
||||
);
|
||||
if (elementType) scopeEnv.set(varName, elementType);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { SyntaxNode } from '../utils.js';
|
||||
import type { ConstructorBindingScanner, ForLoopExtractor, LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, PendingAssignmentExtractor } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName, extractElementTypeFromString, findChildByType, resolveIterableElementType } from './shared.js';
|
||||
import { extractSimpleTypeName, extractVarName, extractElementTypeFromString, extractGenericTypeArgs, findChildByType, resolveIterableElementType, methodToTypeArgPosition, type TypeArgPosition } from './shared.js';
|
||||
|
||||
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
|
||||
'var_declaration',
|
||||
|
|
@ -197,7 +197,7 @@ const GO_FUNCTION_NODE_TYPES = new Set([
|
|||
* array_type "[10]User" → element field → type_identifier "User"
|
||||
* Falls back to text-based extraction via extractElementTypeFromString.
|
||||
*/
|
||||
const extractGoElementTypeFromTypeNode = (typeNode: SyntaxNode): string | undefined => {
|
||||
const extractGoElementTypeFromTypeNode = (typeNode: SyntaxNode, pos: TypeArgPosition = 'last'): string | undefined => {
|
||||
// slice_type: []User — element field is the element type
|
||||
if (typeNode.type === 'slice_type' || typeNode.type === 'array_type') {
|
||||
const elemNode = typeNode.childForFieldName('element');
|
||||
|
|
@ -213,8 +213,14 @@ const extractGoElementTypeFromTypeNode = (typeNode: SyntaxNode): string | undefi
|
|||
const valueNode = typeNode.childForFieldName('value') ?? typeNode.lastNamedChild;
|
||||
if (valueNode) return extractSimpleTypeName(valueNode);
|
||||
}
|
||||
// generic_type: Go 1.18+ generics (e.g., MySlice[User], Cache[string, User])
|
||||
// Use position-aware arg selection: 'first' for keys, 'last' for values.
|
||||
if (typeNode.type === 'generic_type') {
|
||||
const args = extractGenericTypeArgs(typeNode);
|
||||
if (args.length >= 1) return pos === 'first' ? args[0] : args[args.length - 1];
|
||||
}
|
||||
// Fallback: text-based extraction ([]User → User, User[] → User)
|
||||
return extractElementTypeFromString(typeNode.text);
|
||||
return extractElementTypeFromString(typeNode.text, pos);
|
||||
};
|
||||
|
||||
/** Check if a Go type node represents a channel type. Used to determine
|
||||
|
|
@ -242,7 +248,7 @@ const isChannelType = (
|
|||
* name field: identifier (the parameter name)
|
||||
* type field: the type node (slice_type for []User)
|
||||
*/
|
||||
const findGoParamElementType = (iterableName: string, startNode: SyntaxNode): string | undefined => {
|
||||
const findGoParamElementType = (iterableName: string, startNode: SyntaxNode, pos: TypeArgPosition = 'last'): string | undefined => {
|
||||
let current: SyntaxNode | null = startNode.parent;
|
||||
while (current) {
|
||||
if (GO_FUNCTION_NODE_TYPES.has(current.type)) {
|
||||
|
|
@ -255,7 +261,7 @@ const findGoParamElementType = (iterableName: string, startNode: SyntaxNode): st
|
|||
const nameNode = paramDecl.childForFieldName('name');
|
||||
if (nameNode?.text === iterableName) {
|
||||
const typeNode = paramDecl.childForFieldName('type');
|
||||
if (typeNode) return extractGoElementTypeFromTypeNode(typeNode);
|
||||
if (typeNode) return extractGoElementTypeFromTypeNode(typeNode, pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -304,9 +310,12 @@ const extractForLoopBinding: ForLoopExtractor = (
|
|||
if (!rightNode || rightNode.type !== 'identifier') return;
|
||||
const iterableName = rightNode.text;
|
||||
|
||||
const containerTypeName = scopeEnv.get(iterableName);
|
||||
const typeArgPos = methodToTypeArgPosition(undefined, containerTypeName);
|
||||
const elementType = resolveIterableElementType(
|
||||
iterableName, node, scopeEnv, declarationTypeNodes, scope,
|
||||
extractGoElementTypeFromTypeNode, findGoParamElementType,
|
||||
typeArgPos,
|
||||
);
|
||||
if (!elementType) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -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, extractGenericTypeArgs, resolveIterableElementType } from './shared.js';
|
||||
import { extractSimpleTypeName, extractVarName, findChildByType, extractGenericTypeArgs, resolveIterableElementType, methodToTypeArgPosition, type TypeArgPosition } from './shared.js';
|
||||
|
||||
// ── Java ──────────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -91,10 +91,10 @@ const JAVA_FOR_LOOP_NODE_TYPES: ReadonlySet<string> = new Set([
|
|||
|
||||
/** Extract element type from a Java type annotation AST node.
|
||||
* Handles generic_type (List<User>), array_type (User[]). */
|
||||
const extractJavaElementTypeFromTypeNode = (typeNode: SyntaxNode): string | undefined => {
|
||||
const extractJavaElementTypeFromTypeNode = (typeNode: SyntaxNode, pos: TypeArgPosition = 'last'): string | undefined => {
|
||||
if (typeNode.type === 'generic_type') {
|
||||
const args = extractGenericTypeArgs(typeNode);
|
||||
if (args.length >= 1) return args[0];
|
||||
if (args.length >= 1) return pos === 'first' ? args[0] : args[args.length - 1];
|
||||
}
|
||||
if (typeNode.type === 'array_type') {
|
||||
const elemNode = typeNode.firstNamedChild;
|
||||
|
|
@ -104,7 +104,7 @@ const extractJavaElementTypeFromTypeNode = (typeNode: SyntaxNode): string | unde
|
|||
};
|
||||
|
||||
/** Walk up from a for-each to the enclosing method_declaration and search parameters. */
|
||||
const findJavaParamElementType = (iterableName: string, startNode: SyntaxNode): string | undefined => {
|
||||
const findJavaParamElementType = (iterableName: string, startNode: SyntaxNode, pos: TypeArgPosition = 'last'): string | undefined => {
|
||||
let current: SyntaxNode | null = startNode.parent;
|
||||
while (current) {
|
||||
if (current.type === 'method_declaration' || current.type === 'constructor_declaration') {
|
||||
|
|
@ -116,7 +116,7 @@ const findJavaParamElementType = (iterableName: string, startNode: SyntaxNode):
|
|||
const nameNode = param.childForFieldName('name');
|
||||
if (nameNode?.text !== iterableName) continue;
|
||||
const typeNode = param.childForFieldName('type');
|
||||
if (typeNode) return extractJavaElementTypeFromTypeNode(typeNode);
|
||||
if (typeNode) return extractJavaElementTypeFromTypeNode(typeNode, pos);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
|
@ -149,12 +149,27 @@ const extractJavaForLoopBinding: ForLoopExtractor = (
|
|||
|
||||
// Tier 1c: var — resolve from iterable's container type
|
||||
const iterableNode = node.childForFieldName('value');
|
||||
if (!iterableNode || iterableNode.type !== 'identifier') return;
|
||||
const iterableName = iterableNode.text;
|
||||
if (!iterableNode) return;
|
||||
|
||||
let iterableName: string | undefined;
|
||||
let methodName: string | undefined;
|
||||
if (iterableNode.type === 'identifier') {
|
||||
iterableName = iterableNode.text;
|
||||
} else if (iterableNode.type === 'method_invocation') {
|
||||
// data.keySet() → method_invocation > object: identifier + name: identifier
|
||||
const obj = iterableNode.childForFieldName('object');
|
||||
const name = iterableNode.childForFieldName('name');
|
||||
if (obj?.type === 'identifier') iterableName = obj.text;
|
||||
if (name) methodName = name.text;
|
||||
}
|
||||
if (!iterableName) return;
|
||||
|
||||
const containerTypeName = scopeEnv.get(iterableName);
|
||||
const typeArgPos = methodToTypeArgPosition(methodName, containerTypeName);
|
||||
const elementType = resolveIterableElementType(
|
||||
iterableName, node, scopeEnv, declarationTypeNodes, scope,
|
||||
extractJavaElementTypeFromTypeNode, findJavaParamElementType,
|
||||
typeArgPos,
|
||||
);
|
||||
if (elementType) scopeEnv.set(varName, elementType);
|
||||
};
|
||||
|
|
@ -343,16 +358,18 @@ const KOTLIN_FOR_LOOP_NODE_TYPES: ReadonlySet<string> = new Set([
|
|||
/** 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 => {
|
||||
const extractKotlinElementTypeFromTypeNode = (typeNode: SyntaxNode, pos: TypeArgPosition = 'last'): 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;
|
||||
const targetArg = pos === 'first'
|
||||
? argsNode.namedChild(0)
|
||||
: argsNode.namedChild(argsNode.namedChildCount - 1);
|
||||
if (!targetArg) return undefined;
|
||||
// Kotlin wraps type args in type_projection — unwrap to get the inner type
|
||||
const inner = firstArg.type === 'type_projection'
|
||||
? firstArg.firstNamedChild
|
||||
: firstArg;
|
||||
const inner = targetArg.type === 'type_projection'
|
||||
? targetArg.firstNamedChild
|
||||
: targetArg;
|
||||
if (inner) return extractSimpleTypeName(inner);
|
||||
}
|
||||
}
|
||||
|
|
@ -361,7 +378,7 @@ const extractKotlinElementTypeFromTypeNode = (typeNode: SyntaxNode): string | un
|
|||
|
||||
/** 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 => {
|
||||
const findKotlinParamElementType = (iterableName: string, startNode: SyntaxNode, pos: TypeArgPosition = 'last'): string | undefined => {
|
||||
let current: SyntaxNode | null = startNode.parent;
|
||||
while (current) {
|
||||
if (current.type === 'function_declaration') {
|
||||
|
|
@ -373,7 +390,7 @@ const findKotlinParamElementType = (iterableName: string, startNode: SyntaxNode)
|
|||
const nameNode = findChildByType(param, 'simple_identifier');
|
||||
if (nameNode?.text !== iterableName) continue;
|
||||
const typeNode = findChildByType(param, 'user_type');
|
||||
if (typeNode) return extractKotlinElementTypeFromTypeNode(typeNode);
|
||||
if (typeNode) return extractKotlinElementTypeFromTypeNode(typeNode, pos);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
|
@ -407,23 +424,53 @@ const extractKotlinForLoopBinding: ForLoopExtractor = (
|
|||
}
|
||||
|
||||
// Tier 1c: no annotation — resolve from iterable's container type
|
||||
// Kotlin for-loop children: [variable_declaration, simple_identifier(iterable), control_structure_body]
|
||||
// Kotlin for-loop children: [variable_declaration, iterable_expr, control_structure_body]
|
||||
// The iterable is the second named child of the for_statement (after variable_declaration)
|
||||
let iterableName: string | undefined;
|
||||
let methodName: 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') {
|
||||
if (!foundVarDecl || !child) continue;
|
||||
if (child.type === 'simple_identifier') {
|
||||
iterableName = child.text;
|
||||
break;
|
||||
}
|
||||
if (child.type === 'navigation_expression') {
|
||||
// data.keys → navigation_expression > simple_identifier(data) + navigation_suffix > simple_identifier(keys)
|
||||
const obj = child.firstNamedChild;
|
||||
if (obj?.type === 'simple_identifier') iterableName = obj.text;
|
||||
const suffix = findChildByType(child, 'navigation_suffix');
|
||||
if (suffix) {
|
||||
const prop = findChildByType(suffix, 'simple_identifier');
|
||||
if (prop) methodName = prop.text;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (child.type === 'call_expression') {
|
||||
// data.values() → call_expression > navigation_expression > simple_identifier + navigation_suffix
|
||||
const callee = child.firstNamedChild;
|
||||
if (callee?.type === 'navigation_expression') {
|
||||
const obj = callee.firstNamedChild;
|
||||
if (obj?.type === 'simple_identifier') iterableName = obj.text;
|
||||
const suffix = findChildByType(callee, 'navigation_suffix');
|
||||
if (suffix) {
|
||||
const prop = findChildByType(suffix, 'simple_identifier');
|
||||
if (prop) methodName = prop.text;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!iterableName) return;
|
||||
|
||||
const containerTypeName = scopeEnv.get(iterableName);
|
||||
const typeArgPos = methodToTypeArgPosition(methodName, containerTypeName);
|
||||
const elementType = resolveIterableElementType(
|
||||
iterableName, node, scopeEnv, declarationTypeNodes, scope,
|
||||
extractKotlinElementTypeFromTypeNode, findKotlinParamElementType,
|
||||
typeArgPos,
|
||||
);
|
||||
if (elementType) scopeEnv.set(varName, elementType);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { SyntaxNode } from '../utils.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, ReturnTypeExtractor, PendingAssignmentExtractor } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName, extractCalleeName } from './shared.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, ReturnTypeExtractor, PendingAssignmentExtractor, ForLoopExtractor } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName, extractCalleeName, resolveIterableElementType } from './shared.js';
|
||||
|
||||
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
|
||||
'assignment_expression', // For constructor inference: $x = new User()
|
||||
|
|
@ -190,8 +190,12 @@ const extractParameter: ParameterExtractor = (node: SyntaxNode, env: Map<string,
|
|||
|
||||
if (!nameNode || !typeNode) return;
|
||||
const varName = extractVarName(nameNode);
|
||||
if (!varName) return;
|
||||
// Don't overwrite PHPDoc-derived types (e.g. @param User[] $users → User)
|
||||
// with the less-specific AST type annotation (e.g. array).
|
||||
if (env.has(varName)) return;
|
||||
const typeName = extractSimpleTypeName(typeNode);
|
||||
if (varName && typeName) env.set(varName, typeName);
|
||||
if (typeName) env.set(varName, typeName);
|
||||
};
|
||||
|
||||
/** PHP: $x = SomeFactory() or $x = $this->getUser() — bind variable to call return type */
|
||||
|
|
@ -259,12 +263,124 @@ const extractPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) =>
|
|||
return { lhs, rhs };
|
||||
};
|
||||
|
||||
const FOR_LOOP_NODE_TYPES: ReadonlySet<string> = new Set([
|
||||
'foreach_statement',
|
||||
]);
|
||||
|
||||
/** Extract element type from a PHP type annotation AST node.
|
||||
* PHP has limited AST-level container types — `array` is a primitive_type with no generic args.
|
||||
* Named types (e.g., `Collection`) are returned as-is (container descriptor lookup handles them). */
|
||||
const extractPhpElementTypeFromTypeNode = (_typeNode: SyntaxNode): string | undefined => {
|
||||
// PHP AST type nodes don't carry generic parameters (array<User> is PHPDoc-only).
|
||||
// primitive_type 'array' and named_type 'Collection' don't encode element types.
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** Walk up from a foreach to the enclosing function and search parameter type annotations.
|
||||
* PHP parameter type hints are limited (array, ClassName) — this extracts element type when possible. */
|
||||
const findPhpParamElementType = (iterableName: string, startNode: SyntaxNode): string | undefined => {
|
||||
let current: SyntaxNode | null = startNode.parent;
|
||||
while (current) {
|
||||
if (current.type === 'method_declaration' || current.type === 'function_definition') {
|
||||
const paramsNode = current.childForFieldName('parameters');
|
||||
if (paramsNode) {
|
||||
for (let i = 0; i < paramsNode.namedChildCount; i++) {
|
||||
const param = paramsNode.namedChild(i);
|
||||
if (!param || param.type !== 'simple_parameter') continue;
|
||||
const nameNode = param.childForFieldName('name');
|
||||
if (nameNode?.text !== iterableName) continue;
|
||||
const typeNode = param.childForFieldName('type');
|
||||
if (typeNode) return extractPhpElementTypeFromTypeNode(typeNode);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* PHP: foreach ($users as $user) — extract loop variable binding.
|
||||
*
|
||||
* AST structure (from tree-sitter-php grammar):
|
||||
* foreach_statement — no named fields for iterable/value (only 'body')
|
||||
* children[0]: expression (iterable, e.g. $users)
|
||||
* children[1]: expression (simple value) OR pair ($key => $value)
|
||||
* pair children: expression (key), expression (value)
|
||||
*
|
||||
* PHP's PHPDoc @param normalizes `User[]` → `User` in the env, so the iterable's
|
||||
* stored type IS the element type. We first try resolveIterableElementType (for
|
||||
* constructor-binding cases that retain container types), then fall back to direct
|
||||
* scopeEnv lookup (for PHPDoc-normalized types).
|
||||
*/
|
||||
const extractForLoopBinding: ForLoopExtractor = (
|
||||
node: SyntaxNode,
|
||||
scopeEnv: Map<string, string>,
|
||||
declarationTypeNodes: ReadonlyMap<string, SyntaxNode>,
|
||||
scope: string,
|
||||
): void => {
|
||||
if (node.type !== 'foreach_statement') return;
|
||||
|
||||
// Collect non-body named children: first is the iterable, second is value or pair
|
||||
const children: SyntaxNode[] = [];
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (child && child !== node.childForFieldName('body')) {
|
||||
children.push(child);
|
||||
}
|
||||
}
|
||||
if (children.length < 2) return;
|
||||
|
||||
const iterableNode = children[0];
|
||||
const valueOrPair = children[1];
|
||||
|
||||
// Determine the loop variable node
|
||||
let loopVarNode: SyntaxNode;
|
||||
if (valueOrPair.type === 'pair') {
|
||||
// $key => $value — the value is the last named child of the pair
|
||||
const lastChild = valueOrPair.namedChild(valueOrPair.namedChildCount - 1);
|
||||
if (!lastChild) return;
|
||||
// Handle by_ref: foreach ($arr as $k => &$v)
|
||||
loopVarNode = lastChild.type === 'by_ref' ? (lastChild.firstNamedChild ?? lastChild) : lastChild;
|
||||
} else {
|
||||
// Simple: foreach ($users as $user) or foreach ($users as &$user)
|
||||
loopVarNode = valueOrPair.type === 'by_ref' ? (valueOrPair.firstNamedChild ?? valueOrPair) : valueOrPair;
|
||||
}
|
||||
|
||||
const varName = extractVarName(loopVarNode);
|
||||
if (!varName) return;
|
||||
|
||||
// Get iterable variable name (PHP vars include $ prefix)
|
||||
const iterableName = iterableNode.type === 'variable_name' ? iterableNode.text : undefined;
|
||||
if (!iterableName) return;
|
||||
|
||||
// Strategy A: try resolveIterableElementType (handles constructor-binding container types)
|
||||
const elementType = resolveIterableElementType(
|
||||
iterableName, node, scopeEnv, declarationTypeNodes, scope,
|
||||
extractPhpElementTypeFromTypeNode, findPhpParamElementType,
|
||||
);
|
||||
if (elementType) {
|
||||
scopeEnv.set(varName, elementType);
|
||||
return;
|
||||
}
|
||||
|
||||
// Strategy B: direct scopeEnv lookup — PHP normalizePhpType strips User[] → User,
|
||||
// so the iterable's stored type is already the element type from PHPDoc annotations.
|
||||
const iterableType = scopeEnv.get(iterableName);
|
||||
if (iterableType) {
|
||||
scopeEnv.set(varName, iterableType);
|
||||
}
|
||||
};
|
||||
|
||||
export const typeConfig: LanguageTypeConfig = {
|
||||
declarationNodeTypes: DECLARATION_NODE_TYPES,
|
||||
forLoopNodeTypes: FOR_LOOP_NODE_TYPES,
|
||||
extractDeclaration,
|
||||
extractParameter,
|
||||
extractInitializer,
|
||||
scanConstructorBinding,
|
||||
extractReturnType,
|
||||
extractForLoopBinding,
|
||||
extractPendingAssignment,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, ReturnTypeExtractor, PendingAssignmentExtractor } from './types.js';
|
||||
import { extractRubyConstructorAssignment, extractSimpleTypeName } from './shared.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, ReturnTypeExtractor, PendingAssignmentExtractor, ForLoopExtractor } from './types.js';
|
||||
import { extractRubyConstructorAssignment, extractSimpleTypeName, extractElementTypeFromString, extractVarName, resolveIterableElementType } from './shared.js';
|
||||
import type { SyntaxNode } from '../utils.js';
|
||||
|
||||
/**
|
||||
|
|
@ -261,6 +261,120 @@ const scanConstructorBinding: ConstructorBindingScanner = (node) => {
|
|||
return { varName: left.text, calleeName };
|
||||
};
|
||||
|
||||
/** Ruby method node types that carry a parameter list. */
|
||||
const RUBY_METHOD_NODE_TYPES = new Set(['method', 'singleton_method']);
|
||||
|
||||
const FOR_LOOP_NODE_TYPES: ReadonlySet<string> = new Set(['for']);
|
||||
|
||||
/**
|
||||
* Collect raw YARD @param type strings from comment nodes preceding a method.
|
||||
* Unlike collectYardParams which returns simplified type names, this returns the
|
||||
* raw bracket content (e.g., "Array<User>" not "Array") for element type extraction.
|
||||
*/
|
||||
const collectYardRawParams = (methodNode: SyntaxNode): Map<string, string> => {
|
||||
const params = new Map<string, string>();
|
||||
const commentTexts: string[] = [];
|
||||
|
||||
const collectComments = (startNode: SyntaxNode): void => {
|
||||
let sibling = startNode.previousSibling;
|
||||
while (sibling) {
|
||||
if (sibling.type === 'comment') {
|
||||
commentTexts.unshift(sibling.text);
|
||||
} else if (sibling.isNamed) {
|
||||
break;
|
||||
}
|
||||
sibling = sibling.previousSibling;
|
||||
}
|
||||
};
|
||||
|
||||
collectComments(methodNode);
|
||||
if (commentTexts.length === 0 && methodNode.parent?.type === 'body_statement') {
|
||||
collectComments(methodNode.parent);
|
||||
}
|
||||
|
||||
const commentBlock = commentTexts.join('\n');
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
YARD_PARAM_RE.lastIndex = 0;
|
||||
while ((match = YARD_PARAM_RE.exec(commentBlock)) !== null) {
|
||||
params.set(match[1], match[2]);
|
||||
}
|
||||
YARD_PARAM_ALT_RE.lastIndex = 0;
|
||||
while ((match = YARD_PARAM_ALT_RE.exec(commentBlock)) !== null) {
|
||||
if (!params.has(match[2])) params.set(match[2], match[1]);
|
||||
}
|
||||
|
||||
return params;
|
||||
};
|
||||
|
||||
/**
|
||||
* Walk up the AST from a for-statement to find the enclosing method,
|
||||
* then search its YARD @param annotations for one named `iterableName`.
|
||||
* Returns the element type extracted from the raw YARD type string.
|
||||
*
|
||||
* Example: `@param users [Array<User>]` → extracts "User" from "Array<User>".
|
||||
*/
|
||||
const findRubyParamElementType = (iterableName: string, startNode: SyntaxNode): string | undefined => {
|
||||
let current: SyntaxNode | null = startNode.parent;
|
||||
while (current) {
|
||||
if (RUBY_METHOD_NODE_TYPES.has(current.type)) {
|
||||
const rawParams = collectYardRawParams(current);
|
||||
const rawType = rawParams.get(iterableName);
|
||||
if (rawType) return extractElementTypeFromString(rawType);
|
||||
break;
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Ruby: for user in users ... end
|
||||
*
|
||||
* tree-sitter-ruby `for` node structure:
|
||||
* pattern field: the loop variable (identifier)
|
||||
* value field: `in` node whose child is the iterable expression
|
||||
*
|
||||
* Tier 1c: resolves the element type via:
|
||||
* 1. scopeEnv string — extractElementTypeFromString on the stored type
|
||||
* 2. AST walk — walks up to the enclosing method's YARD @param to read Array<User> directly
|
||||
*
|
||||
* Ruby has no static types on loop variables, so this mainly works when the
|
||||
* iterable has a YARD-annotated container type (e.g., `@param users [Array<User>]`).
|
||||
*/
|
||||
const extractForLoopBinding: ForLoopExtractor = (
|
||||
node: SyntaxNode,
|
||||
scopeEnv: Map<string, string>,
|
||||
declarationTypeNodes: ReadonlyMap<string, SyntaxNode>,
|
||||
scope: string,
|
||||
): void => {
|
||||
if (node.type !== 'for') return;
|
||||
|
||||
// The loop variable is the `pattern` field (identifier).
|
||||
const patternNode = node.childForFieldName('pattern');
|
||||
if (!patternNode) return;
|
||||
const loopVarName = extractVarName(patternNode);
|
||||
if (!loopVarName) return;
|
||||
|
||||
// The iterable is inside the `value` field which is an `in` node wrapping the expression.
|
||||
const inNode = node.childForFieldName('value');
|
||||
if (!inNode) return;
|
||||
const iterableNode = inNode.firstNamedChild;
|
||||
if (!iterableNode || iterableNode.type !== 'identifier') return;
|
||||
const iterableName = iterableNode.text;
|
||||
|
||||
// Ruby has no extractFromTypeNode (no AST type annotations), pass a no-op.
|
||||
const noopExtractFromTypeNode = (): string | undefined => undefined;
|
||||
|
||||
const elementType = resolveIterableElementType(
|
||||
iterableName, node, scopeEnv, declarationTypeNodes, scope,
|
||||
noopExtractFromTypeNode, findRubyParamElementType,
|
||||
);
|
||||
if (!elementType) return;
|
||||
|
||||
scopeEnv.set(loopVarName, elementType);
|
||||
};
|
||||
|
||||
/**
|
||||
* Ruby: alias_user = user → assignment with left/right identifier fields.
|
||||
* Only handles plain identifier RHS (not calls, not literals).
|
||||
|
|
@ -279,10 +393,12 @@ const extractPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) =>
|
|||
|
||||
export const typeConfig: LanguageTypeConfig = {
|
||||
declarationNodeTypes: DECLARATION_NODE_TYPES,
|
||||
forLoopNodeTypes: FOR_LOOP_NODE_TYPES,
|
||||
extractDeclaration,
|
||||
extractParameter,
|
||||
extractInitializer,
|
||||
scanConstructorBinding,
|
||||
extractReturnType,
|
||||
extractForLoopBinding,
|
||||
extractPendingAssignment,
|
||||
};
|
||||
|
|
|
|||
10
gitnexus/test/fixtures/lang-resolution/cpp-range-for/Repo.h
vendored
Normal file
10
gitnexus/test/fixtures/lang-resolution/cpp-range-for/Repo.h
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
#pragma once
|
||||
#include <string>
|
||||
|
||||
class Repo {
|
||||
public:
|
||||
Repo(const std::string& name) : name_(name) {}
|
||||
void save() {}
|
||||
private:
|
||||
std::string name_;
|
||||
};
|
||||
|
|
@ -1,8 +1,15 @@
|
|||
#include "user.h"
|
||||
#include "User.h"
|
||||
#include "Repo.h"
|
||||
#include <vector>
|
||||
|
||||
void processUsers(std::vector<User> users) {
|
||||
for (User& user : users) {
|
||||
void processUsers(const std::vector<User>& users) {
|
||||
for (auto& user : users) {
|
||||
user.save();
|
||||
}
|
||||
}
|
||||
|
||||
void processRepos(const std::vector<Repo>& repos) {
|
||||
for (const auto& repo : repos) {
|
||||
repo.save();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
#pragma once
|
||||
#include <string>
|
||||
|
||||
class User {
|
||||
public:
|
||||
bool save() { return true; }
|
||||
User(const std::string& name) : name_(name) {}
|
||||
void save() {}
|
||||
private:
|
||||
std::string name_;
|
||||
};
|
||||
|
|
|
|||
15
gitnexus/test/fixtures/lang-resolution/csharp-dictionary-keys-values/App.cs
vendored
Normal file
15
gitnexus/test/fixtures/lang-resolution/csharp-dictionary-keys-values/App.cs
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
public class App {
|
||||
public void ProcessValues(Dictionary<string, User> data) {
|
||||
foreach (var user in data.Values) {
|
||||
user.Save();
|
||||
}
|
||||
}
|
||||
|
||||
public void ProcessList(List<User> users) {
|
||||
foreach (var user in users) {
|
||||
user.Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
4
gitnexus/test/fixtures/lang-resolution/csharp-dictionary-keys-values/Repo.cs
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/csharp-dictionary-keys-values/Repo.cs
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
public class Repo {
|
||||
public string Name { get; set; }
|
||||
public void Save() {}
|
||||
}
|
||||
4
gitnexus/test/fixtures/lang-resolution/csharp-dictionary-keys-values/User.cs
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/csharp-dictionary-keys-values/User.cs
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
public class User {
|
||||
public string Name { get; set; }
|
||||
public void Save() {}
|
||||
}
|
||||
18
gitnexus/test/fixtures/lang-resolution/java-map-keys-values/src/App.java
vendored
Normal file
18
gitnexus/test/fixtures/lang-resolution/java-map-keys-values/src/App.java
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
package src;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
|
||||
public class App {
|
||||
public void processValues(Map<String, User> data) {
|
||||
for (var user : data.values()) {
|
||||
user.save();
|
||||
}
|
||||
}
|
||||
|
||||
public void processList(List<User> users) {
|
||||
for (var user : users) {
|
||||
user.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/java-map-keys-values/src/Repo.java
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/java-map-keys-values/src/Repo.java
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package src;
|
||||
|
||||
public class Repo {
|
||||
private String name;
|
||||
public Repo(String name) { this.name = name; }
|
||||
public void save() {}
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/java-map-keys-values/src/User.java
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/java-map-keys-values/src/User.java
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package src;
|
||||
|
||||
public class User {
|
||||
private String name;
|
||||
public User(String name) { this.name = name; }
|
||||
public void save() {}
|
||||
}
|
||||
11
gitnexus/test/fixtures/lang-resolution/kotlin-map-keys-values/src/App.kt
vendored
Normal file
11
gitnexus/test/fixtures/lang-resolution/kotlin-map-keys-values/src/App.kt
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
fun processValues(data: HashMap<String, User>) {
|
||||
for (user in data.values) {
|
||||
user.save()
|
||||
}
|
||||
}
|
||||
|
||||
fun processList(users: List<User>) {
|
||||
for (user in users) {
|
||||
user.save()
|
||||
}
|
||||
}
|
||||
3
gitnexus/test/fixtures/lang-resolution/kotlin-map-keys-values/src/Repo.kt
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/kotlin-map-keys-values/src/Repo.kt
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
class Repo(val name: String) {
|
||||
fun save() {}
|
||||
}
|
||||
3
gitnexus/test/fixtures/lang-resolution/kotlin-map-keys-values/src/User.kt
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/kotlin-map-keys-values/src/User.kt
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
class User(val name: String) {
|
||||
fun save() {}
|
||||
}
|
||||
12
gitnexus/test/fixtures/lang-resolution/php-foreach-loop/App.php
vendored
Normal file
12
gitnexus/test/fixtures/lang-resolution/php-foreach-loop/App.php
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
require_once 'User.php';
|
||||
|
||||
class App {
|
||||
/** @param User[] $users */
|
||||
public function processUsers(array $users): void {
|
||||
foreach ($users as $user) {
|
||||
$user->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
11
gitnexus/test/fixtures/lang-resolution/php-foreach-loop/Repo.php
vendored
Normal file
11
gitnexus/test/fixtures/lang-resolution/php-foreach-loop/Repo.php
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
class Repo {
|
||||
public string $name;
|
||||
|
||||
public function __construct(string $name) {
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
public function save(): void {}
|
||||
}
|
||||
11
gitnexus/test/fixtures/lang-resolution/php-foreach-loop/User.php
vendored
Normal file
11
gitnexus/test/fixtures/lang-resolution/php-foreach-loop/User.php
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
class User {
|
||||
public string $name;
|
||||
|
||||
public function __construct(string $name) {
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
public function save(): void {}
|
||||
}
|
||||
8
gitnexus/test/fixtures/lang-resolution/ruby-for-in-loop/app.rb
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/ruby-for-in-loop/app.rb
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
require_relative 'user'
|
||||
|
||||
# @param users [Array<User>]
|
||||
def process_users(users)
|
||||
for user in users
|
||||
user.save
|
||||
end
|
||||
end
|
||||
8
gitnexus/test/fixtures/lang-resolution/ruby-for-in-loop/repo.rb
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/ruby-for-in-loop/repo.rb
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
class Repo
|
||||
def initialize(name)
|
||||
@name = name
|
||||
end
|
||||
|
||||
def save
|
||||
end
|
||||
end
|
||||
8
gitnexus/test/fixtures/lang-resolution/ruby-for-in-loop/user.rb
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/ruby-for-in-loop/user.rb
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
class User
|
||||
def initialize(name)
|
||||
@name = name
|
||||
end
|
||||
|
||||
def save
|
||||
end
|
||||
end
|
||||
|
|
@ -446,10 +446,10 @@ describe('C++ scoped brace-init resolution (ns::Type{})', () => {
|
|||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Range-for with explicit type: for (User& user : users) { user.save(); }
|
||||
// C++ range-based for: for (auto& user : users) — Tier 1c
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('C++ range-for explicit type resolution', () => {
|
||||
describe('C++ range-based for loop resolution', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
|
|
@ -459,22 +459,33 @@ describe('C++ range-for explicit type resolution', () => {
|
|||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects User class and save method', () => {
|
||||
it('detects User and Repo classes with save methods', () => {
|
||||
expect(getNodesByLabel(result, 'Class')).toContain('User');
|
||||
expect(getNodesByLabel(result, 'Method')).toContain('save');
|
||||
expect(getNodesByLabel(result, 'Class')).toContain('Repo');
|
||||
});
|
||||
|
||||
it('resolves user.save() inside range-for to User.save via explicit type', () => {
|
||||
it('resolves user.save() in range-for to User#save', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c => c.target === 'save' && c.targetFilePath === 'user.h');
|
||||
expect(saveCall).toBeDefined();
|
||||
expect(saveCall!.source).toBe('processUsers');
|
||||
const userSave = calls.find(c =>
|
||||
c.target === 'save' && c.source === 'processUsers' && c.targetFilePath?.includes('User'),
|
||||
);
|
||||
expect(userSave).toBeDefined();
|
||||
});
|
||||
|
||||
it('emits HAS_METHOD edge from User to save', () => {
|
||||
const hasMethod = getRelationships(result, 'HAS_METHOD');
|
||||
const edge = hasMethod.find(e => e.source === 'User' && e.target === 'save');
|
||||
expect(edge).toBeDefined();
|
||||
it('resolves repo.save() in const auto& range-for to Repo#save', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const repoSave = calls.find(c =>
|
||||
c.target === 'save' && c.source === 'processRepos' && c.targetFilePath?.includes('Repo'),
|
||||
);
|
||||
expect(repoSave).toBeDefined();
|
||||
});
|
||||
|
||||
it('does NOT cross-resolve user.save() to Repo#save (negative)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const wrongSave = calls.find(c =>
|
||||
c.target === 'save' && c.source === 'processUsers' && c.targetFilePath?.includes('Repo'),
|
||||
);
|
||||
expect(wrongSave).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1077,3 +1077,38 @@ describe('C# switch pattern type resolution', () => {
|
|||
expect(repoSave).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// C# Dictionary .Values foreach — member_access_expression resolution
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('C# Dictionary .Values foreach resolution', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'csharp-dictionary-keys-values'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects User class with Save method', () => {
|
||||
expect(getNodesByLabel(result, 'Class')).toContain('User');
|
||||
});
|
||||
|
||||
it('resolves user.Save() via Dictionary.Values to User#Save', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const userSave = calls.find(c =>
|
||||
c.target === 'Save' && c.source === 'ProcessValues' && c.targetFilePath?.includes('User'),
|
||||
);
|
||||
expect(userSave).toBeDefined();
|
||||
});
|
||||
|
||||
it('does NOT resolve user.Save() to Repo#Save (negative)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const wrongSave = calls.find(c =>
|
||||
c.target === 'Save' && c.source === 'ProcessValues' && c.targetFilePath?.includes('Repo'),
|
||||
);
|
||||
expect(wrongSave).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -918,3 +918,46 @@ describe('Java enum static method call resolution (Phase 5 review fix)', () => {
|
|||
expect(labelCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Java Map .values() for-loop — method-aware type arg resolution
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Java Map .values() for-loop resolution', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'java-map-keys-values'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects User class with save method', () => {
|
||||
expect(getNodesByLabel(result, 'Class')).toContain('User');
|
||||
});
|
||||
|
||||
it('resolves user.save() via Map.values() to User#save', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const userSave = calls.find(c =>
|
||||
c.target === 'save' && c.source === 'processValues' && c.targetFilePath?.includes('User'),
|
||||
);
|
||||
expect(userSave).toBeDefined();
|
||||
});
|
||||
|
||||
it('does NOT resolve user.save() to Repo#save (negative)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const wrongSave = calls.find(c =>
|
||||
c.target === 'save' && c.source === 'processValues' && c.targetFilePath?.includes('Repo'),
|
||||
);
|
||||
expect(wrongSave).toBeUndefined();
|
||||
});
|
||||
|
||||
it('resolves user.save() via List iteration to User#save', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const userSave = calls.find(c =>
|
||||
c.target === 'save' && c.source === 'processList' && c.targetFilePath?.includes('User'),
|
||||
);
|
||||
expect(userSave).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -913,3 +913,46 @@ describe('Kotlin unannotated for-loop type resolution (Tier 1c)', () => {
|
|||
expect(wrong).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Kotlin HashMap .values navigation_expression resolution
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Kotlin HashMap .values for-loop resolution', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'kotlin-map-keys-values'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects User class with save function', () => {
|
||||
expect(getNodesByLabel(result, 'Class')).toContain('User');
|
||||
});
|
||||
|
||||
it('resolves user.save() via HashMap.values to User#save', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const userSave = calls.find(c =>
|
||||
c.target === 'save' && c.source === 'processValues' && c.targetFilePath?.includes('User'),
|
||||
);
|
||||
expect(userSave).toBeDefined();
|
||||
});
|
||||
|
||||
it('does NOT resolve user.save() to Repo#save (negative)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const wrongSave = calls.find(c =>
|
||||
c.target === 'save' && c.source === 'processValues' && c.targetFilePath?.includes('Repo'),
|
||||
);
|
||||
expect(wrongSave).toBeUndefined();
|
||||
});
|
||||
|
||||
it('resolves user.save() via List iteration to User#save', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const userSave = calls.find(c =>
|
||||
c.target === 'save' && c.source === 'processList' && c.targetFilePath?.includes('User'),
|
||||
);
|
||||
expect(userSave).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -989,3 +989,38 @@ describe('PHP assignment chain propagation', () => {
|
|||
expect(userSave!.targetFilePath).not.toBe(repoSave!.targetFilePath);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PHP foreach ($users as $user) — Tier 1c
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('PHP foreach loop resolution', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'php-foreach-loop'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects User class with save method', () => {
|
||||
expect(getNodesByLabel(result, 'Class')).toContain('User');
|
||||
});
|
||||
|
||||
it('resolves $user->save() in foreach to User#save', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const userSave = calls.find(c =>
|
||||
c.target === 'save' && c.source === 'processUsers' && c.targetFilePath?.includes('User'),
|
||||
);
|
||||
expect(userSave).toBeDefined();
|
||||
});
|
||||
|
||||
it('does NOT resolve $user->save() to Repo#save (negative)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const wrongSave = calls.find(c =>
|
||||
c.target === 'save' && c.source === 'processUsers' && c.targetFilePath?.includes('Repo'),
|
||||
);
|
||||
expect(wrongSave).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -811,3 +811,38 @@ describe('Ruby chained method call resolution (Phase 5 review fix)', () => {
|
|||
expect(repoSave).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Ruby for-in loop: for user in users — YARD @param resolution
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Ruby for-in loop resolution', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'ruby-for-in-loop'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects User class with save method', () => {
|
||||
expect(getNodesByLabel(result, 'Class')).toContain('User');
|
||||
});
|
||||
|
||||
it('resolves user.save in for-in to User#save', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const userSave = calls.find(c =>
|
||||
c.target === 'save' && c.source === 'process_users' && c.targetFilePath?.includes('user'),
|
||||
);
|
||||
expect(userSave).toBeDefined();
|
||||
});
|
||||
|
||||
it('does NOT resolve user.save to Repo#save (negative)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const wrongSave = calls.find(c =>
|
||||
c.target === 'save' && c.source === 'process_users' && c.targetFilePath?.includes('repo'),
|
||||
);
|
||||
expect(wrongSave).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue