@veewo/gitnexus 1.3.11 → 1.4.6-rc

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 (181) hide show
  1. package/README.md +37 -80
  2. package/dist/benchmark/agent-context/tool-runner.js +2 -2
  3. package/dist/benchmark/neonspark-candidates.js +3 -3
  4. package/dist/benchmark/tool-runner.js +2 -2
  5. package/dist/cli/ai-context.d.ts +2 -1
  6. package/dist/cli/ai-context.js +16 -12
  7. package/dist/cli/analyze.d.ts +2 -0
  8. package/dist/cli/analyze.js +68 -48
  9. package/dist/cli/augment.js +1 -1
  10. package/dist/cli/eval-server.d.ts +8 -1
  11. package/dist/cli/eval-server.js +30 -13
  12. package/dist/cli/index.js +28 -82
  13. package/dist/cli/lazy-action.d.ts +6 -0
  14. package/dist/cli/lazy-action.js +18 -0
  15. package/dist/cli/mcp.js +3 -1
  16. package/dist/cli/setup.js +87 -48
  17. package/dist/cli/setup.test.js +18 -13
  18. package/dist/cli/skill-gen.d.ts +26 -0
  19. package/dist/cli/skill-gen.js +549 -0
  20. package/dist/cli/status.js +13 -4
  21. package/dist/cli/tool.d.ts +3 -2
  22. package/dist/cli/tool.js +50 -16
  23. package/dist/cli/wiki.js +8 -4
  24. package/dist/config/ignore-service.d.ts +25 -0
  25. package/dist/config/ignore-service.js +76 -0
  26. package/dist/config/supported-languages.d.ts +4 -1
  27. package/dist/config/supported-languages.js +3 -2
  28. package/dist/core/augmentation/engine.js +94 -67
  29. package/dist/core/embeddings/embedder.d.ts +1 -1
  30. package/dist/core/embeddings/embedder.js +1 -1
  31. package/dist/core/embeddings/embedding-pipeline.d.ts +3 -3
  32. package/dist/core/embeddings/embedding-pipeline.js +52 -25
  33. package/dist/core/embeddings/types.d.ts +1 -1
  34. package/dist/core/graph/types.d.ts +7 -2
  35. package/dist/core/ingestion/ast-cache.js +3 -2
  36. package/dist/core/ingestion/call-processor.d.ts +8 -6
  37. package/dist/core/ingestion/call-processor.js +468 -206
  38. package/dist/core/ingestion/call-routing.d.ts +53 -0
  39. package/dist/core/ingestion/call-routing.js +108 -0
  40. package/dist/core/ingestion/constants.d.ts +16 -0
  41. package/dist/core/ingestion/constants.js +16 -0
  42. package/dist/core/ingestion/entry-point-scoring.d.ts +2 -1
  43. package/dist/core/ingestion/entry-point-scoring.js +116 -23
  44. package/dist/core/ingestion/export-detection.d.ts +18 -0
  45. package/dist/core/ingestion/export-detection.js +231 -0
  46. package/dist/core/ingestion/filesystem-walker.js +4 -3
  47. package/dist/core/ingestion/framework-detection.d.ts +19 -4
  48. package/dist/core/ingestion/framework-detection.js +182 -6
  49. package/dist/core/ingestion/heritage-processor.d.ts +13 -5
  50. package/dist/core/ingestion/heritage-processor.js +109 -55
  51. package/dist/core/ingestion/import-processor.d.ts +16 -20
  52. package/dist/core/ingestion/import-processor.js +199 -579
  53. package/dist/core/ingestion/language-config.d.ts +46 -0
  54. package/dist/core/ingestion/language-config.js +167 -0
  55. package/dist/core/ingestion/mro-processor.d.ts +45 -0
  56. package/dist/core/ingestion/mro-processor.js +369 -0
  57. package/dist/core/ingestion/named-binding-extraction.d.ts +61 -0
  58. package/dist/core/ingestion/named-binding-extraction.js +363 -0
  59. package/dist/core/ingestion/parsing-processor.d.ts +4 -1
  60. package/dist/core/ingestion/parsing-processor.js +107 -109
  61. package/dist/core/ingestion/pipeline.d.ts +6 -3
  62. package/dist/core/ingestion/pipeline.js +208 -114
  63. package/dist/core/ingestion/process-processor.js +8 -2
  64. package/dist/core/ingestion/resolution-context.d.ts +53 -0
  65. package/dist/core/ingestion/resolution-context.js +132 -0
  66. package/dist/core/ingestion/resolvers/csharp.d.ts +22 -0
  67. package/dist/core/ingestion/resolvers/csharp.js +109 -0
  68. package/dist/core/ingestion/resolvers/go.d.ts +19 -0
  69. package/dist/core/ingestion/resolvers/go.js +42 -0
  70. package/dist/core/ingestion/resolvers/index.d.ts +18 -0
  71. package/dist/core/ingestion/resolvers/index.js +13 -0
  72. package/dist/core/ingestion/resolvers/jvm.d.ts +23 -0
  73. package/dist/core/ingestion/resolvers/jvm.js +87 -0
  74. package/dist/core/ingestion/resolvers/php.d.ts +15 -0
  75. package/dist/core/ingestion/resolvers/php.js +35 -0
  76. package/dist/core/ingestion/resolvers/python.d.ts +19 -0
  77. package/dist/core/ingestion/resolvers/python.js +52 -0
  78. package/dist/core/ingestion/resolvers/ruby.d.ts +12 -0
  79. package/dist/core/ingestion/resolvers/ruby.js +15 -0
  80. package/dist/core/ingestion/resolvers/rust.d.ts +15 -0
  81. package/dist/core/ingestion/resolvers/rust.js +73 -0
  82. package/dist/core/ingestion/resolvers/standard.d.ts +28 -0
  83. package/dist/core/ingestion/resolvers/standard.js +123 -0
  84. package/dist/core/ingestion/resolvers/utils.d.ts +33 -0
  85. package/dist/core/ingestion/resolvers/utils.js +122 -0
  86. package/dist/core/ingestion/symbol-table.d.ts +21 -1
  87. package/dist/core/ingestion/symbol-table.js +40 -12
  88. package/dist/core/ingestion/tree-sitter-queries.d.ts +13 -10
  89. package/dist/core/ingestion/tree-sitter-queries.js +297 -7
  90. package/dist/core/ingestion/type-env.d.ts +49 -0
  91. package/dist/core/ingestion/type-env.js +611 -0
  92. package/dist/core/ingestion/type-extractors/c-cpp.d.ts +2 -0
  93. package/dist/core/ingestion/type-extractors/c-cpp.js +385 -0
  94. package/dist/core/ingestion/type-extractors/csharp.d.ts +2 -0
  95. package/dist/core/ingestion/type-extractors/csharp.js +383 -0
  96. package/dist/core/ingestion/type-extractors/go.d.ts +2 -0
  97. package/dist/core/ingestion/type-extractors/go.js +467 -0
  98. package/dist/core/ingestion/type-extractors/index.d.ts +22 -0
  99. package/dist/core/ingestion/type-extractors/index.js +31 -0
  100. package/dist/core/ingestion/type-extractors/jvm.d.ts +3 -0
  101. package/dist/core/ingestion/type-extractors/jvm.js +681 -0
  102. package/dist/core/ingestion/type-extractors/php.d.ts +2 -0
  103. package/dist/core/ingestion/type-extractors/php.js +549 -0
  104. package/dist/core/ingestion/type-extractors/python.d.ts +2 -0
  105. package/dist/core/ingestion/type-extractors/python.js +406 -0
  106. package/dist/core/ingestion/type-extractors/ruby.d.ts +2 -0
  107. package/dist/core/ingestion/type-extractors/ruby.js +389 -0
  108. package/dist/core/ingestion/type-extractors/rust.d.ts +2 -0
  109. package/dist/core/ingestion/type-extractors/rust.js +449 -0
  110. package/dist/core/ingestion/type-extractors/shared.d.ts +133 -0
  111. package/dist/core/ingestion/type-extractors/shared.js +703 -0
  112. package/dist/core/ingestion/type-extractors/swift.d.ts +2 -0
  113. package/dist/core/ingestion/type-extractors/swift.js +137 -0
  114. package/dist/core/ingestion/type-extractors/types.d.ts +127 -0
  115. package/dist/core/ingestion/type-extractors/typescript.d.ts +2 -0
  116. package/dist/core/ingestion/type-extractors/typescript.js +494 -0
  117. package/dist/core/ingestion/utils.d.ts +103 -0
  118. package/dist/core/ingestion/utils.js +1085 -4
  119. package/dist/core/ingestion/workers/parse-worker.d.ts +51 -4
  120. package/dist/core/ingestion/workers/parse-worker.js +634 -222
  121. package/dist/core/ingestion/workers/worker-pool.js +8 -0
  122. package/dist/core/{kuzu → lbug}/csv-generator.d.ts +12 -10
  123. package/dist/core/{kuzu → lbug}/csv-generator.js +82 -101
  124. package/dist/core/{kuzu/kuzu-adapter.d.ts → lbug/lbug-adapter.d.ts} +20 -25
  125. package/dist/core/{kuzu/kuzu-adapter.js → lbug/lbug-adapter.js} +150 -122
  126. package/dist/core/{kuzu → lbug}/schema.d.ts +4 -4
  127. package/dist/core/{kuzu → lbug}/schema.js +23 -22
  128. package/dist/core/lbug/schema.test.d.ts +1 -0
  129. package/dist/core/search/bm25-index.d.ts +4 -4
  130. package/dist/core/search/bm25-index.js +12 -11
  131. package/dist/core/search/hybrid-search.d.ts +2 -2
  132. package/dist/core/search/hybrid-search.js +6 -6
  133. package/dist/core/tree-sitter/parser-loader.d.ts +1 -0
  134. package/dist/core/tree-sitter/parser-loader.js +19 -0
  135. package/dist/core/wiki/generator.d.ts +2 -2
  136. package/dist/core/wiki/generator.js +6 -6
  137. package/dist/core/wiki/graph-queries.d.ts +4 -4
  138. package/dist/core/wiki/graph-queries.js +7 -7
  139. package/dist/mcp/compatible-stdio-transport.d.ts +25 -0
  140. package/dist/mcp/compatible-stdio-transport.js +200 -0
  141. package/dist/mcp/core/{kuzu-adapter.d.ts → lbug-adapter.d.ts} +11 -10
  142. package/dist/mcp/core/lbug-adapter.js +327 -0
  143. package/dist/mcp/local/local-backend.d.ts +21 -16
  144. package/dist/mcp/local/local-backend.js +306 -706
  145. package/dist/mcp/local/unity-parity-seed-loader.d.ts +6 -1
  146. package/dist/mcp/local/unity-parity-seed-loader.js +119 -9
  147. package/dist/mcp/local/unity-parity-seed-loader.test.js +95 -7
  148. package/dist/mcp/resources.js +2 -2
  149. package/dist/mcp/server.js +28 -13
  150. package/dist/mcp/staleness.js +2 -2
  151. package/dist/mcp/tools.js +12 -3
  152. package/dist/server/api.js +12 -12
  153. package/dist/server/mcp-http.d.ts +1 -1
  154. package/dist/server/mcp-http.js +1 -1
  155. package/dist/storage/git.js +4 -1
  156. package/dist/storage/repo-manager.d.ts +20 -2
  157. package/dist/storage/repo-manager.js +74 -4
  158. package/dist/types/pipeline.d.ts +1 -1
  159. package/hooks/claude/gitnexus-hook.cjs +149 -46
  160. package/hooks/claude/pre-tool-use.sh +2 -1
  161. package/hooks/claude/session-start.sh +0 -0
  162. package/package.json +20 -4
  163. package/scripts/patch-tree-sitter-swift.cjs +74 -0
  164. package/skills/gitnexus-cli.md +8 -8
  165. package/skills/gitnexus-debugging.md +1 -1
  166. package/skills/gitnexus-exploring.md +1 -1
  167. package/skills/gitnexus-guide.md +1 -1
  168. package/skills/gitnexus-impact-analysis.md +1 -1
  169. package/skills/gitnexus-pr-review.md +163 -0
  170. package/skills/gitnexus-refactoring.md +1 -1
  171. package/dist/cli/claude-hooks.d.ts +0 -22
  172. package/dist/cli/claude-hooks.js +0 -97
  173. package/dist/mcp/core/kuzu-adapter.js +0 -231
  174. /package/dist/core/{kuzu/csv-generator.test.d.ts → ingestion/type-extractors/types.js} +0 -0
  175. /package/dist/core/{kuzu/relationship-pair-buckets.test.d.ts → lbug/csv-generator.test.d.ts} +0 -0
  176. /package/dist/core/{kuzu → lbug}/csv-generator.test.js +0 -0
  177. /package/dist/core/{kuzu → lbug}/relationship-pair-buckets.d.ts +0 -0
  178. /package/dist/core/{kuzu → lbug}/relationship-pair-buckets.js +0 -0
  179. /package/dist/core/{kuzu/schema.test.d.ts → lbug/relationship-pair-buckets.test.d.ts} +0 -0
  180. /package/dist/core/{kuzu → lbug}/relationship-pair-buckets.test.js +0 -0
  181. /package/dist/core/{kuzu → lbug}/schema.test.js +0 -0
