gitnexus 1.4.6 → 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 (40) hide show
  1. package/dist/core/graph/types.d.ts +2 -2
  2. package/dist/core/ingestion/call-processor.d.ts +7 -1
  3. package/dist/core/ingestion/call-processor.js +308 -104
  4. package/dist/core/ingestion/call-routing.d.ts +17 -2
  5. package/dist/core/ingestion/call-routing.js +21 -0
  6. package/dist/core/ingestion/parsing-processor.d.ts +2 -1
  7. package/dist/core/ingestion/parsing-processor.js +32 -6
  8. package/dist/core/ingestion/pipeline.js +5 -1
  9. package/dist/core/ingestion/symbol-table.d.ts +13 -3
  10. package/dist/core/ingestion/symbol-table.js +23 -4
  11. package/dist/core/ingestion/tree-sitter-queries.d.ts +12 -12
  12. package/dist/core/ingestion/tree-sitter-queries.js +200 -0
  13. package/dist/core/ingestion/type-env.js +94 -38
  14. package/dist/core/ingestion/type-extractors/c-cpp.js +27 -2
  15. package/dist/core/ingestion/type-extractors/csharp.js +40 -0
  16. package/dist/core/ingestion/type-extractors/go.js +45 -0
  17. package/dist/core/ingestion/type-extractors/jvm.js +75 -3
  18. package/dist/core/ingestion/type-extractors/php.js +31 -4
  19. package/dist/core/ingestion/type-extractors/python.js +89 -17
  20. package/dist/core/ingestion/type-extractors/ruby.js +17 -2
  21. package/dist/core/ingestion/type-extractors/rust.js +37 -3
  22. package/dist/core/ingestion/type-extractors/shared.d.ts +12 -0
  23. package/dist/core/ingestion/type-extractors/shared.js +110 -3
  24. package/dist/core/ingestion/type-extractors/types.d.ts +17 -4
  25. package/dist/core/ingestion/type-extractors/typescript.js +30 -0
  26. package/dist/core/ingestion/utils.d.ts +25 -0
  27. package/dist/core/ingestion/utils.js +160 -1
  28. package/dist/core/ingestion/workers/parse-worker.d.ts +23 -7
  29. package/dist/core/ingestion/workers/parse-worker.js +68 -26
  30. package/dist/core/lbug/lbug-adapter.d.ts +2 -0
  31. package/dist/core/lbug/lbug-adapter.js +2 -0
  32. package/dist/core/lbug/schema.d.ts +1 -1
  33. package/dist/core/lbug/schema.js +1 -1
  34. package/dist/mcp/core/lbug-adapter.d.ts +22 -0
  35. package/dist/mcp/core/lbug-adapter.js +167 -23
  36. package/dist/mcp/local/local-backend.js +3 -3
  37. package/dist/mcp/resources.js +11 -0
  38. package/dist/mcp/server.js +26 -4
  39. package/dist/mcp/tools.js +15 -5
  40. package/package.json +4 -4
@@ -219,6 +219,32 @@ const extractJavaPendingAssignment = (node, scopeEnv) => {
219
219
  continue;
220
220
  if (valueNode.type === 'identifier' || valueNode.type === 'simple_identifier')
221
221
  return { kind: 'copy', lhs, rhs: valueNode.text };
222
+ // field_access RHS → fieldAccess (a.field)
223
+ if (valueNode.type === 'field_access') {
224
+ const obj = valueNode.childForFieldName('object');
225
+ const field = valueNode.childForFieldName('field');
226
+ if (obj?.type === 'identifier' && field) {
227
+ return { kind: 'fieldAccess', lhs, receiver: obj.text, field: field.text };
228
+ }
229
+ }
230
+ // method_invocation RHS
231
+ if (valueNode.type === 'method_invocation') {
232
+ const objField = valueNode.childForFieldName('object');
233
+ if (!objField) {
234
+ // No receiver → callResult
235
+ const nameField = valueNode.childForFieldName('name');
236
+ if (nameField?.type === 'identifier') {
237
+ return { kind: 'callResult', lhs, callee: nameField.text };
238
+ }
239
+ }
240
+ else if (objField.type === 'identifier') {
241
+ // With receiver → methodCallResult
242
+ const nameField = valueNode.childForFieldName('name');
243
+ if (nameField?.type === 'identifier') {
244
+ return { kind: 'methodCallResult', lhs, receiver: objField.text, method: nameField.text };
245
+ }
246
+ }
247
+ }
222
248
  }
