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.
Files changed (30) hide show
  1. package/dist/cli/eval-server.js +13 -5
  2. package/dist/cli/index.js +0 -0
  3. package/dist/cli/tool.d.ts +3 -2
  4. package/dist/cli/tool.js +48 -13
  5. package/dist/core/ingestion/call-processor.d.ts +0 -1
  6. package/dist/core/ingestion/call-processor.js +1 -132
  7. package/dist/core/ingestion/parsing-processor.js +5 -2
  8. package/dist/core/ingestion/symbol-table.d.ts +6 -0
  9. package/dist/core/ingestion/symbol-table.js +21 -1
  10. package/dist/core/ingestion/type-env.js +62 -10
  11. package/dist/core/ingestion/type-extractors/c-cpp.js +2 -2
  12. package/dist/core/ingestion/type-extractors/csharp.js +21 -7
  13. package/dist/core/ingestion/type-extractors/go.js +41 -10
  14. package/dist/core/ingestion/type-extractors/jvm.js +47 -20
  15. package/dist/core/ingestion/type-extractors/php.js +142 -4
  16. package/dist/core/ingestion/type-extractors/python.js +21 -7
  17. package/dist/core/ingestion/type-extractors/ruby.js +2 -2
  18. package/dist/core/ingestion/type-extractors/rust.js +25 -12
  19. package/dist/core/ingestion/type-extractors/shared.d.ts +1 -0
  20. package/dist/core/ingestion/type-extractors/shared.js +133 -1
  21. package/dist/core/ingestion/type-extractors/types.d.ts +44 -12
  22. package/dist/core/ingestion/type-extractors/typescript.js +22 -8
  23. package/dist/core/ingestion/workers/parse-worker.js +5 -2
  24. package/dist/mcp/local/local-backend.d.ts +1 -0
  25. package/dist/mcp/local/local-backend.js +23 -1
  26. package/hooks/claude/gitnexus-hook.cjs +0 -0
  27. package/hooks/claude/pre-tool-use.sh +0 -0
  28. package/hooks/claude/session-start.sh +0 -0
  29. package/package.json +3 -2
  30. 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 (!iterableName)
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
- const containerTypeName = scopeEnv.get(iterableName);
327
- const typeArgPos = methodToTypeArgPosition(undefined, containerTypeName);
328
- const elementType = resolveIterableElementType(iterableName, node, scopeEnv, declarationTypeNodes, scope, extractGoElementTypeFromTypeNode, findGoParamElementType, typeArgPos);
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
- if (isChannelType(iterableName, scopeEnv, declarationTypeNodes, scope)) {
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
- if (isChannelType(iterableName, scopeEnv, declarationTypeNodes, scope)) {
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
- const containerTypeName = scopeEnv.get(iterableName);
189
- const typeArgPos = methodToTypeArgPosition(methodName, containerTypeName);
190
- const elementType = resolveIterableElementType(iterableName, node, scopeEnv, declarationTypeNodes, scope, extractJavaElementTypeFromTypeNode, findJavaParamElementType, typeArgPos);
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, scopeEnv, declarationTypeNodes, scope) => {
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 containerTypeName = scopeEnv.get(iterableName);
537
- // Fallback: if object has no type in scope, try the property as the iterable name.
538
- // Handles patterns like this.users where the property itself is the iterable variable.
539
- if (!containerTypeName && fallbackIterableName) {
540
- iterableName = fallbackIterableName;
541
- methodName = undefined;
542
- containerTypeName = scopeEnv.get(iterableName);
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 normalizePhpType(match[1]);
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 (!iterableName)
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 (data.items()/keys()/values()).
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
- const containerTypeName = scopeEnv.get(iterableName);
273
- const typeArgPos = methodToTypeArgPosition(methodName, containerTypeName);
274
- const elementType = resolveIterableElementType(iterableName, node, scopeEnv, declarationTypeNodes, scope, extractPyElementTypeFromAnnotation, findPyParamElementType, typeArgPos);
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
- // users.iter() → call_expression > function: field_expression > identifier + field_identifier
403
- const fieldExpr = valueNode.childForFieldName('function');
404
- if (fieldExpr?.type === 'field_expression') {
405
- const obj = fieldExpr.firstNamedChild;
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 = fieldExpr.lastNamedChild;
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
- const containerTypeName = scopeEnv.get(iterableName);
417
- const typeArgPos = methodToTypeArgPosition(methodName, containerTypeName);
418
- const elementType = resolveIterableElementType(iterableName, node, scopeEnv, declarationTypeNodes, scope, extractRustElementTypeFromTypeNode, findRustParamElementType, typeArgPos);
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 {};