gitnexus 1.4.5 → 1.4.7

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 (49) 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/graph/types.d.ts +2 -2
  6. package/dist/core/ingestion/call-processor.d.ts +7 -2
  7. package/dist/core/ingestion/call-processor.js +308 -235
  8. package/dist/core/ingestion/call-routing.d.ts +17 -2
  9. package/dist/core/ingestion/call-routing.js +21 -0
  10. package/dist/core/ingestion/parsing-processor.d.ts +2 -1
  11. package/dist/core/ingestion/parsing-processor.js +37 -8
  12. package/dist/core/ingestion/pipeline.js +5 -1
  13. package/dist/core/ingestion/symbol-table.d.ts +19 -3
  14. package/dist/core/ingestion/symbol-table.js +41 -2
  15. package/dist/core/ingestion/tree-sitter-queries.d.ts +12 -12
  16. package/dist/core/ingestion/tree-sitter-queries.js +200 -0
  17. package/dist/core/ingestion/type-env.js +126 -18
  18. package/dist/core/ingestion/type-extractors/c-cpp.js +28 -3
  19. package/dist/core/ingestion/type-extractors/csharp.js +61 -7
  20. package/dist/core/ingestion/type-extractors/go.js +86 -10
  21. package/dist/core/ingestion/type-extractors/jvm.js +122 -23
  22. package/dist/core/ingestion/type-extractors/php.js +172 -7
  23. package/dist/core/ingestion/type-extractors/python.js +107 -21
  24. package/dist/core/ingestion/type-extractors/ruby.js +18 -3
  25. package/dist/core/ingestion/type-extractors/rust.js +61 -14
  26. package/dist/core/ingestion/type-extractors/shared.d.ts +13 -0
  27. package/dist/core/ingestion/type-extractors/shared.js +243 -4
  28. package/dist/core/ingestion/type-extractors/types.d.ts +57 -12
  29. package/dist/core/ingestion/type-extractors/typescript.js +52 -8
  30. package/dist/core/ingestion/utils.d.ts +25 -0
  31. package/dist/core/ingestion/utils.js +160 -1
  32. package/dist/core/ingestion/workers/parse-worker.d.ts +23 -7
  33. package/dist/core/ingestion/workers/parse-worker.js +73 -28
  34. package/dist/core/lbug/lbug-adapter.d.ts +2 -0
  35. package/dist/core/lbug/lbug-adapter.js +2 -0
  36. package/dist/core/lbug/schema.d.ts +1 -1
  37. package/dist/core/lbug/schema.js +1 -1
  38. package/dist/mcp/core/lbug-adapter.d.ts +22 -0
  39. package/dist/mcp/core/lbug-adapter.js +167 -23
  40. package/dist/mcp/local/local-backend.d.ts +1 -0
  41. package/dist/mcp/local/local-backend.js +25 -3
  42. package/dist/mcp/resources.js +11 -0
  43. package/dist/mcp/server.js +26 -4
  44. package/dist/mcp/tools.js +15 -5
  45. package/hooks/claude/gitnexus-hook.cjs +0 -0
  46. package/hooks/claude/pre-tool-use.sh +0 -0
  47. package/hooks/claude/session-start.sh +0 -0
  48. package/package.json +6 -5
  49. package/scripts/patch-tree-sitter-swift.cjs +0 -0
@@ -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;
@@ -280,13 +373,40 @@ const extractPendingAssignment = (node, scopeEnv) => {
280
373
  const right = node.childForFieldName('right');
281
374
  if (!left || !right)
282
375
  return undefined;
283
- if (left.type !== 'variable_name' || right.type !== 'variable_name')
376
+ if (left.type !== 'variable_name')
284
377
  return undefined;
285
378
  const lhs = left.text;
286
- const rhs = right.text;
287
- if (!lhs || !rhs || scopeEnv.has(lhs))
379
+ if (!lhs || scopeEnv.has(lhs))
288
380
  return undefined;
289
- return { lhs, rhs };
381
+ if (right.type === 'variable_name') {
382
+ const rhs = right.text;
383
+ if (rhs)
384
+ return { kind: 'copy', lhs, rhs };
385
+ }
386
+ // member_access_expression RHS → fieldAccess ($a->field)
387
+ if (right.type === 'member_access_expression') {
388
+ const obj = right.childForFieldName('object');
389
+ const name = right.childForFieldName('name');
390
+ if (obj?.type === 'variable_name' && name) {
391
+ return { kind: 'fieldAccess', lhs, receiver: obj.text, field: name.text };
392
+ }
393
+ }
394
+ // function_call_expression RHS → callResult (bare function calls only)
395
+ if (right.type === 'function_call_expression') {
396
+ const funcNode = right.childForFieldName('function');
397
+ if (funcNode?.type === 'name') {
398
+ return { kind: 'callResult', lhs, callee: funcNode.text };
399
+ }
400
+ }
401
+ // member_call_expression RHS → methodCallResult ($a->method())
402
+ if (right.type === 'member_call_expression') {
403
+ const obj = right.childForFieldName('object');
404
+ const name = right.childForFieldName('name');
405
+ if (obj?.type === 'variable_name' && name) {
406
+ return { kind: 'methodCallResult', lhs, receiver: obj.text, method: name.text };
407
+ }
408
+ }
409
+ return undefined;
290
410
  };
