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
@@ -132,7 +132,7 @@ export function resolveIterableElementType(iterableName, node, scopeEnv, declara
132
132
  /** Known single-arg nullable wrapper types that unwrap to their inner type
133
133
  * for receiver resolution. Optional<User> → "User", Option<User> → "User".
134
134
  * Only nullable wrappers — NOT containers (List, Vec) or async wrappers (Promise, Future).
135
- * See call-processor.ts WRAPPER_GENERICS for the full set used in return-type inference. */
135
+ * See WRAPPER_GENERICS below for the full set used in return-type inference. */
136
136
  const NULLABLE_WRAPPER_TYPES = new Set([
137
137
  'Optional', // Java
138
138
  'Option', // Rust, Scala
@@ -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
@@ -569,3 +574,237 @@ export function extractElementTypeFromString(typeStr, pos = 'last') {
569
574
  }
570
575
  return undefined;
571
576
  }
577
+ // ── Return type text helpers ─────────────────────────────────────────────
578
+ // extractReturnTypeName works on raw return-type text already stored in
579
+ // SymbolDefinition (e.g. "User", "Promise<User>", "User | null", "*User").
580
+ // Extracts the base user-defined type name.
581
+ /** Primitive / built-in types that should NOT produce a receiver binding. */
582
+ const PRIMITIVE_TYPES = new Set([
583
+ 'string', 'number', 'boolean', 'void', 'int', 'float', 'double', 'long',
584
+ 'short', 'byte', 'char', 'bool', 'str', 'i8', 'i16', 'i32', 'i64',
585
+ 'u8', 'u16', 'u32', 'u64', 'f32', 'f64', 'usize', 'isize',
586
+ 'undefined', 'null', 'None', 'nil',
587
+ ]);
588
+ /**
589
+ * Extract a simple type name from raw return-type text.
590
+ * Handles common patterns:
591
+ * "User" → "User"
592
+ * "Promise<User>" → "User" (unwrap wrapper generics)
593
+ * "Option<User>" → "User"
594
+ * "Result<User, Error>" → "User" (first type arg)
595
+ * "User | null" → "User" (strip nullable union)
596
+ * "User?" → "User" (strip nullable suffix)
597
+ * "*User" → "User" (Go pointer)
598
+ * "&User" → "User" (Rust reference)
599
+ * Returns undefined for complex types or primitives.
600
+ */
601
+ const WRAPPER_GENERICS = new Set([
602
+ 'Promise', 'Observable', 'Future', 'CompletableFuture', 'Task', 'ValueTask', // async wrappers
603
+ 'Option', 'Some', 'Optional', 'Maybe', // nullable wrappers
604
+ 'Result', 'Either', // result wrappers
605
+ // Rust smart pointers (Deref to inner type)
606
+ 'Rc', 'Arc', 'Weak', // pointer types
607
+ 'MutexGuard', 'RwLockReadGuard', 'RwLockWriteGuard', // guard types
608
+ 'Ref', 'RefMut', // RefCell guards
609
+ 'Cow', // copy-on-write
610
+ // Containers (List, Array, Vec, Set, etc.) are intentionally excluded —
611
+ // methods are called on the container, not the element type.
612
+ // Non-wrapper generics return the base type (e.g., List) via the else branch.
613
+ ]);
614
+ /**
615
+ * Extracts the first type argument from a comma-separated generic argument string,
616
+ * respecting nested angle brackets. For example:
617
+ * "Result<User, Error>" → "Result<User, Error>" (no top-level comma)
618
+ * "User, Error" → "User"
619
+ * "Map<K, V>, string" → "Map<K, V>"
620
+ */
621
+ function extractFirstGenericArg(args) {
622
+ let depth = 0;
623
+ for (let i = 0; i < args.length; i++) {
624
+ if (args[i] === '<')
625
+ depth++;
626
+ else if (args[i] === '>')
627
+ depth--;
628
+ else if (args[i] === ',' && depth === 0)
629
+ return args.slice(0, i).trim();
630
+ }
631
+ return args.trim();
632
+ }
633
+ /**
634
+ * Extract the first non-lifetime type argument from a generic argument string.
635
+ * Skips Rust lifetime parameters (e.g., `'a`, `'_`) to find the actual type.
636
+ * "'_, User" → "User"
637
+ * "'a, User" → "User"
638
+ * "User, Error" → "User" (no lifetime — delegates to extractFirstGenericArg)
639
+ */
640
+ function extractFirstTypeArg(args) {
641
+ let remaining = args;
642
+ while (remaining) {
643
+ const first = extractFirstGenericArg(remaining);
644
+ if (!first.startsWith("'"))
645
+ return first;
646
+ // Skip past this lifetime arg + the comma separator
647
+ const commaIdx = remaining.indexOf(',', first.length);
648
+ if (commaIdx < 0)
649
+ return first; // only lifetimes — fall through
650
+ remaining = remaining.slice(commaIdx + 1).trim();
651
+ }
652
+ return args.trim();
653
+ }
654
+ const MAX_RETURN_TYPE_INPUT_LENGTH = 2048;
655
+ const MAX_RETURN_TYPE_LENGTH = 512;
656
+ export const extractReturnTypeName = (raw, depth = 0) => {
657
+ if (depth > 10)
658
+ return undefined;
659
+ if (raw.length > MAX_RETURN_TYPE_INPUT_LENGTH)
660
+ return undefined;
661
+ let text = raw.trim();
662
+ if (!text)
663
+ return undefined;
664
+ // Strip pointer/reference prefixes: *User, &User, &mut User
665
+ text = text.replace(/^[&*]+\s*(mut\s+)?/, '');
666
+ // Strip nullable suffix: User?
667
+ text = text.replace(/\?$/, '');
668
+ // Handle union types: "User | null" → "User"
669
+ if (text.includes('|')) {
670
+ const parts = text.split('|').map(p => p.trim()).filter(p => p !== 'null' && p !== 'undefined' && p !== 'void' && p !== 'None' && p !== 'nil');
671
+ if (parts.length === 1)
672
+ text = parts[0];
673
+ else
674
+ return undefined; // genuine union — too complex
675
+ }
676
+ // Handle generics: Promise<User> → unwrap if wrapper, else take base
677
+ const genericMatch = text.match(/^(\w+)\s*<(.+)>$/);
678
+ if (genericMatch) {
679
+ const [, base, args] = genericMatch;
680
+ if (WRAPPER_GENERICS.has(base)) {
681
+ // Take the first non-lifetime type argument, using bracket-balanced splitting
682
+ // so that nested generics like Result<User, Error> are not split at the inner
683
+ // comma. Lifetime parameters (Rust 'a, '_) are skipped.
684
+ const firstArg = extractFirstTypeArg(args);
685
+ return extractReturnTypeName(firstArg, depth + 1);
686
+ }
687
+ // Non-wrapper generic: return the base type (e.g., Map<K,V> → Map)
688
+ return PRIMITIVE_TYPES.has(base.toLowerCase()) ? undefined : base;
689
+ }
690
+ // Bare wrapper type without generic argument (e.g. Task, Promise, Option)
691
+ // should not produce a binding — these are meaningless without a type parameter
692
+ if (WRAPPER_GENERICS.has(text))
693
+ return undefined;
694
+ // Handle qualified names: models.User → User, Models::User → User, \App\Models\User → User
695
+ if (text.includes('::') || text.includes('.') || text.includes('\\')) {
696
+ text = text.split(/::|[.\\]/).pop();
697
+ }
698
+ // Final check: skip primitives
699
+ if (PRIMITIVE_TYPES.has(text) || PRIMITIVE_TYPES.has(text.toLowerCase()))
700
+ return undefined;
701
+ // Must start with uppercase (class/type convention) or be a valid identifier
702
+ if (!/^[A-Z_]\w*$/.test(text))
703
+ return undefined;
704
+ // If the final extracted type name is too long, reject it
705
+ if (text.length > MAX_RETURN_TYPE_LENGTH)
706
+ return undefined;
707
+ return text;
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
+ };
@@ -23,17 +23,61 @@ export type ConstructorBindingScanner = (node: SyntaxNode) => {
23
23
  * Used for languages where return types are expressed in comments (e.g. YARD @return [Type])
24
24
  * rather than in AST fields. Returns undefined if no return type can be determined. */
25
25
  export type ReturnTypeExtractor = (node: SyntaxNode) => string | undefined;
26
- /** Extracts loop variable type binding from a for-each statement.
27
- * All parameters are required (aligned with PatternBindingExtractor convention)
28
- * to prevent new extractors from silently ignoring declarationTypeNodes/scope. */
29
- export type ForLoopExtractor = (node: SyntaxNode, scopeEnv: Map<string, string>, declarationTypeNodes: ReadonlyMap<string, SyntaxNode>, scope: string) => void;
30
- /** Extracts a plain-identifier assignment for Tier 2 propagation.
31
- * For `const b = a`, returns { lhs: 'b', rhs: 'a' } when the LHS has no resolved type.
32
- * Returns undefined if the node is not a plain identifier assignment. */
33
- export type PendingAssignmentExtractor = (node: SyntaxNode, scopeEnv: ReadonlyMap<string, string>) => {
26
+ /** Narrow lookup interface for resolving a callee name → return type name.
27
+ * Backed by SymbolTable.lookupFuzzyCallable; passed via ForLoopExtractorContext.
28
+ * Conservative: returns undefined when the callee is ambiguous (0 or 2+ matches). */
29
+ export interface ReturnTypeLookup {
30
+ /** Processed type name after stripping wrappers (e.g., 'User' from 'Promise<User>').
31
+ * Use for call-result variable bindings (`const b = foo()`). */
32
+ lookupReturnType(callee: string): string | undefined;
33
+ /** Raw return type as declared in the symbol (e.g., '[]User', 'List<User>').
34
+ * Use for iterable-element extraction (`for v := range foo()`). */
35
+ lookupRawReturnType(callee: string): string | undefined;
36
+ }
37
+ /** Context object passed to ForLoopExtractor.
38
+ * Groups the four parameters that were previously positional. */
39
+ export interface ForLoopExtractorContext {
40
+ /** Mutable type-env for the current scope — extractor writes bindings here */
41
+ scopeEnv: Map<string, string>;
42
+ /** Maps `scope\0varName` to the declaration's type annotation AST node */
43
+ declarationTypeNodes: ReadonlyMap<string, SyntaxNode>;
44
+ /** Current scope key, e.g. `"process@42"` */
45
+ scope: string;
46
+ /** Resolves a callee name to its declared return type (undefined = unknown/ambiguous) */
47
+ returnTypeLookup: ReturnTypeLookup;
48
+ }
49
+ /** Extracts loop variable type binding from a for-each statement. */
50
+ export type ForLoopExtractor = (node: SyntaxNode, ctx: ForLoopExtractorContext) => void;
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)
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) */
56
+ export type PendingAssignment = {
57
+ kind: 'copy';
34
58
  lhs: string;
35
59
  rhs: string;
36
- } | undefined;
60
+ } | {
61
+ kind: 'callResult';
62
+ lhs: string;
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;
74
+ };
75
+ /** Extracts a pending assignment for Tier 2 propagation.
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.
79
+ * Returns undefined if the node is not a matching assignment. */
80
+ export type PendingAssignmentExtractor = (node: SyntaxNode, scopeEnv: ReadonlyMap<string, string>) => PendingAssignment | undefined;
37
81
  /** Extracts a typed variable binding from a pattern-matching construct.
38
82
  * Returns { varName, typeName } for patterns that introduce NEW variables.
39
83
  * Examples: `if let Some(user) = opt` (Rust), `x instanceof User user` (Java).
@@ -82,9 +126,10 @@ export interface LanguageTypeConfig {
82
126
  extractReturnType?: ReturnTypeExtractor;
83
127
  /** Extract loop variable → type binding from a for-each AST node. */
84
128
  extractForLoopBinding?: ForLoopExtractor;
85
- /** Extract plain-identifier assignment (e.g. `const b = a`) for Tier 2 chain propagation.
86
- * Called on declaration/assignment nodes; returns {lhs, rhs} when the RHS is a bare identifier
87
- * and the LHS has no resolved type yet. Language-specific because AST shapes differ widely. */
129
+ /** Extract pending assignment for Tier 2 propagation.
130
+ * Called on declaration/assignment nodes; returns a PendingAssignment when the RHS
131
+ * is a bare identifier (copy) or call expression (callResult) and the LHS has no
132
+ * resolved type yet. Language-specific because AST shapes differ widely. */
88
133
  extractPendingAssignment?: PendingAssignmentExtractor;
89
134
  /** Extract a typed variable binding from a pattern-matching construct.
90
135
  * Called on every AST node; returns { varName, typeName } when the node introduces a new
@@ -345,7 +345,7 @@ const findTsIterableElementType = (iterableName, startNode, pos = 'last') => {
345
345
  * 3. AST walk — walks up to the enclosing function's parameters to read User[] annotations directly
346
346
  * Only handles `for...of`; `for...in` produces string keys, not element types.
347
347
  */
348
- const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
348
+ const extractForLoopBinding = (node, { scopeEnv, declarationTypeNodes, scope, returnTypeLookup }) => {
349
349
  if (node.type !== 'for_in_statement')
350
350
  return;
351
351
  // Confirm this is `for...of`, not `for...in`, by scanning unnamed children for the keyword text.
@@ -359,10 +359,11 @@ const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
359
359
  }
360
360
  if (!isForOf)
361
361
  return;
362
- // The iterable is the `right` field — may be identifier or call_expression.
362
+ // The iterable is the `right` field — may be identifier, member_expression, or call_expression.
363
363
  const rightNode = node.childForFieldName('right');
364
364
  let iterableName;
365
365
  let methodName;
366
+ let callExprElementType;
366
367
  if (rightNode?.type === 'identifier') {
367
368
  iterableName = rightNode.text;
368
369
  }
@@ -374,6 +375,7 @@ const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
374
375
  else if (rightNode?.type === 'call_expression') {
375
376
  // entries.values() → call_expression > function: member_expression > object + property
376
377
  // this.repos.values() → nested member_expression: extract property from inner member
378
+ // getUsers() → call_expression > function: identifier (Phase 7.3 — return-type path)
377
379
  const fn = rightNode.childForFieldName('function');
378
380
  if (fn?.type === 'member_expression') {
379
381
  const obj = fn.childForFieldName('object');
@@ -390,13 +392,25 @@ const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
390
392
  if (prop?.type === 'property_identifier')
391
393
  methodName = prop.text;
392
394
  }
395
+ else if (fn?.type === 'identifier') {
396
+ // Direct function call: for (const user of getUsers())
397
+ const rawReturn = returnTypeLookup.lookupRawReturnType(fn.text);
398
+ if (rawReturn)
399
+ callExprElementType = extractElementTypeFromString(rawReturn);
400
+ }
393
401
  }
394
- if (!iterableName)
402
+ if (!iterableName && !callExprElementType)
395
403
  return;
396
- // Look up the container's base type name for descriptor-aware resolution
397
- const containerTypeName = scopeEnv.get(iterableName);
398
- const typeArgPos = methodToTypeArgPosition(methodName, containerTypeName);
399
- const elementType = resolveIterableElementType(iterableName, node, scopeEnv, declarationTypeNodes, scope, extractTsElementTypeFromAnnotation, findTsIterableElementType, typeArgPos);
404
+ let elementType;
405
+ if (callExprElementType) {
406
+ elementType = callExprElementType;
407
+ }
408
+ else {
409
+ // Look up the container's base type name for descriptor-aware resolution
410
+ const containerTypeName = scopeEnv.get(iterableName);
411
+ const typeArgPos = methodToTypeArgPosition(methodName, containerTypeName);
412
+ elementType = resolveIterableElementType(iterableName, node, scopeEnv, declarationTypeNodes, scope, extractTsElementTypeFromAnnotation, findTsIterableElementType, typeArgPos);
413
+ }
400
414
  if (!elementType)
401
415
  return;
402
416
  // The loop variable is the `left` field.
@@ -444,7 +458,37 @@ const extractPendingAssignment = (node, scopeEnv) => {
444
458
  if (scopeEnv.has(lhs))
445
459
  continue;
446
460
  if (valueNode.type === 'identifier')
447
- return { lhs, rhs: valueNode.text };
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
+ }
448
492
  }
