gitnexus 1.4.5 → 1.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/eval-server.js +13 -5
- package/dist/cli/index.js +0 -0
- package/dist/cli/tool.d.ts +3 -2
- package/dist/cli/tool.js +48 -13
- package/dist/core/ingestion/call-processor.d.ts +0 -1
- package/dist/core/ingestion/call-processor.js +1 -132
- package/dist/core/ingestion/parsing-processor.js +5 -2
- package/dist/core/ingestion/symbol-table.d.ts +6 -0
- package/dist/core/ingestion/symbol-table.js +21 -1
- package/dist/core/ingestion/type-env.js +62 -10
- package/dist/core/ingestion/type-extractors/c-cpp.js +2 -2
- package/dist/core/ingestion/type-extractors/csharp.js +21 -7
- package/dist/core/ingestion/type-extractors/go.js +41 -10
- package/dist/core/ingestion/type-extractors/jvm.js +47 -20
- package/dist/core/ingestion/type-extractors/php.js +142 -4
- package/dist/core/ingestion/type-extractors/python.js +21 -7
- package/dist/core/ingestion/type-extractors/ruby.js +2 -2
- package/dist/core/ingestion/type-extractors/rust.js +25 -12
- package/dist/core/ingestion/type-extractors/shared.d.ts +1 -0
- package/dist/core/ingestion/type-extractors/shared.js +133 -1
- package/dist/core/ingestion/type-extractors/types.d.ts +44 -12
- package/dist/core/ingestion/type-extractors/typescript.js +22 -8
- package/dist/core/ingestion/workers/parse-worker.js +5 -2
- package/dist/mcp/local/local-backend.d.ts +1 -0
- package/dist/mcp/local/local-backend.js +23 -1
- package/hooks/claude/gitnexus-hook.cjs +0 -0
- package/hooks/claude/pre-tool-use.sh +0 -0
- package/hooks/claude/session-start.sh +0 -0
- package/package.json +3 -2
- package/scripts/patch-tree-sitter-swift.cjs +0 -0
|
@@ -296,7 +296,7 @@ const findGoParamElementType = (iterableName, startNode, pos = 'last') => {
|
|
|
296
296
|
* For `_, user := range users`, the loop variable is the second identifier in
|
|
297
297
|
* the `left` expression_list (index is discarded, value is the element).
|
|
298
298
|
*/
|
|
299
|
-
const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
|
|
299
|
+
const extractForLoopBinding = (node, { scopeEnv, declarationTypeNodes, scope, returnTypeLookup }) => {
|
|
300
300
|
if (node.type !== 'for_statement')
|
|
301
301
|
return;
|
|
302
302
|
// Find the range_clause child — this distinguishes range loops from other for forms.
|
|
@@ -313,6 +313,7 @@ const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
|
|
|
313
313
|
// The iterable is the `right` field of the range_clause.
|
|
314
314
|
const rightNode = rangeClause.childForFieldName('right');
|
|
315
315
|
let iterableName;
|
|
316
|
+
let callExprElementType;
|
|
316
317
|
if (rightNode?.type === 'identifier') {
|
|
317
318
|
iterableName = rightNode.text;
|
|
318
319
|
}
|
|
@@ -321,11 +322,35 @@ const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
|
|
|
321
322
|
if (field)
|
|
322
323
|
iterableName = field.text;
|
|
323
324
|
}
|
|
324
|
-
if (
|
|
325
|
+
else if (rightNode?.type === 'call_expression') {
|
|
326
|
+
// Range over a call result: `for _, v := range getItems()` or `for _, v := range repo.All()`
|
|
327
|
+
const funcNode = rightNode.childForFieldName('function');
|
|
328
|
+
let callee;
|
|
329
|
+
if (funcNode?.type === 'identifier') {
|
|
330
|
+
callee = funcNode.text;
|
|
331
|
+
}
|
|
332
|
+
else if (funcNode?.type === 'selector_expression') {
|
|
333
|
+
const field = funcNode.childForFieldName('field');
|
|
334
|
+
if (field)
|
|
335
|
+
callee = field.text;
|
|
336
|
+
}
|
|
337
|
+
if (callee) {
|
|
338
|
+
const rawReturn = returnTypeLookup.lookupRawReturnType(callee);
|
|
339
|
+
if (rawReturn)
|
|
340
|
+
callExprElementType = extractElementTypeFromString(rawReturn);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (!iterableName && !callExprElementType)
|
|
325
344
|
return;
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
345
|
+
let elementType;
|
|
346
|
+
if (callExprElementType) {
|
|
347
|
+
elementType = callExprElementType;
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
const containerTypeName = scopeEnv.get(iterableName);
|
|
351
|
+
const typeArgPos = methodToTypeArgPosition(undefined, containerTypeName);
|
|
352
|
+
elementType = resolveIterableElementType(iterableName, node, scopeEnv, declarationTypeNodes, scope, extractGoElementTypeFromTypeNode, findGoParamElementType, typeArgPos);
|
|
353
|
+
}
|
|
329
354
|
if (!elementType)
|
|
330
355
|
return;
|
|
331
356
|
// The loop variable(s) are in the `left` field.
|
|
@@ -343,8 +368,11 @@ const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
|
|
|
343
368
|
loopVarNode = leftNode.namedChild(1);
|
|
344
369
|
}
|
|
345
370
|
else {
|
|
346
|
-
// Single-var in expression_list — yields INDEX for slices/maps, ELEMENT for channels
|
|
347
|
-
|
|
371
|
+
// Single-var in expression_list — yields INDEX for slices/maps, ELEMENT for channels.
|
|
372
|
+
// For call-expression iterables (iterableName undefined), conservative: treat as non-channel.
|
|
373
|
+
// Channels are rarely returned from function calls, and even if they were, skipping here
|
|
374
|
+
// just means we miss a binding rather than create an incorrect one.
|
|
375
|
+
if (iterableName && isChannelType(iterableName, scopeEnv, declarationTypeNodes, scope)) {
|
|
348
376
|
loopVarNode = leftNode.namedChild(0);
|
|
349
377
|
}
|
|
350
378
|
else {
|
|
@@ -354,7 +382,10 @@ const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
|
|
|
354
382
|
}
|
|
355
383
|
else {
|
|
356
384
|
// Plain identifier (single-var form without expression_list)
|
|
357
|
-
|
|
385
|
+
// For call-expression iterables (iterableName undefined), conservative: treat as non-channel.
|
|
386
|
+
// Channels are rarely returned from function calls, and even if they were, skipping here
|
|
387
|
+
// just means we miss a binding rather than create an incorrect one.
|
|
388
|
+
if (iterableName && isChannelType(iterableName, scopeEnv, declarationTypeNodes, scope)) {
|
|
358
389
|
loopVarNode = leftNode;
|
|
359
390
|
}
|
|
360
391
|
else {
|
|
@@ -387,7 +418,7 @@ const extractPendingAssignment = (node, scopeEnv) => {
|
|
|
387
418
|
if (scopeEnv.has(lhs))
|
|
388
419
|
return undefined;
|
|
389
420
|
if (rhsNode.type === 'identifier')
|
|
390
|
-
return { lhs, rhs: rhsNode.text };
|
|
421
|
+
return { kind: 'copy', lhs, rhs: rhsNode.text };
|
|
391
422
|
return undefined;
|
|
392
423
|
}
|
|
393
424
|
if (node.type === 'var_spec' || node.type === 'var_declaration') {
|
|
@@ -420,7 +451,7 @@ const extractPendingAssignment = (node, scopeEnv) => {
|
|
|
420
451
|
}
|
|
421
452
|
const rhsNode = exprList?.firstNamedChild;
|
|
422
453
|
if (rhsNode?.type === 'identifier')
|
|
423
|
-
return { lhs, rhs: rhsNode.text };
|
|
454
|
+
return { kind: 'copy', lhs, rhs: rhsNode.text };
|
|
424
455
|
}
|
|
425
456
|
}
|
|
426
457
|
return undefined;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { extractSimpleTypeName, extractVarName, findChildByType, extractGenericTypeArgs, resolveIterableElementType, methodToTypeArgPosition } from './shared.js';
|
|
1
|
+
import { extractSimpleTypeName, extractVarName, findChildByType, extractGenericTypeArgs, resolveIterableElementType, methodToTypeArgPosition, extractElementTypeFromString } from './shared.js';
|
|
2
2
|
// ── Java ──────────────────────────────────────────────────────────────────
|
|
3
3
|
const JAVA_DECLARATION_NODE_TYPES = new Set([
|
|
4
4
|
'local_variable_declaration',
|
|
@@ -139,7 +139,7 @@ const findJavaParamElementType = (iterableName, startNode, pos = 'last') => {
|
|
|
139
139
|
};
|
|
140
140
|
/** Java: for (User user : users) — extract loop variable binding.
|
|
141
141
|
* Tier 1c: for `for (var user : users)`, resolves element type from iterable. */
|
|
142
|
-
const extractJavaForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
|
|
142
|
+
const extractJavaForLoopBinding = (node, { scopeEnv, declarationTypeNodes, scope, returnTypeLookup }) => {
|
|
143
143
|
const typeNode = node.childForFieldName('type');
|
|
144
144
|
const nameNode = node.childForFieldName('name');
|
|
145
145
|
if (!typeNode || !nameNode)
|
|
@@ -159,6 +159,7 @@ const extractJavaForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope)
|
|
|
159
159
|
return;
|
|
160
160
|
let iterableName;
|
|
161
161
|
let methodName;
|
|
162
|
+
let callExprElementType;
|
|
162
163
|
if (iterableNode.type === 'identifier') {
|
|
163
164
|
iterableName = iterableNode.text;
|
|
164
165
|
}
|
|
@@ -180,14 +181,26 @@ const extractJavaForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope)
|
|
|
180
181
|
if (innerField)
|
|
181
182
|
iterableName = innerField.text;
|
|
182
183
|
}
|
|
184
|
+
else if (!obj && name) {
|
|
185
|
+
// Direct function call: for (var u : getUsers()) — no receiver object
|
|
186
|
+
const rawReturn = returnTypeLookup.lookupRawReturnType(name.text);
|
|
187
|
+
if (rawReturn)
|
|
188
|
+
callExprElementType = extractElementTypeFromString(rawReturn);
|
|
189
|
+
}
|
|
183
190
|
if (name)
|
|
184
191
|
methodName = name.text;
|
|
185
192
|
}
|
|
186
|
-
if (!iterableName)
|
|
193
|
+
if (!iterableName && !callExprElementType)
|
|
187
194
|
return;
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
195
|
+
let elementType;
|
|
196
|
+
if (callExprElementType) {
|
|
197
|
+
elementType = callExprElementType;
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
const containerTypeName = scopeEnv.get(iterableName);
|
|
201
|
+
const typeArgPos = methodToTypeArgPosition(methodName, containerTypeName);
|
|
202
|
+
elementType = resolveIterableElementType(iterableName, node, scopeEnv, declarationTypeNodes, scope, extractJavaElementTypeFromTypeNode, findJavaParamElementType, typeArgPos);
|
|
203
|
+
}
|
|
191
204
|
if (elementType)
|
|
192
205
|
scopeEnv.set(varName, elementType);
|
|
193
206
|
};
|
|
@@ -205,7 +218,7 @@ const extractJavaPendingAssignment = (node, scopeEnv) => {
|
|
|
205
218
|
if (scopeEnv.has(lhs))
|
|
206
219
|
continue;
|
|
207
220
|
if (valueNode.type === 'identifier' || valueNode.type === 'simple_identifier')
|
|
208
|
-
return { lhs, rhs: valueNode.text };
|
|
221
|
+
return { kind: 'copy', lhs, rhs: valueNode.text };
|
|
209
222
|
}
|
|
210
223
|
return undefined;
|
|
211
224
|
};
|
|
@@ -459,7 +472,8 @@ const findKotlinParamElementType = (iterableName, startNode, pos = 'last') => {
|
|
|
459
472
|
};
|
|
460
473
|
/** Kotlin: for (user: User in users) — extract loop variable binding.
|
|
461
474
|
* Tier 1c: for `for (user in users)` without annotation, resolves from iterable. */
|
|
462
|
-
const extractKotlinForLoopBinding = (node,
|
|
475
|
+
const extractKotlinForLoopBinding = (node, ctx) => {
|
|
476
|
+
const { scopeEnv, declarationTypeNodes, scope, returnTypeLookup } = ctx;
|
|
463
477
|
const varDecl = findChildByType(node, 'variable_declaration');
|
|
464
478
|
if (!varDecl)
|
|
465
479
|
return;
|
|
@@ -483,6 +497,7 @@ const extractKotlinForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope
|
|
|
483
497
|
let iterableName;
|
|
484
498
|
let methodName;
|
|
485
499
|
let fallbackIterableName;
|
|
500
|
+
let callExprElementType;
|
|
486
501
|
let foundVarDecl = false;
|
|
487
502
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
488
503
|
const child = node.namedChild(i);
|
|
@@ -528,21 +543,33 @@ const extractKotlinForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope
|
|
|
528
543
|
methodName = prop.text;
|
|
529
544
|
}
|
|
530
545
|
}
|
|
546
|
+
else if (callee?.type === 'simple_identifier') {
|
|
547
|
+
// Direct function call: for (u in getUsers())
|
|
548
|
+
const rawReturn = returnTypeLookup.lookupRawReturnType(callee.text);
|
|
549
|
+
if (rawReturn)
|
|
550
|
+
callExprElementType = extractElementTypeFromString(rawReturn);
|
|
551
|
+
}
|
|
531
552
|
break;
|
|
532
553
|
}
|
|
533
554
|
}
|
|
534
|
-
if (!iterableName)
|
|
555
|
+
if (!iterableName && !callExprElementType)
|
|
535
556
|
return;
|
|
536
|
-
let
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
557
|
+
let elementType;
|
|
558
|
+
if (callExprElementType) {
|
|
559
|
+
elementType = callExprElementType;
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
let containerTypeName = scopeEnv.get(iterableName);
|
|
563
|
+
// Fallback: if object has no type in scope, try the property as the iterable name.
|
|
564
|
+
// Handles patterns like this.users where the property itself is the iterable variable.
|
|
565
|
+
if (!containerTypeName && fallbackIterableName) {
|
|
566
|
+
iterableName = fallbackIterableName;
|
|
567
|
+
methodName = undefined;
|
|
568
|
+
containerTypeName = scopeEnv.get(iterableName);
|
|
569
|
+
}
|
|
570
|
+
const typeArgPos = methodToTypeArgPosition(methodName, containerTypeName);
|
|
571
|
+
elementType = resolveIterableElementType(iterableName, node, scopeEnv, declarationTypeNodes, scope, extractKotlinElementTypeFromTypeNode, findKotlinParamElementType, typeArgPos);
|
|
543
572
|
}
|
|
544
|
-
const typeArgPos = methodToTypeArgPosition(methodName, containerTypeName);
|
|
545
|
-
const elementType = resolveIterableElementType(iterableName, node, scopeEnv, declarationTypeNodes, scope, extractKotlinElementTypeFromTypeNode, findKotlinParamElementType, typeArgPos);
|
|
546
573
|
if (elementType)
|
|
547
574
|
scopeEnv.set(varName, elementType);
|
|
548
575
|
};
|
|
@@ -573,7 +600,7 @@ const extractKotlinPendingAssignment = (node, scopeEnv) => {
|
|
|
573
600
|
continue;
|
|
574
601
|
}
|
|
575
602
|
if (foundEq && child.type === 'simple_identifier') {
|
|
576
|
-
return { lhs, rhs: child.text };
|
|
603
|
+
return { kind: 'copy', lhs, rhs: child.text };
|
|
577
604
|
}
|
|
578
605
|
}
|
|
579
606
|
return undefined;
|
|
@@ -601,7 +628,7 @@ const extractKotlinPendingAssignment = (node, scopeEnv) => {
|
|
|
601
628
|
continue;
|
|
602
629
|
}
|
|
603
630
|
if (foundEq && child.type === 'simple_identifier') {
|
|
604
|
-
return { lhs, rhs: child.text };
|
|
631
|
+
return { kind: 'copy', lhs, rhs: child.text };
|
|
605
632
|
}
|
|
606
633
|
}
|
|
607
634
|
return undefined;
|
|
@@ -81,6 +81,72 @@ const SKIP_NODE_TYPES = new Set(['attribute_list', 'attribute']);
|
|
|
81
81
|
const PHPDOC_PARAM_RE = /@param\s+(\S+)\s+\$(\w+)/g;
|
|
82
82
|
/** Alternate PHPDoc order: `@param $name Type` (name first) */
|
|
83
83
|
const PHPDOC_PARAM_ALT_RE = /@param\s+\$(\w+)\s+(\S+)/g;
|
|
84
|
+
/** Regex to extract PHPDoc @var annotations: `@var Type` */
|
|
85
|
+
const PHPDOC_VAR_RE = /@var\s+(\S+)/;
|
|
86
|
+
/**
|
|
87
|
+
* Extract the element type for a class property from its PHPDoc @var annotation or
|
|
88
|
+
* PHP 7.4+ native type. Walks backward from the property_declaration node to find
|
|
89
|
+
* an immediately preceding comment containing @var.
|
|
90
|
+
*
|
|
91
|
+
* Returns the normalized element type (e.g. User[] → User, Collection<User> → User).
|
|
92
|
+
* Returns undefined when no usable type annotation is found.
|
|
93
|
+
*/
|
|
94
|
+
const extractClassPropertyElementType = (propDecl) => {
|
|
95
|
+
// Strategy 1: PHPDoc @var annotation on a preceding comment sibling
|
|
96
|
+
let sibling = propDecl.previousSibling;
|
|
97
|
+
while (sibling) {
|
|
98
|
+
if (sibling.type === 'comment') {
|
|
99
|
+
const match = PHPDOC_VAR_RE.exec(sibling.text);
|
|
100
|
+
if (match)
|
|
101
|
+
return normalizePhpType(match[1]);
|
|
102
|
+
}
|
|
103
|
+
else if (sibling.isNamed && !SKIP_NODE_TYPES.has(sibling.type)) {
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
sibling = sibling.previousSibling;
|
|
107
|
+
}
|
|
108
|
+
// Strategy 2: PHP 7.4+ native type field — skip generic 'array' since element type is unknown
|
|
109
|
+
const typeNode = propDecl.childForFieldName('type');
|
|
110
|
+
if (!typeNode)
|
|
111
|
+
return undefined;
|
|
112
|
+
const typeName = extractSimpleTypeName(typeNode);
|
|
113
|
+
if (!typeName || typeName === 'array')
|
|
114
|
+
return undefined;
|
|
115
|
+
return typeName;
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* Scan a class body for a property_declaration matching the given property name,
|
|
119
|
+
* and extract its element type. The class body is the `declaration_list` child of
|
|
120
|
+
* a `class_declaration` node.
|
|
121
|
+
*
|
|
122
|
+
* Used as Strategy C in extractForLoopBinding for `$this->property` iterables
|
|
123
|
+
* where Strategy A (resolveIterableElementType) and Strategy B (scopeEnv lookup)
|
|
124
|
+
* both fail to find the type.
|
|
125
|
+
*/
|
|
126
|
+
const findClassPropertyElementType = (propName, classNode) => {
|
|
127
|
+
const declList = classNode.childForFieldName('body')
|
|
128
|
+
?? (classNode.namedChild(classNode.namedChildCount - 1)?.type === 'declaration_list'
|
|
129
|
+
? classNode.namedChild(classNode.namedChildCount - 1)
|
|
130
|
+
: null); // fallback: last named child, only if it's a declaration_list
|
|
131
|
+
if (!declList)
|
|
132
|
+
return undefined;
|
|
133
|
+
for (let i = 0; i < declList.namedChildCount; i++) {
|
|
134
|
+
const child = declList.namedChild(i);
|
|
135
|
+
if (child?.type !== 'property_declaration')
|
|
136
|
+
continue;
|
|
137
|
+
// Check if any property_element has a variable_name matching '$propName'
|
|
138
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
139
|
+
const elem = child.namedChild(j);
|
|
140
|
+
if (elem?.type !== 'property_element')
|
|
141
|
+
continue;
|
|
142
|
+
const varNameNode = elem.firstNamedChild; // variable_name node
|
|
143
|
+
if (varNameNode?.text === '$' + propName) {
|
|
144
|
+
return extractClassPropertyElementType(child);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return undefined;
|
|
149
|
+
};
|
|
84
150
|
/**
|
|
85
151
|
* Collect PHPDoc @param type bindings from comment nodes preceding a method/function.
|
|
86
152
|
* Returns a map of paramName → typeName (without $ prefix).
|
|
@@ -253,9 +319,36 @@ const scanConstructorBinding = (node) => {
|
|
|
253
319
|
};
|
|
254
320
|
/** Regex to extract PHPDoc @return annotations: `@return User` */
|
|
255
321
|
const PHPDOC_RETURN_RE = /@return\s+(\S+)/;
|
|
322
|
+
/**
|
|
323
|
+
* Normalize a PHPDoc return type for storage in the SymbolTable.
|
|
324
|
+
* Unlike normalizePhpType (which strips User[] → User for scopeEnv), this preserves
|
|
325
|
+
* array notation so lookupRawReturnType can extract element types for for-loop resolution.
|
|
326
|
+
* \App\Models\User[] → User[]
|
|
327
|
+
* ?User → User
|
|
328
|
+
* Collection<User> → Collection<User> (preserved for extractElementTypeFromString)
|
|
329
|
+
*/
|
|
330
|
+
const normalizePhpReturnType = (raw) => {
|
|
331
|
+
// Strip nullable prefix: ?User[] → User[]
|
|
332
|
+
let type = raw.startsWith('?') ? raw.slice(1) : raw;
|
|
333
|
+
// Strip union with null/false/void: User[]|null → User[]
|
|
334
|
+
const parts = type.split('|').filter(p => p !== 'null' && p !== 'false' && p !== 'void' && p !== 'mixed');
|
|
335
|
+
if (parts.length !== 1)
|
|
336
|
+
return undefined;
|
|
337
|
+
type = parts[0];
|
|
338
|
+
// Strip namespace: \App\Models\User[] → User[]
|
|
339
|
+
const segments = type.split('\\');
|
|
340
|
+
type = segments[segments.length - 1];
|
|
341
|
+
// Skip uninformative types
|
|
342
|
+
if (type === 'mixed' || type === 'void' || type === 'self' || type === 'static' || type === 'object' || type === 'array')
|
|
343
|
+
return undefined;
|
|
344
|
+
if (/^\w+(\[\])?$/.test(type) || /^\w+\s*</.test(type))
|
|
345
|
+
return type;
|
|
346
|
+
return undefined;
|
|
347
|
+
};
|
|
256
348
|
/**
|
|
257
349
|
* Extract return type from PHPDoc `@return Type` annotation preceding a method.
|
|
258
350
|
* Walks backwards through preceding siblings looking for comment nodes.
|
|
351
|
+
* Preserves array notation (e.g., User[]) for for-loop element type extraction.
|
|
259
352
|
*/
|
|
260
353
|
const extractReturnType = (node) => {
|
|
261
354
|
let sibling = node.previousSibling;
|
|
@@ -263,7 +356,7 @@ const extractReturnType = (node) => {
|
|
|
263
356
|
if (sibling.type === 'comment') {
|
|
264
357
|
const match = PHPDOC_RETURN_RE.exec(sibling.text);
|
|
265
358
|
if (match)
|
|
266
|
-
return
|
|
359
|
+
return normalizePhpReturnType(match[1]);
|
|
267
360
|
}
|
|
268
361
|
else if (sibling.isNamed && !SKIP_NODE_TYPES.has(sibling.type))
|
|
269
362
|
break;
|
|
@@ -286,7 +379,7 @@ const extractPendingAssignment = (node, scopeEnv) => {
|
|
|
286
379
|
const rhs = right.text;
|
|
287
380
|
if (!lhs || !rhs || scopeEnv.has(lhs))
|
|
288
381
|
return undefined;
|
|
289
|
-
return { lhs, rhs };
|
|
382
|
+
return { kind: 'copy', lhs, rhs };
|
|
290
383
|
};
|
|
291
384
|
const FOR_LOOP_NODE_TYPES = new Set([
|
|
292
385
|
'foreach_statement',
|
|
@@ -339,7 +432,7 @@ const findPhpParamElementType = (iterableName, startNode) => {
|
|
|
339
432
|
* constructor-binding cases that retain container types), then fall back to direct
|
|
340
433
|
* scopeEnv lookup (for PHPDoc-normalized types).
|
|
341
434
|
*/
|
|
342
|
-
const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
|
|
435
|
+
const extractForLoopBinding = (node, { scopeEnv, declarationTypeNodes, scope, returnTypeLookup }) => {
|
|
343
436
|
if (node.type !== 'foreach_statement')
|
|
344
437
|
return;
|
|
345
438
|
// Collect non-body named children: first is the iterable, second is value or pair
|
|
@@ -373,6 +466,7 @@ const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
|
|
|
373
466
|
return;
|
|
374
467
|
// Get iterable variable name (PHP vars include $ prefix)
|
|
375
468
|
let iterableName;
|
|
469
|
+
let callExprElementType;
|
|
376
470
|
if (iterableNode.type === 'variable_name') {
|
|
377
471
|
iterableName = iterableNode.text;
|
|
378
472
|
}
|
|
@@ -383,8 +477,31 @@ const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
|
|
|
383
477
|
if (name)
|
|
384
478
|
iterableName = '$' + name.text;
|
|
385
479
|
}
|
|
386
|
-
if (
|
|
480
|
+
else if (iterableNode?.type === 'function_call_expression') {
|
|
481
|
+
// foreach (getUsers() as $user) — resolve via return type lookup
|
|
482
|
+
const calleeName = extractCalleeName(iterableNode);
|
|
483
|
+
if (calleeName) {
|
|
484
|
+
const rawReturn = returnTypeLookup.lookupRawReturnType(calleeName);
|
|
485
|
+
if (rawReturn)
|
|
486
|
+
callExprElementType = extractElementTypeFromString(rawReturn);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
else if (iterableNode?.type === 'member_call_expression') {
|
|
490
|
+
// foreach ($this->getUsers() as $user) — resolve via return type lookup
|
|
491
|
+
const methodName = iterableNode.childForFieldName('name');
|
|
492
|
+
if (methodName) {
|
|
493
|
+
const rawReturn = returnTypeLookup.lookupRawReturnType(methodName.text);
|
|
494
|
+
if (rawReturn)
|
|
495
|
+
callExprElementType = extractElementTypeFromString(rawReturn);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (!iterableName && !callExprElementType)
|
|
499
|
+
return;
|
|
500
|
+
// If we resolved the element type from a call expression, bind and return early
|
|
501
|
+
if (callExprElementType) {
|
|
502
|
+
scopeEnv.set(varName, callExprElementType);
|
|
387
503
|
return;
|
|
504
|
+
}
|
|
388
505
|
// Strategy A: try resolveIterableElementType (handles constructor-binding container types)
|
|
389
506
|
const elementType = resolveIterableElementType(iterableName, node, scopeEnv, declarationTypeNodes, scope, extractPhpElementTypeFromTypeNode, findPhpParamElementType, undefined);
|
|
390
507
|
if (elementType) {
|
|
@@ -396,6 +513,27 @@ const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
|
|
|
396
513
|
const iterableType = scopeEnv.get(iterableName);
|
|
397
514
|
if (iterableType) {
|
|
398
515
|
scopeEnv.set(varName, iterableType);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
// Strategy C: $this->property — scan the enclosing class body for the property
|
|
519
|
+
// declaration and extract its element type from @var PHPDoc or native type.
|
|
520
|
+
// This handles the common PHP pattern where the property type is declared on the
|
|
521
|
+
// class body (/** @var User[] */ private $users) but the foreach is in a method
|
|
522
|
+
// whose scopeEnv does not contain the property type.
|
|
523
|
+
if (iterableNode?.type === 'member_access_expression') {
|
|
524
|
+
const obj = iterableNode.childForFieldName('object');
|
|
525
|
+
if (obj?.text === '$this') {
|
|
526
|
+
const nameNode = iterableNode.childForFieldName('name');
|
|
527
|
+
const propName = nameNode?.text;
|
|
528
|
+
if (propName) {
|
|
529
|
+
const classNode = findEnclosingClass(iterableNode);
|
|
530
|
+
if (classNode) {
|
|
531
|
+
const elementType = findClassPropertyElementType(propName, classNode);
|
|
532
|
+
if (elementType)
|
|
533
|
+
scopeEnv.set(varName, elementType);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
399
537
|
}
|
|
400
538
|
};
|
|
401
539
|
export const typeConfig = {
|
|
@@ -239,13 +239,14 @@ const findPyParamElementType = (iterableName, startNode, pos = 'last') => {
|
|
|
239
239
|
* 2. scopeEnv string — extractElementTypeFromString on the stored type
|
|
240
240
|
* 3. AST walk — walks up to the enclosing function's parameters to read List[User] directly
|
|
241
241
|
*/
|
|
242
|
-
const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
|
|
242
|
+
const extractForLoopBinding = (node, { scopeEnv, declarationTypeNodes, scope, returnTypeLookup }) => {
|
|
243
243
|
if (node.type !== 'for_statement')
|
|
244
244
|
return;
|
|
245
|
-
// The iterable is the `right` field — may be identifier or call
|
|
245
|
+
// The iterable is the `right` field — may be identifier, attribute, or call.
|
|
246
246
|
const rightNode = node.childForFieldName('right');
|
|
247
247
|
let iterableName;
|
|
248
248
|
let methodName;
|
|
249
|
+
let callExprElementType;
|
|
249
250
|
if (rightNode?.type === 'identifier') {
|
|
250
251
|
iterableName = rightNode.text;
|
|
251
252
|
}
|
|
@@ -256,6 +257,7 @@ const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
|
|
|
256
257
|
}
|
|
257
258
|
else if (rightNode?.type === 'call') {
|
|
258
259
|
// data.items() → call > function: attribute > identifier('data') + identifier('items')
|
|
260
|
+
// get_users() → call > function: identifier (Phase 7.3 — return-type path)
|
|
259
261
|
const fn = rightNode.childForFieldName('function');
|
|
260
262
|
if (fn?.type === 'attribute') {
|
|
261
263
|
const obj = fn.firstNamedChild;
|
|
@@ -266,12 +268,24 @@ const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
|
|
|
266
268
|
if (method?.type === 'identifier' && method !== obj)
|
|
267
269
|
methodName = method.text;
|
|
268
270
|
}
|
|
271
|
+
else if (fn?.type === 'identifier') {
|
|
272
|
+
// Direct function call: for user in get_users()
|
|
273
|
+
const rawReturn = returnTypeLookup.lookupRawReturnType(fn.text);
|
|
274
|
+
if (rawReturn)
|
|
275
|
+
callExprElementType = extractElementTypeFromString(rawReturn);
|
|
276
|
+
}
|
|
269
277
|
}
|
|
270
|
-
if (!iterableName)
|
|
278
|
+
if (!iterableName && !callExprElementType)
|
|
271
279
|
return;
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
280
|
+
let elementType;
|
|
281
|
+
if (callExprElementType) {
|
|
282
|
+
elementType = callExprElementType;
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
const containerTypeName = scopeEnv.get(iterableName);
|
|
286
|
+
const typeArgPos = methodToTypeArgPosition(methodName, containerTypeName);
|
|
287
|
+
elementType = resolveIterableElementType(iterableName, node, scopeEnv, declarationTypeNodes, scope, extractPyElementTypeFromAnnotation, findPyParamElementType, typeArgPos);
|
|
288
|
+
}
|
|
275
289
|
if (!elementType)
|
|
276
290
|
return;
|
|
277
291
|
// The loop variable is the `left` field — identifier or pattern_list.
|
|
@@ -312,7 +326,7 @@ const extractPendingAssignment = (node, scopeEnv) => {
|
|
|
312
326
|
if (!lhs || scopeEnv.has(lhs))
|
|
313
327
|
return undefined;
|
|
314
328
|
if (right.type === 'identifier')
|
|
315
|
-
return { lhs, rhs: right.text };
|
|
329
|
+
return { kind: 'copy', lhs, rhs: right.text };
|
|
316
330
|
return undefined;
|
|
317
331
|
};
|
|
318
332
|
/**
|
|
@@ -324,7 +324,7 @@ const findRubyParamElementType = (iterableName, startNode) => {
|
|
|
324
324
|
* Ruby has no static types on loop variables, so this mainly works when the
|
|
325
325
|
* iterable has a YARD-annotated container type (e.g., `@param users [Array<User>]`).
|
|
326
326
|
*/
|
|
327
|
-
const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
|
|
327
|
+
const extractForLoopBinding = (node, { scopeEnv, declarationTypeNodes, scope }) => {
|
|
328
328
|
if (node.type !== 'for')
|
|
329
329
|
return;
|
|
330
330
|
// The loop variable is the `pattern` field (identifier).
|
|
@@ -374,7 +374,7 @@ const extractPendingAssignment = (node, scopeEnv) => {
|
|
|
374
374
|
const rhsNode = node.childForFieldName('right');
|
|
375
375
|
if (!rhsNode || rhsNode.type !== 'identifier')
|
|
376
376
|
return undefined;
|
|
377
|
-
return { lhs: varName, rhs: rhsNode.text };
|
|
377
|
+
return { kind: 'copy', lhs: varName, rhs: rhsNode.text };
|
|
378
378
|
};
|
|
379
379
|
export const typeConfig = {
|
|
380
380
|
declarationNodeTypes: DECLARATION_NODE_TYPES,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { extractSimpleTypeName, extractVarName, hasTypeAnnotation, unwrapAwait, extractGenericTypeArgs, resolveIterableElementType, methodToTypeArgPosition } from './shared.js';
|
|
1
|
+
import { extractSimpleTypeName, extractVarName, hasTypeAnnotation, unwrapAwait, extractGenericTypeArgs, resolveIterableElementType, methodToTypeArgPosition, extractElementTypeFromString } from './shared.js';
|
|
2
2
|
const DECLARATION_NODE_TYPES = new Set([
|
|
3
3
|
'let_declaration',
|
|
4
4
|
'let_condition',
|
|
@@ -210,7 +210,7 @@ const extractPendingAssignment = (node, scopeEnv) => {
|
|
|
210
210
|
if (!lhs || scopeEnv.has(lhs))
|
|
211
211
|
return undefined;
|
|
212
212
|
if (value.type === 'identifier')
|
|
213
|
-
return { lhs, rhs: value.text };
|
|
213
|
+
return { kind: 'copy', lhs, rhs: value.text };
|
|
214
214
|
return undefined;
|
|
215
215
|
};
|
|
216
216
|
/**
|
|
@@ -375,7 +375,7 @@ const findRustParamElementType = (iterableName, startNode, pos = 'last') => {
|
|
|
375
375
|
};
|
|
376
376
|
/** Rust: for user in &users where users has a known container type.
|
|
377
377
|
* Unwraps reference_expression (&users, &mut users) to get the iterable name. */
|
|
378
|
-
const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
|
|
378
|
+
const extractForLoopBinding = (node, { scopeEnv, declarationTypeNodes, scope, returnTypeLookup }) => {
|
|
379
379
|
if (node.type !== 'for_expression')
|
|
380
380
|
return;
|
|
381
381
|
const patternNode = node.childForFieldName('pattern');
|
|
@@ -385,6 +385,7 @@ const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
|
|
|
385
385
|
// Extract iterable name + method — may be &users, users, or users.iter()/keys()/values()
|
|
386
386
|
let iterableName;
|
|
387
387
|
let methodName;
|
|
388
|
+
let callExprElementType;
|
|
388
389
|
if (valueNode.type === 'reference_expression') {
|
|
389
390
|
const inner = valueNode.lastNamedChild;
|
|
390
391
|
if (inner?.type === 'identifier')
|
|
@@ -399,23 +400,35 @@ const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
|
|
|
399
400
|
iterableName = prop.text;
|
|
400
401
|
}
|
|
401
402
|
else if (valueNode.type === 'call_expression') {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
const obj =
|
|
403
|
+
const funcExpr = valueNode.childForFieldName('function');
|
|
404
|
+
if (funcExpr?.type === 'field_expression') {
|
|
405
|
+
// users.iter() → field_expression > identifier + field_identifier
|
|
406
|
+
const obj = funcExpr.firstNamedChild;
|
|
406
407
|
if (obj?.type === 'identifier')
|
|
407
408
|
iterableName = obj.text;
|
|
408
409
|
// Extract method name: iter, keys, values, into_iter, etc.
|
|
409
|
-
const field =
|
|
410
|
+
const field = funcExpr.lastNamedChild;
|
|
410
411
|
if (field?.type === 'field_identifier')
|
|
411
412
|
methodName = field.text;
|
|
412
413
|
}
|
|
414
|
+
else if (funcExpr?.type === 'identifier') {
|
|
415
|
+
// Direct function call: for user in get_users()
|
|
416
|
+
const rawReturn = returnTypeLookup.lookupRawReturnType(funcExpr.text);
|
|
417
|
+
if (rawReturn)
|
|
418
|
+
callExprElementType = extractElementTypeFromString(rawReturn);
|
|
419
|
+
}
|
|
413
420
|
}
|
|
414
|
-
if (!iterableName)
|
|
421
|
+
if (!iterableName && !callExprElementType)
|
|
415
422
|
return;
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
423
|
+
let elementType;
|
|
424
|
+
if (callExprElementType) {
|
|
425
|
+
elementType = callExprElementType;
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
const containerTypeName = scopeEnv.get(iterableName);
|
|
429
|
+
const typeArgPos = methodToTypeArgPosition(methodName, containerTypeName);
|
|
430
|
+
elementType = resolveIterableElementType(iterableName, node, scopeEnv, declarationTypeNodes, scope, extractRustElementTypeFromTypeNode, findRustParamElementType, typeArgPos);
|
|
431
|
+
}
|
|
419
432
|
if (!elementType)
|
|
420
433
|
return;
|
|
421
434
|
const loopVarName = extractVarName(patternNode);
|
|
@@ -129,4 +129,5 @@ export declare const findChildByType: (node: SyntaxNode, type: string) => Syntax
|
|
|
129
129
|
* Returns undefined when the extracted type is not a simple word.
|
|
130
130
|
*/
|
|
131
131
|
export declare function extractElementTypeFromString(typeStr: string, pos?: TypeArgPosition): string | undefined;
|
|
132
|
+
export declare const extractReturnTypeName: (raw: string, depth?: number) => string | undefined;
|
|
132
133
|
export {};
|