291
411
  const FOR_LOOP_NODE_TYPES = new Set([
292
412
  'foreach_statement',
@@ -339,7 +459,7 @@ const findPhpParamElementType = (iterableName, startNode) => {
339
459
  * constructor-binding cases that retain container types), then fall back to direct
340
460
  * scopeEnv lookup (for PHPDoc-normalized types).
341
461
  */
342
- const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
462
+ const extractForLoopBinding = (node, { scopeEnv, declarationTypeNodes, scope, returnTypeLookup }) => {
343
463
  if (node.type !== 'foreach_statement')
344
464
  return;
345
465
  // Collect non-body named children: first is the iterable, second is value or pair
@@ -373,6 +493,7 @@ const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
373
493
  return;
374
494
  // Get iterable variable name (PHP vars include $ prefix)
375
495
  let iterableName;
496
+ let callExprElementType;
376
497
  if (iterableNode.type === 'variable_name') {
377
498
  iterableName = iterableNode.text;
378
499
  }
@@ -383,8 +504,31 @@ const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
383
504
  if (name)
384
505
  iterableName = '$' + name.text;
385
506
  }
386
- if (!iterableName)
507
+ else if (iterableNode?.type === 'function_call_expression') {
508
+ // foreach (getUsers() as $user) — resolve via return type lookup
509
+ const calleeName = extractCalleeName(iterableNode);
510
+ if (calleeName) {
511
+ const rawReturn = returnTypeLookup.lookupRawReturnType(calleeName);
512
+ if (rawReturn)
513
+ callExprElementType = extractElementTypeFromString(rawReturn);
514
+ }
515
+ }
516
+ else if (iterableNode?.type === 'member_call_expression') {
517
+ // foreach ($this->getUsers() as $user) — resolve via return type lookup
518
+ const methodName = iterableNode.childForFieldName('name');
519
+ if (methodName) {
520
+ const rawReturn = returnTypeLookup.lookupRawReturnType(methodName.text);
521
+ if (rawReturn)
522
+ callExprElementType = extractElementTypeFromString(rawReturn);
523
+ }
524
+ }
525
+ if (!iterableName && !callExprElementType)
526
+ return;
527
+ // If we resolved the element type from a call expression, bind and return early
528
+ if (callExprElementType) {
529
+ scopeEnv.set(varName, callExprElementType);
387
530
  return;
531
+ }
388
532
  // Strategy A: try resolveIterableElementType (handles constructor-binding container types)
389
533
  const elementType = resolveIterableElementType(iterableName, node, scopeEnv, declarationTypeNodes, scope, extractPhpElementTypeFromTypeNode, findPhpParamElementType, undefined);
390
534
  if (elementType) {
@@ -396,6 +540,27 @@ const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
396
540
  const iterableType = scopeEnv.get(iterableName);
397
541
  if (iterableType) {
398
542
  scopeEnv.set(varName, iterableType);
543
+ return;
544
+ }
545
+ // Strategy C: $this->property — scan the enclosing class body for the property
546
+ // declaration and extract its element type from @var PHPDoc or native type.
547
+ // This handles the common PHP pattern where the property type is declared on the
548
+ // class body (/** @var User[] */ private $users) but the foreach is in a method
549
+ // whose scopeEnv does not contain the property type.
550
+ if (iterableNode?.type === 'member_access_expression') {
551
+ const obj = iterableNode.childForFieldName('object');
552
+ if (obj?.text === '$this') {
553
+ const nameNode = iterableNode.childForFieldName('name');
554
+ const propName = nameNode?.text;
555
+ if (propName) {
556
+ const classNode = findEnclosingClass(iterableNode);
557
+ if (classNode) {
558
+ const elementType = findClassPropertyElementType(propName, classNode);
559
+ if (elementType)
560
+ scopeEnv.set(varName, elementType);
561
+ }
562
+ }
563
+ }
399
564
  }
400
565
  };
401
566
  export const typeConfig = {
@@ -61,6 +61,10 @@ const extractParameter = (node, env) => {
61
61
  else {
62
62
  nameNode = node.childForFieldName('name') ?? node.childForFieldName('pattern');
63
63
  typeNode = node.childForFieldName('type');
64
+ // Python typed_parameter: name is a positional child (identifier), not a named field
65
+ if (!nameNode && node.type === 'typed_parameter') {
66
+ nameNode = node.firstNamedChild?.type === 'identifier' ? node.firstNamedChild : null;
67
+ }
64
68
  }
65
69
  if (!nameNode || !typeNode)
66
70
  return;
@@ -229,6 +233,38 @@ const findPyParamElementType = (iterableName, startNode, pos = 'last') => {
229
233
  }
230
234
  return undefined;
231
235
  };
236
+ /**
237
+ * Extracts iterableName and methodName from a call expression like `data.items()`.
238
+ * Returns undefined if the call doesn't match the expected pattern.
239
+ */
240
+ const extractMethodCall = (callNode) => {
241
+ const fn = callNode.childForFieldName('function');
242
+ if (fn?.type !== 'attribute')
243
+ return undefined;
244
+ const obj = fn.firstNamedChild;
245
+ if (obj?.type !== 'identifier')
246
+ return undefined;
247
+ const method = fn.lastNamedChild;
248
+ const methodName = (method?.type === 'identifier' && method !== obj) ? method.text : undefined;
249
+ return { iterableName: obj.text, methodName };
250
+ };
251
+ /**
252
+ * Collects all identifier nodes from a pattern, descending into nested tuple_patterns.
253
+ * For `i, (k, v)` returns [i, k, v]. For `key, value` returns [key, value].
254
+ */
255
+ const collectPatternIdentifiers = (pattern) => {
256
+ const vars = [];
257
+ for (let i = 0; i < pattern.namedChildCount; i++) {
258
+ const child = pattern.namedChild(i);
259
+ if (child?.type === 'identifier') {
260
+ vars.push(child);
261
+ }
262
+ else if (child?.type === 'tuple_pattern') {
263
+ vars.push(...collectPatternIdentifiers(child));
264
+ }
265
+ }
266
+ return vars;
267
+ };
232
268
  /**
233
269
  * Python: for user in users: where users has a known container type annotation.
234
270
  *
@@ -238,14 +274,19 @@ const findPyParamElementType = (iterableName, startNode, pos = 'last') => {
238
274
  * 1. declarationTypeNodes — raw type annotation AST node (covers stored container types)
239
275
  * 2. scopeEnv string — extractElementTypeFromString on the stored type
240
276
  * 3. AST walk — walks up to the enclosing function's parameters to read List[User] directly
277
+ *
278
+ * Also handles `enumerate(iterable)` — unwraps the outer call and skips the integer
279
+ * index variable so the value variable still resolves to the element type.
241
280
  */
242
- const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
281
+ const extractForLoopBinding = (node, { scopeEnv, declarationTypeNodes, scope, returnTypeLookup }) => {
243
282
  if (node.type !== 'for_statement')
244
283
  return;
245
- // The iterable is the `right` field — may be identifier or call (data.items()/keys()/values()).
246
284
  const rightNode = node.childForFieldName('right');
247
285
  let iterableName;
248
286
  let methodName;
287
+ let callExprElementType;
288
+ let isEnumerate = false;
289
+ // Extract iterable info from the `right` field — may be identifier, attribute, or call.
249
290
  if (rightNode?.type === 'identifier') {
250
291
  iterableName = rightNode.text;
251
292
  }
@@ -255,34 +296,56 @@ const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
255
296
  iterableName = prop.text;
256
297
  }
257
298
  else if (rightNode?.type === 'call') {
258
- // data.items() → call > function: attribute > identifier('data') + identifier('items')
259
299
  const fn = rightNode.childForFieldName('function');
260
- if (fn?.type === 'attribute') {
261
- const obj = fn.firstNamedChild;
262
- if (obj?.type === 'identifier')
263
- iterableName = obj.text;
264
- // Extract method name: items, keys, values
265
- const method = fn.lastNamedChild;
266
- if (method?.type === 'identifier' && method !== obj)
267
- methodName = method.text;
300
+ if (fn?.type === 'identifier' && fn.text === 'enumerate') {
301
+ // enumerate(iterable) or enumerate(d.items()) — unwrap to inner iterable.
302
+ isEnumerate = true;
303
+ const innerArg = rightNode.childForFieldName('arguments')?.firstNamedChild;
304
+ if (innerArg?.type === 'identifier') {
305
+ iterableName = innerArg.text;
306
+ }
307
+ else if (innerArg?.type === 'call') {
308
+ const extracted = extractMethodCall(innerArg);
309
+ if (extracted)
310
+ ({ iterableName, methodName } = extracted);
311
+ }
312
+ }
313
+ else if (fn?.type === 'attribute') {
314
+ // data.items() → call > function: attribute > identifier('data') + identifier('items')
315
+ const extracted = extractMethodCall(rightNode);
316
+ if (extracted)
317
+ ({ iterableName, methodName } = extracted);
318
+ }
319
+ else if (fn?.type === 'identifier') {
320
+ // Direct function call: for user in get_users() (Phase 7.3 — return-type path)
321
+ const rawReturn = returnTypeLookup.lookupRawReturnType(fn.text);
322
+ if (rawReturn)
323
+ callExprElementType = extractElementTypeFromString(rawReturn);
268
324
  }
269
325
  }
270
- if (!iterableName)
326
+ if (!iterableName && !callExprElementType)
271
327
  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);
328
+ let elementType;
329
+ if (callExprElementType) {
330
+ elementType = callExprElementType;
331
+ }
332
+ else {
333
+ const containerTypeName = scopeEnv.get(iterableName);
334
+ const typeArgPos = methodToTypeArgPosition(methodName, containerTypeName);
335
+ elementType = resolveIterableElementType(iterableName, node, scopeEnv, declarationTypeNodes, scope, extractPyElementTypeFromAnnotation, findPyParamElementType, typeArgPos);
336
+ }
275
337
  if (!elementType)
276
338
  return;
277
339
  // The loop variable is the `left` field — identifier or pattern_list.
278
340
  const leftNode = node.childForFieldName('left');
279
341
  if (!leftNode)
280
342
  return;
281
- // Handle tuple unpacking: for key, value in data.items()
282
- if (leftNode.type === 'pattern_list') {
283
- const lastChild = leftNode.lastNamedChild;
284
- if (lastChild?.type === 'identifier') {
285
- scopeEnv.set(lastChild.text, elementType);
343
+ if (leftNode.type === 'pattern_list' || leftNode.type === 'tuple_pattern') {
344
+ // Tuple unpacking: `key, value` or `i, (k, v)` or `(k, v)` — bind the last identifier to element type.
345
+ // With enumerate, skip binding if there's only one var (just the index, no value to bind).
346
+ const vars = collectPatternIdentifiers(leftNode);
347
+ if (vars.length > 0 && (!isEnumerate || vars.length > 1)) {
348
+ scopeEnv.set(vars[vars.length - 1].text, elementType);
286
349
  }
287
350
  return;
288
351
  }
@@ -312,7 +375,30 @@ const extractPendingAssignment = (node, scopeEnv) => {
312
375
  if (!lhs || scopeEnv.has(lhs))
313
376
  return undefined;
314
377
  if (right.type === 'identifier')
315
- return { lhs, rhs: right.text };
378
+ return { kind: 'copy', lhs, rhs: right.text };
379
+ // attribute RHS → fieldAccess (a.field)
380
+ if (right.type === 'attribute') {
381
+ const obj = right.firstNamedChild;
382
+ const field = right.lastNamedChild;
383
+ if (obj?.type === 'identifier' && field?.type === 'identifier' && obj !== field) {
384
+ return { kind: 'fieldAccess', lhs, receiver: obj.text, field: field.text };
385
+ }
386
+ }
387
+ // call RHS
388
+ if (right.type === 'call') {
389
+ const funcNode = right.childForFieldName('function');
390
+ if (funcNode?.type === 'identifier') {
391
+ return { kind: 'callResult', lhs, callee: funcNode.text };
392
+ }
393
+ // method call with receiver: call → function: attribute
394
+ if (funcNode?.type === 'attribute') {
395
+ const obj = funcNode.firstNamedChild;
396
+ const method = funcNode.lastNamedChild;
397
+ if (obj?.type === 'identifier' && method?.type === 'identifier' && obj !== method) {
398
+ return { kind: 'methodCallResult', lhs, receiver: obj.text, method: method.text };
399
+ }
400
+ }
401
+ }
316
402
  return undefined;
317
403
  };
318
404
  /**
@@ -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).
@@ -372,9 +372,24 @@ const extractPendingAssignment = (node, scopeEnv) => {
372
372
  if (scopeEnv.has(varName))
373
373
  return undefined;
374
374
  const rhsNode = node.childForFieldName('right');
375
- if (!rhsNode || rhsNode.type !== 'identifier')
375
+ if (!rhsNode)
376
376
  return undefined;
377
- return { lhs: varName, rhs: rhsNode.text };
377
+ if (rhsNode.type === 'identifier')
378
+ return { kind: 'copy', lhs: varName, rhs: rhsNode.text };
379
+ // call/method_call RHS — Ruby uses method calls for both field access and method calls
380
+ if (rhsNode.type === 'call' || rhsNode.type === 'method_call') {
381
+ const methodNode = rhsNode.childForFieldName('method');
382
+ const receiverNode = rhsNode.childForFieldName('receiver');
383
+ if (!receiverNode && methodNode?.type === 'identifier') {
384
+ // No receiver → callResult (bare function call)
385
+ return { kind: 'callResult', lhs: varName, callee: methodNode.text };
386
+ }
387
+ if (receiverNode?.type === 'identifier' && methodNode?.type === 'identifier') {
388
+ // With receiver → methodCallResult (a.method)
389
+ return { kind: 'methodCallResult', lhs: varName, receiver: receiverNode.text, method: methodNode.text };
390
+ }
391
+ }
392
+ return undefined;
378
393
  };
379
394
  export const typeConfig = {
380
395
  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',
@@ -95,7 +95,7 @@ const extractDeclaration = (node, env) => {
95
95
  env.set(varName, typeName);
96
96
  };
97
97
  /** Rust: let x = User::new(), let x = User::default(), or let x = User { ... } */
98
- const extractInitializer = (node, env, _classNames) => {
98
+ const extractInitializer = (node, env, classNames) => {
99
99
  // Skip if there's an explicit type annotation — Tier 0 already handled it
100
100
  if (node.childForFieldName('type') !== null)
101
101
  return;
@@ -119,6 +119,13 @@ const extractInitializer = (node, env, _classNames) => {
119
119
  env.set(varName, typeName);
120
120
  return;
121
121
  }
122
+ // Unit struct instantiation: let svc = UserService; (bare identifier, no braces or call)
123
+ if (value.type === 'identifier' && classNames.has(value.text)) {
124
+ const varName = extractVarName(pattern);
125
+ if (varName)
126
+ env.set(varName, value.text);
127
+ return;
128
+ }
122
129
  if (value.type !== 'call_expression')
123
130
  return;
124
131
  const func = value.childForFieldName('function');
@@ -209,8 +216,35 @@ const extractPendingAssignment = (node, scopeEnv) => {
209
216
  const lhs = extractVarName(pattern);
210
217
  if (!lhs || scopeEnv.has(lhs))
211
218
  return undefined;
212
- if (value.type === 'identifier')
213
- return { lhs, rhs: value.text };
219
+ // Unwrap Rust .await: `let user = get_user().await` call_expression
220
+ const unwrapped = unwrapAwait(value) ?? value;
221
+ if (unwrapped.type === 'identifier')
222
+ return { kind: 'copy', lhs, rhs: unwrapped.text };
223
+ // field_expression RHS → fieldAccess (a.field)
224
+ if (unwrapped.type === 'field_expression') {
225
+ const obj = unwrapped.firstNamedChild;
226
+ const field = unwrapped.lastNamedChild;
227
+ if (obj?.type === 'identifier' && field?.type === 'field_identifier') {
228
+ return { kind: 'fieldAccess', lhs, receiver: obj.text, field: field.text };
229
+ }
230
+ }
231
+ // call_expression RHS → callResult (simple calls only)
232
+ if (unwrapped.type === 'call_expression') {
233
+ const funcNode = unwrapped.childForFieldName('function');
234
+ if (funcNode?.type === 'identifier') {
235
+ return { kind: 'callResult', lhs, callee: funcNode.text };
236
+ }
237
+ }
238
+ // method_call_expression RHS → methodCallResult (receiver.method())
239
+ if (unwrapped.type === 'method_call_expression') {
240
+ const obj = unwrapped.firstNamedChild;
241
+ if (obj?.type === 'identifier') {
242
+ const methodNode = unwrapped.childForFieldName('name') ?? unwrapped.namedChild(1);
243
+ if (methodNode?.type === 'field_identifier') {
244
+ return { kind: 'methodCallResult', lhs, receiver: obj.text, method: methodNode.text };
245
+ }
246
+ }
247
+ }
214
248
  return undefined;
215
249
  };
216
250
  /**
@@ -375,7 +409,7 @@ const findRustParamElementType = (iterableName, startNode, pos = 'last') => {
375
409
  };
376
410
  /** Rust: for user in &users where users has a known container type.
377
411
  * Unwraps reference_expression (&users, &mut users) to get the iterable name. */
378
- const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
412
+ const extractForLoopBinding = (node, { scopeEnv, declarationTypeNodes, scope, returnTypeLookup }) => {
379
413
  if (node.type !== 'for_expression')
380
414
  return;
381
415
  const patternNode = node.childForFieldName('pattern');
@@ -385,6 +419,7 @@ const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
385
419
  // Extract iterable name + method — may be &users, users, or users.iter()/keys()/values()
386
420
  let iterableName;
387
421
  let methodName;
422
+ let callExprElementType;
388
423
  if (valueNode.type === 'reference_expression') {
389
424
  const inner = valueNode.lastNamedChild;
390
425
  if (inner?.type === 'identifier')
@@ -399,23 +434,35 @@ const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
399
434
  iterableName = prop.text;
400
435
  }
401
436
  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;
437
+ const funcExpr = valueNode.childForFieldName('function');
438
+ if (funcExpr?.type === 'field_expression') {
439
+ // users.iter() field_expression > identifier + field_identifier
440
+ const obj = funcExpr.firstNamedChild;
406
441
  if (obj?.type === 'identifier')
407
442
  iterableName = obj.text;
408
443
  // Extract method name: iter, keys, values, into_iter, etc.
409
- const field = fieldExpr.lastNamedChild;
444
+ const field = funcExpr.lastNamedChild;
410
445
  if (field?.type === 'field_identifier')
411
446
  methodName = field.text;
412
447
  }
448
+ else if (funcExpr?.type === 'identifier') {
449
+ // Direct function call: for user in get_users()
450
+ const rawReturn = returnTypeLookup.lookupRawReturnType(funcExpr.text);
451
+ if (rawReturn)
452
+ callExprElementType = extractElementTypeFromString(rawReturn);
453
+ }
413
454
  }
414
- if (!iterableName)
455
+ if (!iterableName && !callExprElementType)
415
456
  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);
457
+ let elementType;
458
+ if (callExprElementType) {
459
+ elementType = callExprElementType;
460
+ }
461
+ else {
462
+ const containerTypeName = scopeEnv.get(iterableName);
463
+ const typeArgPos = methodToTypeArgPosition(methodName, containerTypeName);
464
+ elementType = resolveIterableElementType(iterableName, node, scopeEnv, declarationTypeNodes, scope, extractRustElementTypeFromTypeNode, findRustParamElementType, typeArgPos);
465
+ }
419
466
  if (!elementType)
420
467
  return;
421
468
  const loopVarName = extractVarName(patternNode);
@@ -129,4 +129,17 @@ 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;
133
+ /**
134
+ * Extract the declared type of a property/field from its AST definition node.
135
+ * Handles cross-language patterns:
136
+ * - TypeScript: `name: Type` → type_annotation child
137
+ * - Java: `Type name` → type child on field_declaration
138
+ * - C#: `Type Name { get; set; }` → type child on property_declaration
139
+ * - Go: `Name Type` → type child on field_declaration
140
+ * - Kotlin: `var name: Type` → variable_declaration child with type field
141
+ *
142
+ * Returns the normalized type name, or undefined if no type can be extracted.
143
+ */
144
+ export declare const extractPropertyDeclaredType: (definitionNode: SyntaxNode | null) => string | undefined;
132
145
  export {};