449
493
  return undefined;
450
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 {};
@@ -254,7 +254,7 @@ export const CLASS_CONTAINER_TYPES = new Set([
254
254
  'class_declaration', 'abstract_class_declaration',
255
255
  'interface_declaration', 'struct_declaration', 'record_declaration',
256
256
  'class_specifier', 'struct_specifier',
257
- 'impl_item', 'trait_item',
257
+ 'impl_item', 'trait_item', 'struct_item', 'enum_item',
258
258
  'class_definition',
259
259
  'trait_declaration',
260
260
  'protocol_declaration',
@@ -275,6 +275,8 @@ export const CONTAINER_TYPE_TO_LABEL = {
275
275
  class_definition: 'Class',
276
276
  impl_item: 'Impl',
277
277
  trait_item: 'Trait',
278
+ struct_item: 'Struct',
279
+ enum_item: 'Enum',
278
280
  trait_declaration: 'Trait',
279
281
  record_declaration: 'Record',
280
282
  protocol_declaration: 'Interface',
@@ -306,6 +308,21 @@ export const findEnclosingClassId = (node, filePath) => {
306
308
  }
307
309
  }
308
310
  }
311
+ // Go: type_declaration wrapping a struct_type (type User struct { ... })
312
+ // field_declaration → field_declaration_list → struct_type → type_spec → type_declaration
313
+ if (current.type === 'type_declaration') {
314
+ const typeSpec = current.children?.find((c) => c.type === 'type_spec');
315
+ if (typeSpec) {
316
+ const typeBody = typeSpec.childForFieldName?.('type');
317
+ if (typeBody?.type === 'struct_type' || typeBody?.type === 'interface_type') {
318
+ const nameNode = typeSpec.childForFieldName?.('name');
319
+ if (nameNode) {
320
+ const label = typeBody.type === 'struct_type' ? 'Struct' : 'Interface';
321
+ return generateId(label, `${filePath}:${nameNode.text}`);
322
+ }
323
+ }
324
+ }
325
+ }
309
326
  if (CLASS_CONTAINER_TYPES.has(current.type)) {
310
327
  // Rust impl_item: for `impl Trait for Struct {}`, pick the type after `for`
311
328
  if (current.type === 'impl_item') {
@@ -1129,3 +1146,145 @@ export function extractCallChain(receiverCallNode) {
1129
1146
  }
1130
1147
  return chain.length > 0 ? { chain, baseReceiverName: undefined } : undefined;
1131
1148
  }
1149
+ /** Node types representing member/field access across languages. */
1150
+ const FIELD_ACCESS_NODE_TYPES = new Set([
1151
+ 'member_expression', // TS/JS
1152
+ 'member_access_expression', // C#
1153
+ 'selector_expression', // Go
1154
+ 'field_expression', // Rust/C++
1155
+ 'field_access', // Java
1156
+ 'attribute', // Python
1157
+ 'navigation_expression', // Kotlin/Swift
1158
+ 'member_binding_expression', // C# null-conditional (user?.Address)
1159
+ ]);
1160
+ /**
1161
+ * Walk a receiver AST node that may interleave field accesses and method calls,
1162
+ * building a unified chain of steps up to MAX_CHAIN_DEPTH.
1163
+ *
1164
+ * For `svc.getUser().address.save()`, called with the receiver of `save`
1165
+ * (`svc.getUser().address`, a field access node):
1166
+ * returns { chain: [{ kind:'call', name:'getUser' }, { kind:'field', name:'address' }],
1167
+ * baseReceiverName: 'svc' }
1168
+ *
1169
+ * For `user.getAddress().city.getName()`, called with receiver of `getName`
1170
+ * (`user.getAddress().city`):
1171
+ * returns { chain: [{ kind:'call', name:'getAddress' }, { kind:'field', name:'city' }],
1172
+ * baseReceiverName: 'user' }
1173
+ *
1174
+ * Pure field chains and pure call chains are special cases (all steps same kind).
1175
+ */
1176
+ export function extractMixedChain(receiverNode) {
1177
+ const chain = [];
1178
+ let current = receiverNode;
1179
+ while (chain.length < MAX_CHAIN_DEPTH) {
1180
+ if (CALL_EXPRESSION_TYPES.has(current.type)) {
1181
+ // ── Call expression: extract method name + inner receiver ────────────
1182
+ const funcNode = current.childForFieldName?.('function')
1183
+ ?? current.childForFieldName?.('name')
1184
+ ?? current.childForFieldName?.('method');
1185
+ let methodName;
1186
+ let innerReceiver = null;
1187
+ if (funcNode) {
1188
+ methodName = funcNode.lastNamedChild?.text ?? funcNode.text;
1189
+ }
1190
+ // Kotlin/Swift: call_expression → navigation_expression
1191
+ if (!funcNode && current.type === 'call_expression') {
1192
+ const callee = current.firstNamedChild;
1193
+ if (callee?.type === 'navigation_expression') {
1194
+ const suffix = callee.lastNamedChild;
1195
+ if (suffix?.type === 'navigation_suffix') {
1196
+ methodName = suffix.lastNamedChild?.text;
1197
+ for (let i = 0; i < callee.namedChildCount; i++) {
1198
+ const child = callee.namedChild(i);
1199
+ if (child && child.type !== 'navigation_suffix') {
1200
+ innerReceiver = child;
1201
+ break;
1202
+ }
1203
+ }
1204
+ }
1205
+ }
1206
+ }
1207
+ if (!methodName)
1208
+ break;
1209
+ chain.unshift({ kind: 'call', name: methodName });
1210
+ if (!innerReceiver && funcNode) {
1211
+ innerReceiver = funcNode.childForFieldName?.('object')
1212
+ ?? funcNode.childForFieldName?.('value')
1213
+ ?? funcNode.childForFieldName?.('operand')
1214
+ ?? funcNode.childForFieldName?.('argument') // C/C++ field_expression
1215
+ ?? funcNode.childForFieldName?.('expression')
1216
+ ?? null;
1217
+ }
1218
+ if (!innerReceiver && current.type === 'method_invocation') {
1219
+ innerReceiver = current.childForFieldName?.('object') ?? null;
1220
+ }
1221
+ if (!innerReceiver && (current.type === 'member_call_expression' || current.type === 'nullsafe_member_call_expression')) {
1222
+ innerReceiver = current.childForFieldName?.('object') ?? null;
1223
+ }
1224
+ if (!innerReceiver && current.type === 'call') {
1225
+ innerReceiver = current.childForFieldName?.('receiver') ?? null;
1226
+ }
1227
+ if (!innerReceiver)
1228
+ break;
1229
+ if (CALL_EXPRESSION_TYPES.has(innerReceiver.type) || FIELD_ACCESS_NODE_TYPES.has(innerReceiver.type)) {
1230
+ current = innerReceiver;
1231
+ }
1232
+ else {
1233
+ return { chain, baseReceiverName: innerReceiver.text || undefined };
1234
+ }
1235
+ }
1236
+ else if (FIELD_ACCESS_NODE_TYPES.has(current.type)) {
1237
+ // ── Field/member access: extract property name + inner object ─────────
1238
+ let propertyName;
1239
+ let innerObject = null;
1240
+ if (current.type === 'navigation_expression') {
1241
+ for (const child of current.children ?? []) {
1242
+ if (child.type === 'navigation_suffix') {
1243
+ for (const sc of child.children ?? []) {
1244
+ if (sc.isNamed && sc.type !== '.') {
1245
+ propertyName = sc.text;
1246
+ break;
1247
+ }
1248
+ }
1249
+ }
1250
+ else if (child.isNamed && !innerObject) {
1251
+ innerObject = child;
1252
+ }
1253
+ }
1254
+ }
1255
+ else if (current.type === 'attribute') {
1256
+ innerObject = current.childForFieldName?.('object') ?? null;
1257
+ propertyName = current.childForFieldName?.('attribute')?.text;
1258
+ }
1259
+ else {
1260
+ innerObject = current.childForFieldName?.('object')
1261
+ ?? current.childForFieldName?.('value')
1262
+ ?? current.childForFieldName?.('operand')
1263
+ ?? current.childForFieldName?.('argument') // C/C++ field_expression
1264
+ ?? current.childForFieldName?.('expression')
1265
+ ?? null;
1266
+ propertyName = (current.childForFieldName?.('property')
1267
+ ?? current.childForFieldName?.('field')
1268
+ ?? current.childForFieldName?.('name'))?.text;
1269
+ }
1270
+ if (!propertyName)
1271
+ break;
1272
+ chain.unshift({ kind: 'field', name: propertyName });
1273
+ if (!innerObject)
1274
+ break;
1275
+ if (CALL_EXPRESSION_TYPES.has(innerObject.type) || FIELD_ACCESS_NODE_TYPES.has(innerObject.type)) {
1276
+ current = innerObject;
1277
+ }
1278
+ else {
1279
+ return { chain, baseReceiverName: innerObject.text || undefined };
1280
+ }
1281
+ }
1282
+ else {
1283
+ // Simple identifier — this is the base receiver
1284
+ return chain.length > 0
1285
+ ? { chain, baseReceiverName: current.text || undefined }
1286
+ : undefined;
1287
+ }
1288
+ }
1289
+ return chain.length > 0 ? { chain, baseReceiverName: undefined } : undefined;
1290
+ }