@@ -0,0 +1,703 @@
1
+ /** Empty set for containers that have no key-yielding methods */
2
+ const NO_KEYS = new Set();
3
+ /** Standard key-yielding methods across languages */
4
+ const STD_KEY_METHODS = new Set(['keys']);
5
+ const JAVA_KEY_METHODS = new Set(['keySet']);
6
+ const CSHARP_KEY_METHODS = new Set(['Keys']);
7
+ /** Standard value-yielding methods across languages */
8
+ const STD_VALUE_METHODS = new Set(['values', 'get', 'pop', 'remove']);
9
+ const CSHARP_VALUE_METHODS = new Set(['Values', 'TryGetValue']);
10
+ const SINGLE_ELEMENT_METHODS = new Set([
11
+ 'iter', 'into_iter', 'iterator', 'get', 'first', 'last', 'pop',
12
+ 'peek', 'poll', 'find', 'filter', 'map',
13
+ ]);
14
+ const CONTAINER_DESCRIPTORS = new Map([
15
+ // --- Map / Dict types (arity 2: key + value) ---
16
+ ['Map', { arity: 2, keyMethods: STD_KEY_METHODS, valueMethods: STD_VALUE_METHODS }],
17
+ ['WeakMap', { arity: 2, keyMethods: STD_KEY_METHODS, valueMethods: STD_VALUE_METHODS }],
18
+ ['HashMap', { arity: 2, keyMethods: STD_KEY_METHODS, valueMethods: STD_VALUE_METHODS }],
19
+ ['BTreeMap', { arity: 2, keyMethods: STD_KEY_METHODS, valueMethods: STD_VALUE_METHODS }],
20
+ ['LinkedHashMap', { arity: 2, keyMethods: JAVA_KEY_METHODS, valueMethods: STD_VALUE_METHODS }],
21
+ ['TreeMap', { arity: 2, keyMethods: JAVA_KEY_METHODS, valueMethods: STD_VALUE_METHODS }],
22
+ ['dict', { arity: 2, keyMethods: STD_KEY_METHODS, valueMethods: STD_VALUE_METHODS }],
23
+ ['Dict', { arity: 2, keyMethods: STD_KEY_METHODS, valueMethods: STD_VALUE_METHODS }],
24
+ ['Dictionary', { arity: 2, keyMethods: CSHARP_KEY_METHODS, valueMethods: CSHARP_VALUE_METHODS }],
25
+ ['SortedDictionary', { arity: 2, keyMethods: CSHARP_KEY_METHODS, valueMethods: CSHARP_VALUE_METHODS }],
26
+ ['Record', { arity: 2, keyMethods: STD_KEY_METHODS, valueMethods: STD_VALUE_METHODS }],
27
+ ['OrderedDict', { arity: 2, keyMethods: STD_KEY_METHODS, valueMethods: STD_VALUE_METHODS }],
28
+ ['ConcurrentHashMap', { arity: 2, keyMethods: JAVA_KEY_METHODS, valueMethods: STD_VALUE_METHODS }],
29
+ ['ConcurrentDictionary', { arity: 2, keyMethods: CSHARP_KEY_METHODS, valueMethods: CSHARP_VALUE_METHODS }],
30
+ // --- Single-element containers (arity 1) ---
31
+ ['Array', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
32
+ ['List', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
33
+ ['ArrayList', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
34
+ ['LinkedList', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
35
+ ['Vec', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
36
+ ['VecDeque', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
37
+ ['Set', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
38
+ ['HashSet', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
39
+ ['BTreeSet', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
40
+ ['TreeSet', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
41
+ ['Queue', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
42
+ ['Deque', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
43
+ ['Stack', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
44
+ ['Sequence', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
45
+ ['Iterable', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
46
+ ['Iterator', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
47
+ ['IEnumerable', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
48
+ ['IList', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
49
+ ['ICollection', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
50
+ ['Collection', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
51
+ ['ObservableCollection', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
52
+ ['IEnumerator', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
53
+ ['SortedSet', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
54
+ ['Stream', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
55
+ ['MutableList', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
56
+ ['MutableSet', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
57
+ ['LinkedHashSet', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
58
+ ['ArrayDeque', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
59
+ ['PriorityQueue', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
60
+ ['MutableMap', { arity: 2, keyMethods: STD_KEY_METHODS, valueMethods: STD_VALUE_METHODS }],
61
+ ['list', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
62
+ ['set', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
63
+ ['tuple', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
64
+ ['frozenset', { arity: 1, keyMethods: NO_KEYS, valueMethods: SINGLE_ELEMENT_METHODS }],
65
+ ]);
66
+ /** Determine which type arg to extract based on container type name and access method.
67
+ *
68
+ * Resolution order:
69
+ * 1. If container is known and method is in keyMethods → 'first'
70
+ * 2. If container is known with arity 1 → 'last' (same as 'first' for single-arg)
71
+ * 3. If container is unknown → fall back to method name heuristic
72
+ * 4. Default: 'last' (value type)
73
+ */
74
+ export function methodToTypeArgPosition(methodName, containerTypeName) {
75
+ if (containerTypeName) {
76
+ const desc = CONTAINER_DESCRIPTORS.get(containerTypeName);
77
+ if (desc) {
78
+ // Single-element container: always 'last' (= only arg)
79
+ if (desc.arity === 1)
80
+ return 'last';
81
+ // Multi-element: check if method yields key type
82
+ if (methodName && desc.keyMethods.has(methodName))
83
+ return 'first';
84
+ // Default for multi-element: value type
85
+ return 'last';
86
+ }
87
+ }
88
+ // Fallback for unknown containers: simple method name heuristic
89
+ if (methodName && (methodName === 'keys' || methodName === 'keySet' || methodName === 'Keys')) {
90
+ return 'first';
91
+ }
92
+ return 'last';
93
+ }
94
+ /** Look up the container descriptor for a type name. Exported for heritage-chain lookups. */
95
+ export function getContainerDescriptor(typeName) {
96
+ return CONTAINER_DESCRIPTORS.get(typeName);
97
+ }
98
+ /**
99
+ * Shared 3-strategy fallback for resolving the element type of a container variable.
100
+ * Used by all for-loop extractors to resolve the loop variable's type from the iterable.
101
+ *
102
+ * Strategy 1: declarationTypeNodes — raw AST type annotation node (handles container types
103
+ * where extractSimpleTypeName returned undefined, e.g., User[], List[User])
104
+ * Strategy 2: scopeEnv string — extractElementTypeFromString on the stored type string
105
+ * Strategy 3: AST walk — language-specific upward walk to enclosing function parameters
106
+ *
107
+ * @param extractFromTypeNode Language-specific function to extract element type from AST node
108
+ * @param findParamElementType Optional language-specific AST walk to find parameter type
109
+ * @param typeArgPos Which generic type arg to extract: 'first' for keys, 'last' for values (default)
110
+ */
111
+ export function resolveIterableElementType(iterableName, node, scopeEnv, declarationTypeNodes, scope, extractFromTypeNode, findParamElementType, typeArgPos = 'last') {
112
+ // Strategy 1: declarationTypeNodes AST node (check current scope, then file scope)
113
+ const typeNode = declarationTypeNodes.get(`${scope}\0${iterableName}`)
114
+ ?? (scope !== '' ? declarationTypeNodes.get(`\0${iterableName}`) : undefined);
115
+ if (typeNode) {
116
+ const t = extractFromTypeNode(typeNode, typeArgPos);
117
+ if (t)
118
+ return t;
119
+ }
120
+ // Strategy 2: scopeEnv string → extractElementTypeFromString
121
+ const iterableType = scopeEnv.get(iterableName);
122
+ if (iterableType) {
123
+ const el = extractElementTypeFromString(iterableType, typeArgPos);
124
+ if (el)
125
+ return el;
126
+ }
127
+ // Strategy 3: AST walk to function parameters
128
+ if (findParamElementType)
129
+ return findParamElementType(iterableName, node, typeArgPos);
130
+ return undefined;
131
+ }
132
+ /** Known single-arg nullable wrapper types that unwrap to their inner type
133
+ * for receiver resolution. Optional<User> → "User", Option<User> → "User".
134
+ * Only nullable wrappers — NOT containers (List, Vec) or async wrappers (Promise, Future).
135
+ * See WRAPPER_GENERICS below for the full set used in return-type inference. */
136
+ const NULLABLE_WRAPPER_TYPES = new Set([
137
+ 'Optional', // Java
138
+ 'Option', // Rust, Scala
139
+ 'Maybe', // Haskell-style, Kotlin Arrow
140
+ ]);
141
+ /**
142
+ * Extract the simple type name from a type AST node.
143
+ * Handles generic types (e.g., List<User> → List), qualified names
144
+ * (e.g., models.User → User), and nullable types (e.g., User? → User).
145
+ * Returns undefined for complex types (unions, intersections, function types).
146
+ */
147
+ export const extractSimpleTypeName = (typeNode, depth = 0) => {
148
+ if (depth > 50 || typeNode.text.length > 2048)
149
+ return undefined;
150
+ // Direct type identifier (includes Ruby 'constant' for class names)
151
+ if (typeNode.type === 'type_identifier' || typeNode.type === 'identifier'
152
+ || typeNode.type === 'simple_identifier' || typeNode.type === 'constant') {
153
+ return typeNode.text;
154
+ }
155
+ // Qualified/scoped names: take the last segment (e.g., models.User → User, Models::User → User)
156
+ if (typeNode.type === 'scoped_identifier' || typeNode.type === 'qualified_identifier'
157
+ || typeNode.type === 'scoped_type_identifier' || typeNode.type === 'qualified_name'
158
+ || typeNode.type === 'qualified_type'
159
+ || typeNode.type === 'member_expression' || typeNode.type === 'member_access_expression'
160
+ || typeNode.type === 'attribute'
161
+ || typeNode.type === 'scope_resolution'
162
+ || typeNode.type === 'selector_expression') {
163
+ const last = typeNode.lastNamedChild;
164
+ if (last && (last.type === 'type_identifier' || last.type === 'identifier'
165
+ || last.type === 'simple_identifier' || last.type === 'name'
166
+ || last.type === 'constant' || last.type === 'property_identifier'
167
+ || last.type === 'field_identifier')) {
168
+ return last.text;
169
+ }
170
+ }
171
+ // C++ template_type (e.g., vector<User>, map<string, User>): extract base name
172
+ if (typeNode.type === 'template_type') {
173
+ const base = typeNode.childForFieldName('name') ?? typeNode.firstNamedChild;
174
+ if (base)
175
+ return extractSimpleTypeName(base, depth + 1);
176
+ }
177
+ // Generic types: extract the base type (e.g., List<User> → List)
178
+ // For nullable wrappers (Optional<User>, Option<User>), unwrap to inner type.
179
+ if (typeNode.type === 'generic_type' || typeNode.type === 'parameterized_type'
180
+ || typeNode.type === 'generic_name') {
181
+ const base = typeNode.childForFieldName('name')
182
+ ?? typeNode.childForFieldName('type')
183
+ ?? typeNode.firstNamedChild;
184
+ if (!base)
185
+ return undefined;
186
+ const baseName = extractSimpleTypeName(base, depth + 1);
187
+ // Unwrap known nullable wrappers: Optional<User> → User, Option<User> → User
188
+ if (baseName && NULLABLE_WRAPPER_TYPES.has(baseName)) {
189
+ const args = extractGenericTypeArgs(typeNode);
190
+ if (args.length >= 1)
191
+ return args[0];
192
+ }
193
+ return baseName;
194
+ }
195
+ // Nullable types (Kotlin User?, C# User?)
196
+ if (typeNode.type === 'nullable_type') {
197
+ const inner = typeNode.firstNamedChild;
198
+ if (inner)
199
+ return extractSimpleTypeName(inner, depth + 1);
200
+ }
201
+ // Nullable union types (TS/JS: User | null, User | undefined, User | null | undefined)
202
+ // Extract the single non-null/undefined type from the union.
203
+ if (typeNode.type === 'union_type') {
204
+ const nonNullTypes = [];
205
+ for (let i = 0; i < typeNode.namedChildCount; i++) {
206
+ const child = typeNode.namedChild(i);
207
+ if (!child)
208
+ continue;
209
+ // Skip null/undefined/void literal types
210
+ const text = child.text;
211
+ if (text === 'null' || text === 'undefined' || text === 'void')
212
+ continue;
213
+ nonNullTypes.push(child);
214
+ }
215
+ // Only unwrap if exactly one meaningful type remains
216
+ if (nonNullTypes.length === 1) {
217
+ return extractSimpleTypeName(nonNullTypes[0], depth + 1);
218
+ }
219
+ }
220
+ // Type annotations that wrap the actual type (TS/Python: `: Foo`, Kotlin: user_type)
221
+ if (typeNode.type === 'type_annotation' || typeNode.type === 'type'
222
+ || typeNode.type === 'user_type') {
223
+ const inner = typeNode.firstNamedChild;
224
+ if (inner)
225
+ return extractSimpleTypeName(inner, depth + 1);
226
+ }
227
+ // Pointer/reference types (C++, Rust): User*, &User, &mut User
228
+ if (typeNode.type === 'pointer_type' || typeNode.type === 'reference_type') {
229
+ const inner = typeNode.firstNamedChild;
230
+ if (inner)
231
+ return extractSimpleTypeName(inner, depth + 1);
232
+ }
233
+ // Primitive/predefined types: string, int, float, bool, number, unknown, any
234
+ // PHP: primitive_type; TS/JS: predefined_type
235
+ if (typeNode.type === 'primitive_type' || typeNode.type === 'predefined_type') {
236
+ return typeNode.text;
237
+ }
238
+ // PHP named_type / optional_type
239
+ if (typeNode.type === 'named_type' || typeNode.type === 'optional_type') {
240
+ const inner = typeNode.childForFieldName('name') ?? typeNode.firstNamedChild;
241
+ if (inner)
242
+ return extractSimpleTypeName(inner, depth + 1);
243
+ }
244
+ // Name node (PHP)
245
+ if (typeNode.type === 'name') {
246
+ return typeNode.text;
247
+ }
248
+ return undefined;
249
+ };
250
+ /**
251
+ * Extract variable name from a declarator or pattern node.
252
+ * Returns the simple identifier text, or undefined for destructuring/complex patterns.
253
+ */
254
+ export const extractVarName = (node) => {
255
+ if (node.type === 'identifier' || node.type === 'simple_identifier'
256
+ || node.type === 'variable_name' || node.type === 'name'
257
+ || node.type === 'constant' || node.type === 'property_identifier') {
258
+ return node.text;
259
+ }
260
+ // variable_declarator (Java/C#): has a 'name' field
261
+ if (node.type === 'variable_declarator') {
262
+ const nameChild = node.childForFieldName('name');
263
+ if (nameChild)
264
+ return extractVarName(nameChild);
265
+ }
266
+ // Rust: let mut x = ... — mut_pattern wraps an identifier
267
+ if (node.type === 'mut_pattern') {
268
+ const inner = node.firstNamedChild;
269
+ if (inner)
270
+ return extractVarName(inner);
271
+ }
272
+ return undefined;
273
+ };
274
+ /** Node types for function/method parameters with type annotations */
275
+ export const TYPED_PARAMETER_TYPES = new Set([
276
+ 'required_parameter', // TS: (x: Foo)
277
+ 'optional_parameter', // TS: (x?: Foo)
278
+ 'formal_parameter', // Java/Kotlin
279
+ 'parameter', // C#/Rust/Go/Python/Swift
280
+ 'typed_parameter', // Python: def f(x: Foo) — distinct from 'parameter' in tree-sitter-python
281
+ 'parameter_declaration', // C/C++ void f(Type name)
282
+ 'simple_parameter', // PHP function(Foo $x)
283
+ 'property_promotion_parameter', // PHP 8.0+ constructor promotion: __construct(private Foo $x)
284
+ 'closure_parameter', // Rust: |user: User| — typed closure parameters
285
+ ]);
286
+ /**
287
+ * Extract type arguments from a generic type node.
288
+ * e.g., List<User, String> → ['User', 'String'], Vec<User> → ['User']
289
+ *
290
+ * Used by extractSimpleTypeName to unwrap nullable wrappers (Optional<User> → User).
291
+ *
292
+ * Handles language-specific AST structures:
293
+ * - TS/Java/Rust/Go: generic_type > type_arguments > type nodes
294
+ * - C#: generic_type > type_argument_list > type nodes
295
+ * - Kotlin: generic_type > type_arguments > type_projection > type nodes
296
+ *
297
+ * Note: Go slices/maps use slice_type/map_type, not generic_type — those are
298
+ * NOT handled here. Use language-specific extractors for Go container types.
299
+ *
300
+ * @param typeNode A generic_type or parameterized_type AST node (or any node —
301
+ * returns [] for non-generic types).
302
+ * @returns Array of resolved type argument names. Unresolvable arguments are omitted.
303
+ */
304
+ export const extractGenericTypeArgs = (typeNode, depth = 0) => {
305
+ if (depth > 50)
306
+ return [];
307
+ // Unwrap wrapper nodes that may sit above the generic_type
308
+ if (typeNode.type === 'type_annotation' || typeNode.type === 'type'
309
+ || typeNode.type === 'user_type' || typeNode.type === 'nullable_type'
310
+ || typeNode.type === 'optional_type') {
311
+ const inner = typeNode.firstNamedChild;
312
+ if (inner)
313
+ return extractGenericTypeArgs(inner, depth + 1);
314
+ return [];
315
+ }
316
+ // Only process generic/parameterized type nodes (includes C#'s generic_name)
317
+ if (typeNode.type !== 'generic_type' && typeNode.type !== 'parameterized_type'
318
+ && typeNode.type !== 'generic_name') {
319
+ return [];
320
+ }
321
+ // Find the type_arguments / type_argument_list child
322
+ let argsNode = null;
323
+ for (let i = 0; i < typeNode.namedChildCount; i++) {
324
+ const child = typeNode.namedChild(i);
325
+ if (child && (child.type === 'type_arguments' || child.type === 'type_argument_list')) {
326
+ argsNode = child;
327
+ break;
328
+ }
329
+ }
330
+ if (!argsNode)
331
+ return [];
332
+ const result = [];
333
+ for (let i = 0; i < argsNode.namedChildCount; i++) {
334
+ let argNode = argsNode.namedChild(i);
335
+ if (!argNode)
336
+ continue;
337
+ // Kotlin: type_arguments > type_projection > user_type > type_identifier
338
+ if (argNode.type === 'type_projection') {
339
+ argNode = argNode.firstNamedChild;
340
+ if (!argNode)
341
+ continue;
342
+ }
343
+ const name = extractSimpleTypeName(argNode);
344
+ if (name)
345
+ result.push(name);
346
+ }
347
+ return result;
348
+ };
349
+ /**
350
+ * Match Ruby constructor assignment: `user = User.new` or `service = Models::User.new`.
351
+ * Returns { varName, calleeName } or undefined if the node is not a Ruby constructor assignment.
352
+ * Handles both simple constants and scope_resolution (namespaced) receivers.
353
+ */
354
+ export const extractRubyConstructorAssignment = (node) => {
355
+ if (node.type !== 'assignment')
356
+ return undefined;
357
+ const left = node.childForFieldName('left');
358
+ const right = node.childForFieldName('right');
359
+ if (!left || !right)
360
+ return undefined;
361
+ if (left.type !== 'identifier' && left.type !== 'constant')
362
+ return undefined;
363
+ if (right.type !== 'call')
364
+ return undefined;
365
+ const method = right.childForFieldName('method');
366
+ if (!method || method.text !== 'new')
367
+ return undefined;
368
+ const receiver = right.childForFieldName('receiver');
369
+ if (!receiver)
370
+ return undefined;
371
+ let calleeName;
372
+ if (receiver.type === 'constant') {
373
+ calleeName = receiver.text;
374
+ }
375
+ else if (receiver.type === 'scope_resolution') {
376
+ // Models::User → extract last segment "User"
377
+ const last = receiver.lastNamedChild;
378
+ if (!last || last.type !== 'constant')
379
+ return undefined;
380
+ calleeName = last.text;
381
+ }
382
+ else {
383
+ return undefined;
384
+ }
385
+ return { varName: left.text, calleeName };
386
+ };
387
+ /**
388
+ * Check if an AST node has an explicit type annotation.
389
+ * Checks both named fields ('type') and child nodes ('type_annotation').
390
+ * Used by constructor binding scanners to skip annotated declarations.
391
+ */
392
+ export const hasTypeAnnotation = (node) => {
393
+ if (node.childForFieldName('type'))
394
+ return true;
395
+ for (let i = 0; i < node.childCount; i++) {
396
+ if (node.child(i)?.type === 'type_annotation')
397
+ return true;
398
+ }
399
+ return false;
400
+ };
401
+ /** Bare nullable keywords that should not produce a receiver binding. */
402
+ const NULLABLE_KEYWORDS = new Set(['null', 'undefined', 'void', 'None', 'nil']);
403
+ /**
404
+ * Strip nullable wrappers from a type name string.
405
+ * Used by both lookupInEnv (TypeEnv annotations) and extractReturnTypeName
406
+ * (return-type text) to normalize types before receiver lookup.
407
+ *
408
+ * "User | null" → "User"
409
+ * "User | undefined" → "User"
410
+ * "User | null | undefined" → "User"
411
+ * "User?" → "User"
412
+ * "User | Repo" → undefined (genuine union — refuse)
413
+ * "null" → undefined
414
+ */
415
+ export const stripNullable = (typeName) => {
416
+ let text = typeName.trim();
417
+ if (!text)
418
+ return undefined;
419
+ if (NULLABLE_KEYWORDS.has(text))
420
+ return undefined;
421
+ // Strip nullable suffix: User? → User
422
+ if (text.endsWith('?'))
423
+ text = text.slice(0, -1).trim();
424
+ // Strip union with null/undefined/None/nil/void
425
+ if (text.includes('|')) {
426
+ const parts = text.split('|').map(p => p.trim()).filter(p => p !== '' && !NULLABLE_KEYWORDS.has(p));
427
+ if (parts.length === 1)
428
+ return parts[0];
429
+ return undefined; // genuine union or all-nullable — refuse
430
+ }
431
+ return text || undefined;
432
+ };
433
+ /**
434
+ * Unwrap an await_expression to get the inner value.
435
+ * Returns the node itself if not an await_expression, or null if input is null.
436
+ */
437
+ export const unwrapAwait = (node) => {
438
+ if (!node)
439
+ return null;
440
+ return node.type === 'await_expression' ? node.firstNamedChild : node;
441
+ };
442
+ /**
443
+ * Extract the callee name from a call_expression node.
444
+ * Navigates to the 'function' field (or first named child) and extracts a simple type name.
445
+ */
446
+ export const extractCalleeName = (callNode) => {
447
+ const func = callNode.childForFieldName('function') ?? callNode.firstNamedChild;
448
+ if (!func)
449
+ return undefined;
450
+ return extractSimpleTypeName(func);
451
+ };
452
+ /** Find the first named child with the given node type */
453
+ export const findChildByType = (node, type) => {
454
+ for (let i = 0; i < node.namedChildCount; i++) {
455
+ const child = node.namedChild(i);
456
+ if (child?.type === type)
457
+ return child;
458
+ }
459
+ return null;
460
+ };
461
+ // Internal helper: extract the first comma-separated argument from a string,
462
+ // respecting nested angle-bracket and square-bracket depth.
463
+ function extractFirstArg(args) {
464
+ let depth = 0;
465
+ for (let i = 0; i < args.length; i++) {
466
+ const ch = args[i];
467
+ if (ch === '<' || ch === '[')
468
+ depth++;
469
+ else if (ch === '>' || ch === ']')
470
+ depth--;
471
+ else if (ch === ',' && depth === 0)
472
+ return args.slice(0, i).trim();
473
+ }
474
+ return args.trim();
475
+ }
476
+ /**
477
+ * Extract element type from a container type string.
478
+ * Uses bracket-balanced parsing (no regex) for generic argument extraction.
479
+ * Returns undefined for ambiguous or unparseable strings.
480
+ *
481
+ * Handles:
482
+ * - Array<User> → User (generic angle brackets)
483
+ * - User[] → User (array suffix)
484
+ * - []User → User (Go slice prefix)
485
+ * - List[User] → User (Python subscript)
486
+ * - [User] → User (Swift array sugar)
487
+ * - vector<User> → User (C++ container)
488
+ * - Vec<User> → User (Rust container)
489
+ *
490
+ * For multi-argument generics (Map<K, V>), returns the first or last type arg
491
+ * based on `pos` ('first' for keys, 'last' for values — default 'last').
492
+ * Returns undefined when the extracted type is not a simple word.
493
+ */
494
+ export function extractElementTypeFromString(typeStr, pos = 'last') {
495
+ if (!typeStr || typeStr.length === 0 || typeStr.length > 2048)
496
+ return undefined;
497
+ // 1. Array suffix: User[] → User
498
+ if (typeStr.endsWith('[]')) {
499
+ const base = typeStr.slice(0, -2).trim();
500
+ return base && /^\w+$/.test(base) ? base : undefined;
501
+ }
502
+ // 2. Go slice prefix: []User → User
503
+ if (typeStr.startsWith('[]')) {
504
+ const element = typeStr.slice(2).trim();
505
+ return element && /^\w+$/.test(element) ? element : undefined;
506
+ }
507
+ // 3. Swift array sugar: [User] → User
508
+ // Must start with '[', end with ']', and contain no angle brackets
509
+ // (to avoid confusing with List[User] handled below).
510
+ if (typeStr.startsWith('[') && typeStr.endsWith(']') && !typeStr.includes('<')) {
511
+ const element = typeStr.slice(1, -1).trim();
512
+ return element && /^\w+$/.test(element) ? element : undefined;
513
+ }
514
+ // 4. Generic bracket-balanced extraction: Array<User> / List[User] / Vec<User>
515
+ // Find the first opening bracket (< or [) and pick the one that appears first.
516
+ const openAngle = typeStr.indexOf('<');
517
+ const openSquare = typeStr.indexOf('[');
518
+ let openIdx = -1;
519
+ let openChar = '';
520
+ let closeChar = '';
521
+ if (openAngle >= 0 && (openSquare < 0 || openAngle < openSquare)) {
522
+ openIdx = openAngle;
523
+ openChar = '<';
524
+ closeChar = '>';
525
+ }
526
+ else if (openSquare >= 0) {
527
+ openIdx = openSquare;
528
+ openChar = '[';
529
+ closeChar = ']';
530
+ }
531
+ if (openIdx < 0)
532
+ return undefined;
533
+ // Walk bracket-balanced from the character after the opening bracket to find
534
+ // the matching close bracket, tracking depth for nested brackets.
535
+ // All bracket types (<, >, [, ]) contribute to depth uniformly, but only the
536
+ // selected closeChar can match at depth 0 (prevents cross-bracket miscounting).
537
+ let depth = 0;
538
+ const start = openIdx + 1;
539
+ let lastCommaIdx = -1; // Track last top-level comma for 'last' position
540
+ for (let i = start; i < typeStr.length; i++) {
541
+ const ch = typeStr[i];
542
+ if (ch === '<' || ch === '[') {
543
+ depth++;
544
+ }
545
+ else if (ch === '>' || ch === ']') {
546
+ if (depth === 0) {
547
+ // At depth 0 — only match if it is our selected close bracket.
548
+ if (ch !== closeChar)
549
+ return undefined; // mismatched bracket = malformed
550
+ if (pos === 'last' && lastCommaIdx >= 0) {
551
+ // Return last arg (text after last comma)
552
+ const lastArg = typeStr.slice(lastCommaIdx + 1, i).trim();
553
+ return lastArg && /^\w+$/.test(lastArg) ? lastArg : undefined;
554
+ }
555
+ const inner = typeStr.slice(start, i).trim();
556
+ const firstArg = extractFirstArg(inner);
557
+ return firstArg && /^\w+$/.test(firstArg) ? firstArg : undefined;
558
+ }
559
+ depth--;
560
+ }
561
+ else if (ch === ',' && depth === 0) {
562
+ if (pos === 'first') {
563
+ // Return first arg (text before first comma)
564
+ const arg = typeStr.slice(start, i).trim();
565
+ return arg && /^\w+$/.test(arg) ? arg : undefined;
566
+ }
567
+ lastCommaIdx = i;
568
+ }
569
+ }
570
+ return undefined;
571
+ }
572
+ // ── Return type text helpers ─────────────────────────────────────────────
573
+ // extractReturnTypeName works on raw return-type text already stored in
574
+ // SymbolDefinition (e.g. "User", "Promise<User>", "User | null", "*User").
575
+ // Extracts the base user-defined type name.
576
+ /** Primitive / built-in types that should NOT produce a receiver binding. */
577
+ const PRIMITIVE_TYPES = new Set([
578
+ 'string', 'number', 'boolean', 'void', 'int', 'float', 'double', 'long',
579
+ 'short', 'byte', 'char', 'bool', 'str', 'i8', 'i16', 'i32', 'i64',
580
+ 'u8', 'u16', 'u32', 'u64', 'f32', 'f64', 'usize', 'isize',
581
+ 'undefined', 'null', 'None', 'nil',
582
+ ]);
583
+ /**
584
+ * Extract a simple type name from raw return-type text.
585
+ * Handles common patterns:
586
+ * "User" → "User"
587
+ * "Promise<User>" → "User" (unwrap wrapper generics)
588
+ * "Option<User>" → "User"
589
+ * "Result<User, Error>" → "User" (first type arg)
590
+ * "User | null" → "User" (strip nullable union)
591
+ * "User?" → "User" (strip nullable suffix)
592
+ * "*User" → "User" (Go pointer)
593
+ * "&User" → "User" (Rust reference)
594
+ * Returns undefined for complex types or primitives.
595
+ */
596
+ const WRAPPER_GENERICS = new Set([
597
+ 'Promise', 'Observable', 'Future', 'CompletableFuture', 'Task', 'ValueTask', // async wrappers
598
+ 'Option', 'Some', 'Optional', 'Maybe', // nullable wrappers
599
+ 'Result', 'Either', // result wrappers
600
+ // Rust smart pointers (Deref to inner type)
601
+ 'Rc', 'Arc', 'Weak', // pointer types
602
+ 'MutexGuard', 'RwLockReadGuard', 'RwLockWriteGuard', // guard types
603
+ 'Ref', 'RefMut', // RefCell guards
604
+ 'Cow', // copy-on-write
605
+ // Containers (List, Array, Vec, Set, etc.) are intentionally excluded —
606
+ // methods are called on the container, not the element type.
607
+ // Non-wrapper generics return the base type (e.g., List) via the else branch.
608
+ ]);
609
+ /**
610
+ * Extracts the first type argument from a comma-separated generic argument string,
611
+ * respecting nested angle brackets. For example:
612
+ * "Result<User, Error>" → "Result<User, Error>" (no top-level comma)
613
+ * "User, Error" → "User"
614
+ * "Map<K, V>, string" → "Map<K, V>"
615
+ */
616
+ function extractFirstGenericArg(args) {
617
+ let depth = 0;
618
+ for (let i = 0; i < args.length; i++) {
619
+ if (args[i] === '<')
620
+ depth++;
621
+ else if (args[i] === '>')
622
+ depth--;
623
+ else if (args[i] === ',' && depth === 0)
624
+ return args.slice(0, i).trim();
625
+ }
626
+ return args.trim();
627
+ }
628
+ /**
629
+ * Extract the first non-lifetime type argument from a generic argument string.
630
+ * Skips Rust lifetime parameters (e.g., `'a`, `'_`) to find the actual type.
631
+ * "'_, User" → "User"
632
+ * "'a, User" → "User"
633
+ * "User, Error" → "User" (no lifetime — delegates to extractFirstGenericArg)
634
+ */
635
+ function extractFirstTypeArg(args) {
636
+ let remaining = args;
637
+ while (remaining) {
638
+ const first = extractFirstGenericArg(remaining);
639
+ if (!first.startsWith("'"))
640
+ return first;
641
+ // Skip past this lifetime arg + the comma separator
642
+ const commaIdx = remaining.indexOf(',', first.length);
643
+ if (commaIdx < 0)
644
+ return first; // only lifetimes — fall through
645
+ remaining = remaining.slice(commaIdx + 1).trim();
646
+ }
647
+ return args.trim();
648
+ }
649
+ const MAX_RETURN_TYPE_INPUT_LENGTH = 2048;
650
+ const MAX_RETURN_TYPE_LENGTH = 512;
651
+ export const extractReturnTypeName = (raw, depth = 0) => {
652
+ if (depth > 10)
653
+ return undefined;
654
+ if (raw.length > MAX_RETURN_TYPE_INPUT_LENGTH)
655
+ return undefined;
656
+ let text = raw.trim();
657
+ if (!text)
658
+ return undefined;
659
+ // Strip pointer/reference prefixes: *User, &User, &mut User
660
+ text = text.replace(/^[&*]+\s*(mut\s+)?/, '');
661
+ // Strip nullable suffix: User?
662
+ text = text.replace(/\?$/, '');
663
+ // Handle union types: "User | null" → "User"
664
+ if (text.includes('|')) {
665
+ const parts = text.split('|').map(p => p.trim()).filter(p => p !== 'null' && p !== 'undefined' && p !== 'void' && p !== 'None' && p !== 'nil');
666
+ if (parts.length === 1)
667
+ text = parts[0];
668
+ else
669
+ return undefined; // genuine union — too complex
670
+ }
671
+ // Handle generics: Promise<User> → unwrap if wrapper, else take base
672
+ const genericMatch = text.match(/^(\w+)\s*<(.+)>$/);
673
+ if (genericMatch) {
674
+ const [, base, args] = genericMatch;
675
+ if (WRAPPER_GENERICS.has(base)) {
676
+ // Take the first non-lifetime type argument, using bracket-balanced splitting
677
+ // so that nested generics like Result<User, Error> are not split at the inner
678
+ // comma. Lifetime parameters (Rust 'a, '_) are skipped.
679
+ const firstArg = extractFirstTypeArg(args);
680
+ return extractReturnTypeName(firstArg, depth + 1);
681
+ }
682
+ // Non-wrapper generic: return the base type (e.g., Map<K,V> → Map)
683
+ return PRIMITIVE_TYPES.has(base.toLowerCase()) ? undefined : base;
684
+ }
685
+ // Bare wrapper type without generic argument (e.g. Task, Promise, Option)
686
+ // should not produce a binding — these are meaningless without a type parameter
687
+ if (WRAPPER_GENERICS.has(text))
688
+ return undefined;
689
+ // Handle qualified names: models.User → User, Models::User → User, \App\Models\User → User
690
+ if (text.includes('::') || text.includes('.') || text.includes('\\')) {
691
+ text = text.split(/::|[.\\]/).pop();
692
+ }
693
+ // Final check: skip primitives
694
+ if (PRIMITIVE_TYPES.has(text) || PRIMITIVE_TYPES.has(text.toLowerCase()))
695
+ return undefined;
696
+ // Must start with uppercase (class/type convention) or be a valid identifier
697
+ if (!/^[A-Z_]\w*$/.test(text))
698
+ return undefined;
699
+ // If the final extracted type name is too long, reject it
700
+ if (text.length > MAX_RETURN_TYPE_LENGTH)
701
+ return undefined;
702
+ return text;
703
+ };