223
249
  return undefined;
224
250
  };
@@ -589,7 +615,7 @@ const extractKotlinPendingAssignment = (node, scopeEnv) => {
589
615
  const lhs = nameNode.text;
590
616
  if (scopeEnv.has(lhs))
591
617
  return undefined;
592
- // Find the RHS: a simple_identifier sibling after the "=" token
618
+ // Find the RHS after the "=" token
593
619
  let foundEq = false;
594
620
  for (let i = 0; i < node.childCount; i++) {
595
621
  const child = node.child(i);
@@ -602,6 +628,31 @@ const extractKotlinPendingAssignment = (node, scopeEnv) => {
602
628
  if (foundEq && child.type === 'simple_identifier') {
603
629
  return { kind: 'copy', lhs, rhs: child.text };
604
630
  }
631
+ // navigation_expression RHS → fieldAccess (a.field)
632
+ if (foundEq && child.type === 'navigation_expression') {
633
+ const recv = child.firstNamedChild;
634
+ const suffix = child.lastNamedChild;
635
+ const fieldNode = suffix?.type === 'navigation_suffix' ? suffix.lastNamedChild : suffix;
636
+ if (recv?.type === 'simple_identifier' && fieldNode?.type === 'simple_identifier') {
637
+ return { kind: 'fieldAccess', lhs, receiver: recv.text, field: fieldNode.text };
638
+ }
639
+ }
640
+ // call_expression RHS
641
+ if (foundEq && child.type === 'call_expression') {
642
+ const calleeNode = child.firstNamedChild;
643
+ if (calleeNode?.type === 'simple_identifier') {
644
+ return { kind: 'callResult', lhs, callee: calleeNode.text };
645
+ }
646
+ // navigation_expression callee → methodCallResult (a.method())
647
+ if (calleeNode?.type === 'navigation_expression') {
648
+ const recv = calleeNode.firstNamedChild;
649
+ const suffix = calleeNode.lastNamedChild;
650
+ const methodNode = suffix?.type === 'navigation_suffix' ? suffix.lastNamedChild : suffix;
651
+ if (recv?.type === 'simple_identifier' && methodNode?.type === 'simple_identifier') {
652
+ return { kind: 'methodCallResult', lhs, receiver: recv.text, method: methodNode.text };
653
+ }
654
+ }
655
+ }
605
656
  }
606
657
  return undefined;
607
658
  }
@@ -613,8 +664,7 @@ const extractKotlinPendingAssignment = (node, scopeEnv) => {
613
664
  const lhs = nameNode.text;
614
665
  if (scopeEnv.has(lhs))
615
666
  return undefined;
616
- // Look for RHS simple_identifier after "=" in the parent (property_declaration)
617
- // variable_declaration itself doesn't contain "=" — it's in the parent
667
+ // Look for RHS after "=" in the parent (property_declaration)
618
668
  const parent = node.parent;
619
669
  if (!parent)
620
670
  return undefined;
@@ -630,6 +680,28 @@ const extractKotlinPendingAssignment = (node, scopeEnv) => {
630
680
  if (foundEq && child.type === 'simple_identifier') {
631
681
  return { kind: 'copy', lhs, rhs: child.text };
632
682
  }
683
+ if (foundEq && child.type === 'navigation_expression') {
684
+ const recv = child.firstNamedChild;
685
+ const suffix = child.lastNamedChild;
686
+ const fieldNode = suffix?.type === 'navigation_suffix' ? suffix.lastNamedChild : suffix;
687
+ if (recv?.type === 'simple_identifier' && fieldNode?.type === 'simple_identifier') {
688
+ return { kind: 'fieldAccess', lhs, receiver: recv.text, field: fieldNode.text };
689
+ }
690
+ }
691
+ if (foundEq && child.type === 'call_expression') {
692
+ const calleeNode = child.firstNamedChild;
693
+ if (calleeNode?.type === 'simple_identifier') {
694
+ return { kind: 'callResult', lhs, callee: calleeNode.text };
695
+ }
696
+ if (calleeNode?.type === 'navigation_expression') {
697
+ const recv = calleeNode.firstNamedChild;
698
+ const suffix = calleeNode.lastNamedChild;
699
+ const methodNode = suffix?.type === 'navigation_suffix' ? suffix.lastNamedChild : suffix;
700
+ if (recv?.type === 'simple_identifier' && methodNode?.type === 'simple_identifier') {
701
+ return { kind: 'methodCallResult', lhs, receiver: recv.text, method: methodNode.text };
702
+ }
703
+ }
704
+ }
633
705
  }
634
706
  return undefined;
635
707
  }
@@ -373,13 +373,40 @@ const extractPendingAssignment = (node, scopeEnv) => {
373
373
  const right = node.childForFieldName('right');
374
374
  if (!left || !right)
375
375
  return undefined;
376
- if (left.type !== 'variable_name' || right.type !== 'variable_name')
376
+ if (left.type !== 'variable_name')
377
377
  return undefined;
378
378
  const lhs = left.text;
379
- const rhs = right.text;
380
- if (!lhs || !rhs || scopeEnv.has(lhs))
379
+ if (!lhs || scopeEnv.has(lhs))
381
380
  return undefined;
382
- return { kind: 'copy', 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;
383
410
  };
384
411
  const FOR_LOOP_NODE_TYPES = new Set([
385
412
  'foreach_statement',
@@ -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,15 +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
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, attribute, or call.
246
284
  const rightNode = node.childForFieldName('right');
247
285
  let iterableName;
248
286
  let methodName;
249
287
  let callExprElementType;
288
+ let isEnumerate = false;
289
+ // Extract iterable info from the `right` field — may be identifier, attribute, or call.
250
290
  if (rightNode?.type === 'identifier') {
251
291
  iterableName = rightNode.text;
252
292
  }
@@ -256,20 +296,28 @@ const extractForLoopBinding = (node, { scopeEnv, declarationTypeNodes, scope, re
256
296
  iterableName = prop.text;
257
297
  }
258
298
  else if (rightNode?.type === 'call') {
259
- // data.items() → call > function: attribute > identifier('data') + identifier('items')
260
- // get_users() → call > function: identifier (Phase 7.3 — return-type path)
261
299
  const fn = rightNode.childForFieldName('function');
262
- if (fn?.type === 'attribute') {
263
- const obj = fn.firstNamedChild;
264
- if (obj?.type === 'identifier')
265
- iterableName = obj.text;
266
- // Extract method name: items, keys, values
267
- const method = fn.lastNamedChild;
268
- if (method?.type === 'identifier' && method !== obj)
269
- 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);
270
318
  }
271
319
  else if (fn?.type === 'identifier') {
272
- // Direct function call: for user in get_users()
320
+ // Direct function call: for user in get_users() (Phase 7.3 — return-type path)
273
321
  const rawReturn = returnTypeLookup.lookupRawReturnType(fn.text);
274
322
  if (rawReturn)
275
323
  callExprElementType = extractElementTypeFromString(rawReturn);
@@ -292,11 +340,12 @@ const extractForLoopBinding = (node, { scopeEnv, declarationTypeNodes, scope, re
292
340
  const leftNode = node.childForFieldName('left');
293
341
  if (!leftNode)
294
342
  return;
295
- // Handle tuple unpacking: for key, value in data.items()
296
- if (leftNode.type === 'pattern_list') {
297
- const lastChild = leftNode.lastNamedChild;
298
- if (lastChild?.type === 'identifier') {
299
- 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);
300
349
  }
301
350
  return;
302
351
  }
@@ -327,6 +376,29 @@ const extractPendingAssignment = (node, scopeEnv) => {
327
376
  return undefined;
328
377
  if (right.type === 'identifier')
329
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
+ }
330
402
  return undefined;
331
403
  };
332
404
  /**
@@ -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 { kind: 'copy', 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,
@@ -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 { kind: 'copy', 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
  /**
@@ -130,4 +130,16 @@ export declare const findChildByType: (node: SyntaxNode, type: string) => Syntax
130
130
  */
131
131
  export declare function extractElementTypeFromString(typeStr: string, pos?: TypeArgPosition): string | undefined;
132
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;
133
145
  export {};
@@ -226,9 +226,14 @@ export const extractSimpleTypeName = (typeNode, depth = 0) => {
226
226
  }
227
227
  // Pointer/reference types (C++, Rust): User*, &User, &mut User
228
228
  if (typeNode.type === 'pointer_type' || typeNode.type === 'reference_type') {
229
- const inner = typeNode.firstNamedChild;
230
- if (inner)
231
- return extractSimpleTypeName(inner, depth + 1);
229
+ // Skip mutable_specifier for Rust &mut references — firstNamedChild would be
230
+ // `mutable_specifier` not the actual type. Walk named children to find the type.
231
+ for (let i = 0; i < typeNode.namedChildCount; i++) {
232
+ const child = typeNode.namedChild(i);
233
+ if (child && child.type !== 'mutable_specifier') {
234
+ return extractSimpleTypeName(child, depth + 1);
235
+ }
236
+ }
232
237
  }
233
238
  // Primitive/predefined types: string, int, float, bool, number, unknown, any
234
239
  // PHP: primitive_type; TS/JS: predefined_type
@@ -701,3 +706,105 @@ export const extractReturnTypeName = (raw, depth = 0) => {
701
706
  return undefined;
702
707
  return text;
703
708
  };
709
+ // ── Property declared-type extraction ────────────────────────────────────
710
+ // Shared between parse-worker (worker path) and parsing-processor (sequential path).
711
+ /**
712
+ * Extract the declared type of a property/field from its AST definition node.
713
+ * Handles cross-language patterns:
714
+ * - TypeScript: `name: Type` → type_annotation child
715
+ * - Java: `Type name` → type child on field_declaration
716
+ * - C#: `Type Name { get; set; }` → type child on property_declaration
717
+ * - Go: `Name Type` → type child on field_declaration
718
+ * - Kotlin: `var name: Type` → variable_declaration child with type field
719
+ *
720
+ * Returns the normalized type name, or undefined if no type can be extracted.
721
+ */
722
+ export const extractPropertyDeclaredType = (definitionNode) => {
723
+ if (!definitionNode)
724
+ return undefined;
725
+ // Strategy 1: Look for a `type` or `type_annotation` named field
726
+ const typeNode = definitionNode.childForFieldName?.('type');
727
+ if (typeNode) {
728
+ const typeName = extractSimpleTypeName(typeNode);
729
+ if (typeName)
730
+ return typeName;
731
+ // Fallback: use the raw text (for complex types like User[] or List<User>)
732
+ const text = typeNode.text?.trim();
733
+ if (text && text.length < 100)
734
+ return text;
735
+ }
736
+ // Strategy 2: Walk children looking for type_annotation (TypeScript pattern)
737
+ for (let i = 0; i < definitionNode.childCount; i++) {
738
+ const child = definitionNode.child(i);
739
+ if (!child)
740
+ continue;
741
+ if (child.type === 'type_annotation') {
742
+ // Type annotation has the actual type as a child
743
+ for (let j = 0; j < child.childCount; j++) {
744
+ const typeChild = child.child(j);
745
+ if (typeChild && typeChild.type !== ':') {
746
+ const typeName = extractSimpleTypeName(typeChild);
747
+ if (typeName)
748
+ return typeName;
749
+ const text = typeChild.text?.trim();
750
+ if (text && text.length < 100)
751
+ return text;
752
+ }
753
+ }
754
+ }
755
+ }
756
+ // Strategy 3: For Java field_declaration, the type is a sibling of variable_declarator
757
+ // AST: (field_declaration type: (type_identifier) declarator: (variable_declarator ...))
758
+ const parentDecl = definitionNode.parent;
759
+ if (parentDecl) {
760
+ const parentType = parentDecl.childForFieldName?.('type');
761
+ if (parentType) {
762
+ const typeName = extractSimpleTypeName(parentType);
763
+ if (typeName)
764
+ return typeName;
765
+ }
766
+ }
767
+ // Strategy 4: Kotlin property_declaration — type is nested inside variable_declaration child
768
+ // AST: (property_declaration (variable_declaration (simple_identifier) ":" (user_type (type_identifier))))
769
+ // Kotlin's variable_declaration has NO named 'type' field — children are all positional.
770
+ for (let i = 0; i < definitionNode.childCount; i++) {
771
+ const child = definitionNode.child(i);
772
+ if (child?.type === 'variable_declaration') {
773
+ // Try named field first (works for other languages sharing this strategy)
774
+ const varType = child.childForFieldName?.('type');
775
+ if (varType) {
776
+ const typeName = extractSimpleTypeName(varType);
777
+ if (typeName)
778
+ return typeName;
779
+ const text = varType.text?.trim();
780
+ if (text && text.length < 100)
781
+ return text;
782
+ }
783
+ // Fallback: walk unnamed children for user_type / type_identifier (Kotlin)
784
+ for (let j = 0; j < child.namedChildCount; j++) {
785
+ const varChild = child.namedChild(j);
786
+ if (varChild && (varChild.type === 'user_type' || varChild.type === 'type_identifier'
787
+ || varChild.type === 'nullable_type' || varChild.type === 'generic_type')) {
788
+ const typeName = extractSimpleTypeName(varChild);
789
+ if (typeName)
790
+ return typeName;
791
+ }
792
+ }
793
+ }
794
+ }
795
+ // Strategy 5: PHP @var PHPDoc — look for preceding comment with @var Type
796
+ // Handles pre-PHP-7.4 code: /** @var Address */ public $address;
797
+ const prevSibling = definitionNode.previousNamedSibling ?? definitionNode.parent?.previousNamedSibling;
798
+ if (prevSibling?.type === 'comment') {
799
+ const commentText = prevSibling.text;
800
+ const varMatch = commentText?.match(/@var\s+([A-Z][\w\\]*)/);
801
+ if (varMatch) {
802
+ // Strip namespace prefix: \App\Models\User → User
803
+ const raw = varMatch[1];
804
+ const base = raw.includes('\\') ? raw.split('\\').pop() : raw;
805
+ if (base && /^[A-Z]\w*$/.test(base))
806
+ return base;
807
+ }
808
+ }
809
+ return undefined;
810
+ };
@@ -49,8 +49,10 @@ export interface ForLoopExtractorContext {
49
49
  /** Extracts loop variable type binding from a for-each statement. */
50
50
  export type ForLoopExtractor = (node: SyntaxNode, ctx: ForLoopExtractorContext) => void;
51
51
  /** Discriminated union for pending Tier-2 propagation items.
52
- * - `copy` — `const b = a` (identifier alias, propagate a's type to b)
53
- * - `callResult` — `const b = foo()` (bind b to foo's declared return type) */
52
+ * - `copy` — `const b = a` (identifier alias, propagate a's type to b)
53
+ * - `callResult` — `const b = foo()` (bind b to foo's declared return type)
54
+ * - `fieldAccess` — `const b = a.field` (bind b to field's declaredType on a's type)
55
+ * - `methodCallResult` — `const b = a.method()` (bind b to method's returnType on a's type) */
54
56
  export type PendingAssignment = {
55
57
  kind: 'copy';
56
58
  lhs: string;
@@ -59,10 +61,21 @@ export type PendingAssignment = {
59
61
  kind: 'callResult';
60
62
  lhs: string;
61
63
  callee: string;
64
+ } | {
65
+ kind: 'fieldAccess';
66
+ lhs: string;
67
+ receiver: string;
68
+ field: string;
69
+ } | {
70
+ kind: 'methodCallResult';
71
+ lhs: string;
72
+ receiver: string;
73
+ method: string;
62
74
  };
63
75
  /** Extracts a pending assignment for Tier 2 propagation.
64
- * Returns a PendingAssignment when the RHS is a bare identifier (`copy`) or a
65
- * call expression (`callResult`) and the LHS has no resolved type yet.
76
+ * Returns a PendingAssignment when the RHS is a bare identifier (`copy`), a
77
+ * call expression (`callResult`), a field access (`fieldAccess`), or a
78
+ * method call with receiver (`methodCallResult`) and the LHS has no resolved type yet.
66
79
  * Returns undefined if the node is not a matching assignment. */
67
80
  export type PendingAssignmentExtractor = (node: SyntaxNode, scopeEnv: ReadonlyMap<string, string>) => PendingAssignment | undefined;
68
81
  /** Extracts a typed variable binding from a pattern-matching construct.
@@ -459,6 +459,36 @@ const extractPendingAssignment = (node, scopeEnv) => {
459
459
  continue;
460
460
  if (valueNode.type === 'identifier')
461
461
  return { kind: 'copy', lhs, rhs: valueNode.text };
462
+ // member_expression RHS → fieldAccess (a.field, this.field)
463
+ if (valueNode.type === 'member_expression') {
464
+ const obj = valueNode.childForFieldName('object');
465
+ const prop = valueNode.childForFieldName('property');
466
+ if (obj && prop?.type === 'property_identifier' &&
467
+ (obj.type === 'identifier' || obj.type === 'this')) {
468
+ return { kind: 'fieldAccess', lhs, receiver: obj.text, field: prop.text };
469
+ }
470
+ continue;
471
+ }
472
+ // Unwrap await: `const user = await fetchUser()` or `await a.getC()`
473
+ const callNode = unwrapAwait(valueNode);
474
+ if (!callNode || callNode.type !== 'call_expression')
475
+ continue;
476
+ const funcNode = callNode.childForFieldName('function');
477
+ if (!funcNode)
478
+ continue;
479
+ // Simple call → callResult: getUser()
480
+ if (funcNode.type === 'identifier') {
481
+ return { kind: 'callResult', lhs, callee: funcNode.text };
482
+ }
483
+ // Method call with receiver → methodCallResult: a.getC()
484
+ if (funcNode.type === 'member_expression') {
485
+ const obj = funcNode.childForFieldName('object');
486
+ const prop = funcNode.childForFieldName('property');
487
+ if (obj && prop?.type === 'property_identifier' &&
488
+ (obj.type === 'identifier' || obj.type === 'this')) {
489
+ return { kind: 'methodCallResult', lhs, receiver: obj.text, method: prop.text };
490
+ }
491
+ }
462
492
  }
463
493
  return undefined;
464
494
  };
@@ -110,4 +110,29 @@ export declare function extractCallChain(receiverCallNode: SyntaxNode): {
110
110
  chain: string[];
111
111
  baseReceiverName: string | undefined;
112
112
  } | undefined;
113
+ /** One step in a mixed receiver chain. */
114
+ export type MixedChainStep = {
115
+ kind: 'field' | 'call';
116
+ name: string;
117
+ };
118
+ /**
119
+ * Walk a receiver AST node that may interleave field accesses and method calls,
120
+ * building a unified chain of steps up to MAX_CHAIN_DEPTH.
121
+ *
122
+ * For `svc.getUser().address.save()`, called with the receiver of `save`
123
+ * (`svc.getUser().address`, a field access node):
124
+ * returns { chain: [{ kind:'call', name:'getUser' }, { kind:'field', name:'address' }],
125
+ * baseReceiverName: 'svc' }
126
+ *
127
+ * For `user.getAddress().city.getName()`, called with receiver of `getName`
128
+ * (`user.getAddress().city`):
129
+ * returns { chain: [{ kind:'call', name:'getAddress' }, { kind:'field', name:'city' }],
130
+ * baseReceiverName: 'user' }
131
+ *
132
+ * Pure field chains and pure call chains are special cases (all steps same kind).
133
+ */
134
+ export declare function extractMixedChain(receiverNode: SyntaxNode): {
135
+ chain: MixedChainStep[];
136
+ baseReceiverName: string | undefined;
137
+ } | undefined;
113
138
  export {};