gitnexus 1.4.7 → 1.4.9

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 (242) hide show
  1. package/README.md +29 -1
  2. package/dist/cli/ai-context.d.ts +1 -1
  3. package/dist/cli/ai-context.js +1 -1
  4. package/dist/cli/analyze.d.ts +2 -0
  5. package/dist/cli/analyze.js +54 -21
  6. package/dist/cli/index-repo.d.ts +15 -0
  7. package/dist/cli/index-repo.js +115 -0
  8. package/dist/cli/index.js +13 -3
  9. package/dist/cli/setup.js +90 -10
  10. package/dist/cli/wiki.d.ts +4 -0
  11. package/dist/cli/wiki.js +174 -53
  12. package/dist/config/supported-languages.d.ts +33 -1
  13. package/dist/config/supported-languages.js +32 -0
  14. package/dist/core/embeddings/embedder.d.ts +6 -1
  15. package/dist/core/embeddings/embedder.js +65 -5
  16. package/dist/core/embeddings/embedding-pipeline.js +11 -9
  17. package/dist/core/embeddings/http-client.d.ts +31 -0
  18. package/dist/core/embeddings/http-client.js +179 -0
  19. package/dist/core/embeddings/index.d.ts +1 -0
  20. package/dist/core/embeddings/index.js +1 -0
  21. package/dist/core/embeddings/types.d.ts +1 -1
  22. package/dist/core/graph/graph.js +9 -1
  23. package/dist/core/graph/types.d.ts +11 -2
  24. package/dist/core/ingestion/call-processor.d.ts +66 -2
  25. package/dist/core/ingestion/call-processor.js +650 -30
  26. package/dist/core/ingestion/call-routing.d.ts +9 -18
  27. package/dist/core/ingestion/call-routing.js +0 -19
  28. package/dist/core/ingestion/cobol/cobol-copy-expander.d.ts +57 -0
  29. package/dist/core/ingestion/cobol/cobol-copy-expander.js +385 -0
  30. package/dist/core/ingestion/cobol/cobol-preprocessor.d.ts +210 -0
  31. package/dist/core/ingestion/cobol/cobol-preprocessor.js +1509 -0
  32. package/dist/core/ingestion/cobol/jcl-parser.d.ts +68 -0
  33. package/dist/core/ingestion/cobol/jcl-parser.js +217 -0
  34. package/dist/core/ingestion/cobol/jcl-processor.d.ts +33 -0
  35. package/dist/core/ingestion/cobol/jcl-processor.js +229 -0
  36. package/dist/core/ingestion/cobol-processor.d.ts +54 -0
  37. package/dist/core/ingestion/cobol-processor.js +1186 -0
  38. package/dist/core/ingestion/entry-point-scoring.d.ts +17 -0
  39. package/dist/core/ingestion/entry-point-scoring.js +52 -28
  40. package/dist/core/ingestion/export-detection.d.ts +47 -8
  41. package/dist/core/ingestion/export-detection.js +29 -50
  42. package/dist/core/ingestion/field-extractor.d.ts +29 -0
  43. package/dist/core/ingestion/field-extractor.js +25 -0
  44. package/dist/core/ingestion/field-extractors/configs/c-cpp.d.ts +3 -0
  45. package/dist/core/ingestion/field-extractors/configs/c-cpp.js +108 -0
  46. package/dist/core/ingestion/field-extractors/configs/csharp.d.ts +8 -0
  47. package/dist/core/ingestion/field-extractors/configs/csharp.js +73 -0
  48. package/dist/core/ingestion/field-extractors/configs/dart.d.ts +8 -0
  49. package/dist/core/ingestion/field-extractors/configs/dart.js +76 -0
  50. package/dist/core/ingestion/field-extractors/configs/go.d.ts +11 -0
  51. package/dist/core/ingestion/field-extractors/configs/go.js +64 -0
  52. package/dist/core/ingestion/field-extractors/configs/helpers.d.ts +44 -0
  53. package/dist/core/ingestion/field-extractors/configs/helpers.js +134 -0
  54. package/dist/core/ingestion/field-extractors/configs/jvm.d.ts +3 -0
  55. package/dist/core/ingestion/field-extractors/configs/jvm.js +118 -0
  56. package/dist/core/ingestion/field-extractors/configs/php.d.ts +8 -0
  57. package/dist/core/ingestion/field-extractors/configs/php.js +67 -0
  58. package/dist/core/ingestion/field-extractors/configs/python.d.ts +12 -0
  59. package/dist/core/ingestion/field-extractors/configs/python.js +91 -0
  60. package/dist/core/ingestion/field-extractors/configs/ruby.d.ts +16 -0
  61. package/dist/core/ingestion/field-extractors/configs/ruby.js +75 -0
  62. package/dist/core/ingestion/field-extractors/configs/rust.d.ts +9 -0
  63. package/dist/core/ingestion/field-extractors/configs/rust.js +55 -0
  64. package/dist/core/ingestion/field-extractors/configs/swift.d.ts +8 -0
  65. package/dist/core/ingestion/field-extractors/configs/swift.js +63 -0
  66. package/dist/core/ingestion/field-extractors/configs/typescript-javascript.d.ts +3 -0
  67. package/dist/core/ingestion/field-extractors/configs/typescript-javascript.js +60 -0
  68. package/dist/core/ingestion/field-extractors/generic.d.ts +46 -0
  69. package/dist/core/ingestion/field-extractors/generic.js +111 -0
  70. package/dist/core/ingestion/field-extractors/typescript.d.ts +77 -0
  71. package/dist/core/ingestion/field-extractors/typescript.js +291 -0
  72. package/dist/core/ingestion/field-types.d.ts +59 -0
  73. package/dist/core/ingestion/field-types.js +2 -0
  74. package/dist/core/ingestion/framework-detection.d.ts +97 -2
  75. package/dist/core/ingestion/framework-detection.js +114 -14
  76. package/dist/core/ingestion/heritage-processor.js +62 -66
  77. package/dist/core/ingestion/import-processor.d.ts +9 -10
  78. package/dist/core/ingestion/import-processor.js +150 -196
  79. package/dist/core/ingestion/{resolvers → import-resolvers}/csharp.d.ts +6 -9
  80. package/dist/core/ingestion/{resolvers → import-resolvers}/csharp.js +20 -2
  81. package/dist/core/ingestion/import-resolvers/dart.d.ts +7 -0
  82. package/dist/core/ingestion/import-resolvers/dart.js +44 -0
  83. package/dist/core/ingestion/{resolvers → import-resolvers}/go.d.ts +4 -5
  84. package/dist/core/ingestion/{resolvers → import-resolvers}/go.js +17 -0
  85. package/dist/core/ingestion/{resolvers → import-resolvers}/jvm.d.ts +10 -1
  86. package/dist/core/ingestion/import-resolvers/jvm.js +159 -0
  87. package/dist/core/ingestion/import-resolvers/php.d.ts +25 -0
  88. package/dist/core/ingestion/import-resolvers/php.js +80 -0
  89. package/dist/core/ingestion/{resolvers → import-resolvers}/python.d.ts +9 -3
  90. package/dist/core/ingestion/{resolvers → import-resolvers}/python.js +35 -3
  91. package/dist/core/ingestion/{resolvers → import-resolvers}/ruby.d.ts +5 -2
  92. package/dist/core/ingestion/{resolvers → import-resolvers}/ruby.js +7 -2
  93. package/dist/core/ingestion/{resolvers → import-resolvers}/rust.d.ts +5 -2
  94. package/dist/core/ingestion/{resolvers → import-resolvers}/rust.js +41 -2
  95. package/dist/core/ingestion/{resolvers → import-resolvers}/standard.d.ts +15 -7
  96. package/dist/core/ingestion/{resolvers → import-resolvers}/standard.js +22 -3
  97. package/dist/core/ingestion/import-resolvers/swift.d.ts +7 -0
  98. package/dist/core/ingestion/import-resolvers/swift.js +23 -0
  99. package/dist/core/ingestion/import-resolvers/types.d.ts +44 -0
  100. package/dist/core/ingestion/import-resolvers/types.js +6 -0
  101. package/dist/core/ingestion/{resolvers → import-resolvers}/utils.d.ts +2 -0
  102. package/dist/core/ingestion/{resolvers → import-resolvers}/utils.js +7 -0
  103. package/dist/core/ingestion/language-config.d.ts +6 -0
  104. package/dist/core/ingestion/language-config.js +13 -0
  105. package/dist/core/ingestion/language-provider.d.ts +121 -0
  106. package/dist/core/ingestion/language-provider.js +24 -0
  107. package/dist/core/ingestion/languages/c-cpp.d.ts +12 -0
  108. package/dist/core/ingestion/languages/c-cpp.js +71 -0
  109. package/dist/core/ingestion/languages/cobol.d.ts +1 -0
  110. package/dist/core/ingestion/languages/cobol.js +26 -0
  111. package/dist/core/ingestion/languages/csharp.d.ts +8 -0
  112. package/dist/core/ingestion/languages/csharp.js +49 -0
  113. package/dist/core/ingestion/languages/dart.d.ts +12 -0
  114. package/dist/core/ingestion/languages/dart.js +58 -0
  115. package/dist/core/ingestion/languages/go.d.ts +11 -0
  116. package/dist/core/ingestion/languages/go.js +28 -0
  117. package/dist/core/ingestion/languages/index.d.ts +38 -0
  118. package/dist/core/ingestion/languages/index.js +63 -0
  119. package/dist/core/ingestion/languages/java.d.ts +9 -0
  120. package/dist/core/ingestion/languages/java.js +29 -0
  121. package/dist/core/ingestion/languages/kotlin.d.ts +9 -0
  122. package/dist/core/ingestion/languages/kotlin.js +53 -0
  123. package/dist/core/ingestion/languages/php.d.ts +8 -0
  124. package/dist/core/ingestion/languages/php.js +145 -0
  125. package/dist/core/ingestion/languages/python.d.ts +12 -0
  126. package/dist/core/ingestion/languages/python.js +39 -0
  127. package/dist/core/ingestion/languages/ruby.d.ts +9 -0
  128. package/dist/core/ingestion/languages/ruby.js +44 -0
  129. package/dist/core/ingestion/languages/rust.d.ts +12 -0
  130. package/dist/core/ingestion/languages/rust.js +44 -0
  131. package/dist/core/ingestion/languages/swift.d.ts +12 -0
  132. package/dist/core/ingestion/languages/swift.js +133 -0
  133. package/dist/core/ingestion/languages/typescript.d.ts +10 -0
  134. package/dist/core/ingestion/languages/typescript.js +60 -0
  135. package/dist/core/ingestion/markdown-processor.d.ts +17 -0
  136. package/dist/core/ingestion/markdown-processor.js +124 -0
  137. package/dist/core/ingestion/mro-processor.js +22 -18
  138. package/dist/core/ingestion/named-binding-processor.d.ts +18 -0
  139. package/dist/core/ingestion/named-binding-processor.js +42 -0
  140. package/dist/core/ingestion/named-bindings/csharp.d.ts +3 -0
  141. package/dist/core/ingestion/named-bindings/csharp.js +37 -0
  142. package/dist/core/ingestion/named-bindings/java.d.ts +3 -0
  143. package/dist/core/ingestion/named-bindings/java.js +29 -0
  144. package/dist/core/ingestion/named-bindings/kotlin.d.ts +3 -0
  145. package/dist/core/ingestion/named-bindings/kotlin.js +36 -0
  146. package/dist/core/ingestion/named-bindings/php.d.ts +3 -0
  147. package/dist/core/ingestion/named-bindings/php.js +61 -0
  148. package/dist/core/ingestion/named-bindings/python.d.ts +3 -0
  149. package/dist/core/ingestion/named-bindings/python.js +49 -0
  150. package/dist/core/ingestion/named-bindings/rust.d.ts +3 -0
  151. package/dist/core/ingestion/named-bindings/rust.js +64 -0
  152. package/dist/core/ingestion/named-bindings/types.d.ts +16 -0
  153. package/dist/core/ingestion/named-bindings/types.js +6 -0
  154. package/dist/core/ingestion/named-bindings/typescript.d.ts +3 -0
  155. package/dist/core/ingestion/named-bindings/typescript.js +58 -0
  156. package/dist/core/ingestion/parsing-processor.d.ts +6 -2
  157. package/dist/core/ingestion/parsing-processor.js +125 -85
  158. package/dist/core/ingestion/pipeline.d.ts +10 -0
  159. package/dist/core/ingestion/pipeline.js +1235 -317
  160. package/dist/core/ingestion/resolution-context.d.ts +5 -0
  161. package/dist/core/ingestion/resolution-context.js +8 -5
  162. package/dist/core/ingestion/route-extractors/expo.d.ts +1 -0
  163. package/dist/core/ingestion/route-extractors/expo.js +36 -0
  164. package/dist/core/ingestion/route-extractors/middleware.d.ts +47 -0
  165. package/dist/core/ingestion/route-extractors/middleware.js +143 -0
  166. package/dist/core/ingestion/route-extractors/nextjs.d.ts +3 -0
  167. package/dist/core/ingestion/route-extractors/nextjs.js +76 -0
  168. package/dist/core/ingestion/route-extractors/php.d.ts +7 -0
  169. package/dist/core/ingestion/route-extractors/php.js +21 -0
  170. package/dist/core/ingestion/route-extractors/response-shapes.d.ts +20 -0
  171. package/dist/core/ingestion/route-extractors/response-shapes.js +290 -0
  172. package/dist/core/ingestion/symbol-table.d.ts +16 -0
  173. package/dist/core/ingestion/symbol-table.js +20 -6
  174. package/dist/core/ingestion/tree-sitter-queries.d.ts +10 -9
  175. package/dist/core/ingestion/tree-sitter-queries.js +274 -11
  176. package/dist/core/ingestion/type-env.d.ts +42 -18
  177. package/dist/core/ingestion/type-env.js +481 -106
  178. package/dist/core/ingestion/type-extractors/c-cpp.d.ts +5 -0
  179. package/dist/core/ingestion/type-extractors/c-cpp.js +119 -0
  180. package/dist/core/ingestion/type-extractors/csharp.js +149 -16
  181. package/dist/core/ingestion/type-extractors/dart.d.ts +15 -0
  182. package/dist/core/ingestion/type-extractors/dart.js +371 -0
  183. package/dist/core/ingestion/type-extractors/jvm.js +169 -66
  184. package/dist/core/ingestion/type-extractors/rust.js +35 -1
  185. package/dist/core/ingestion/type-extractors/shared.d.ts +1 -15
  186. package/dist/core/ingestion/type-extractors/shared.js +14 -112
  187. package/dist/core/ingestion/type-extractors/swift.js +338 -7
  188. package/dist/core/ingestion/type-extractors/types.d.ts +40 -8
  189. package/dist/core/ingestion/type-extractors/typescript.js +141 -9
  190. package/dist/core/ingestion/utils/ast-helpers.d.ts +83 -0
  191. package/dist/core/ingestion/utils/ast-helpers.js +817 -0
  192. package/dist/core/ingestion/utils/call-analysis.d.ts +73 -0
  193. package/dist/core/ingestion/utils/call-analysis.js +527 -0
  194. package/dist/core/ingestion/utils/event-loop.d.ts +5 -0
  195. package/dist/core/ingestion/utils/event-loop.js +5 -0
  196. package/dist/core/ingestion/utils/language-detection.d.ts +9 -0
  197. package/dist/core/ingestion/utils/language-detection.js +70 -0
  198. package/dist/core/ingestion/utils/verbose.d.ts +1 -0
  199. package/dist/core/ingestion/utils/verbose.js +7 -0
  200. package/dist/core/ingestion/workers/parse-worker.d.ts +55 -5
  201. package/dist/core/ingestion/workers/parse-worker.js +415 -225
  202. package/dist/core/lbug/csv-generator.js +51 -1
  203. package/dist/core/lbug/lbug-adapter.d.ts +10 -0
  204. package/dist/core/lbug/lbug-adapter.js +75 -4
  205. package/dist/core/lbug/schema.d.ts +8 -4
  206. package/dist/core/lbug/schema.js +65 -4
  207. package/dist/core/tree-sitter/parser-loader.js +7 -1
  208. package/dist/core/wiki/cursor-client.d.ts +31 -0
  209. package/dist/core/wiki/cursor-client.js +127 -0
  210. package/dist/core/wiki/generator.d.ts +28 -9
  211. package/dist/core/wiki/generator.js +115 -18
  212. package/dist/core/wiki/graph-queries.d.ts +4 -0
  213. package/dist/core/wiki/graph-queries.js +7 -1
  214. package/dist/core/wiki/llm-client.d.ts +2 -0
  215. package/dist/core/wiki/llm-client.js +8 -4
  216. package/dist/core/wiki/prompts.d.ts +3 -3
  217. package/dist/core/wiki/prompts.js +6 -0
  218. package/dist/mcp/core/embedder.js +11 -3
  219. package/dist/mcp/core/lbug-adapter.d.ts +5 -0
  220. package/dist/mcp/core/lbug-adapter.js +23 -2
  221. package/dist/mcp/local/local-backend.d.ts +38 -5
  222. package/dist/mcp/local/local-backend.js +804 -63
  223. package/dist/mcp/resources.js +2 -0
  224. package/dist/mcp/tools.js +73 -4
  225. package/dist/server/api.d.ts +19 -1
  226. package/dist/server/api.js +66 -6
  227. package/dist/storage/git.d.ts +12 -0
  228. package/dist/storage/git.js +21 -0
  229. package/dist/storage/repo-manager.d.ts +3 -0
  230. package/package.json +25 -16
  231. package/dist/core/ingestion/named-binding-extraction.d.ts +0 -61
  232. package/dist/core/ingestion/named-binding-extraction.js +0 -363
  233. package/dist/core/ingestion/resolvers/index.d.ts +0 -18
  234. package/dist/core/ingestion/resolvers/index.js +0 -13
  235. package/dist/core/ingestion/resolvers/jvm.js +0 -87
  236. package/dist/core/ingestion/resolvers/php.d.ts +0 -15
  237. package/dist/core/ingestion/resolvers/php.js +0 -35
  238. package/dist/core/ingestion/type-extractors/index.d.ts +0 -22
  239. package/dist/core/ingestion/type-extractors/index.js +0 -31
  240. package/dist/core/ingestion/utils.d.ts +0 -138
  241. package/dist/core/ingestion/utils.js +0 -1290
  242. package/scripts/patch-tree-sitter-swift.cjs +0 -74
@@ -1,8 +1,12 @@
1
- import { FUNCTION_NODE_TYPES, extractFunctionName, CLASS_CONTAINER_TYPES, isBuiltInOrNoise } from './utils.js';
2
- import { typeConfigs, TYPED_PARAMETER_TYPES } from './type-extractors/index.js';
1
+ import { FUNCTION_NODE_TYPES, extractFunctionName, CLASS_CONTAINER_TYPES } from './utils/ast-helpers.js';
2
+ import { CALL_EXPRESSION_TYPES } from './utils/call-analysis.js';
3
+ import { TYPED_PARAMETER_TYPES } from './type-extractors/shared.js';
4
+ import { getProvider } from './languages/index.js';
3
5
  import { extractSimpleTypeName, extractVarName, stripNullable, extractReturnTypeName } from './type-extractors/shared.js';
4
6
  /** File-level scope key */
5
7
  const FILE_SCOPE = '';
8
+ /** Shared empty map for files with no file-scope bindings. */
9
+ const EMPTY_FILE_SCOPE = new Map();
6
10
  /** Fallback for languages where class names aren't in a 'name' field (e.g. Kotlin uses type_identifier). */
7
11
  const findTypeIdentifierChild = (node) => {
8
12
  for (let i = 0; i < node.childCount; i++) {
@@ -12,16 +16,21 @@ const findTypeIdentifierChild = (node) => {
12
16
  }
13
17
  return null;
14
18
  };
15
- /** AST node types that represent mutually exclusive branch containers for pattern bindings. */
16
- const PATTERN_BRANCH_TYPES = new Set([
19
+ /** AST node types that represent mutually exclusive branch containers for pattern bindings.
20
+ * Includes both multi-arm pattern-match branches AND if-statement bodies for null-check narrowing. */
21
+ const NARROWING_BRANCH_TYPES = new Set([
17
22
  'when_entry', // Kotlin when
18
23
  'switch_block_label', // Java switch (enhanced)
24
+ 'if_statement', // TS/JS, Java, C/C++
25
+ 'if_expression', // Kotlin (if is an expression)
26
+ 'statement_block', // TS/JS: { ... } body of if
27
+ 'control_structure_body', // Kotlin: body of if
19
28
  ]);
20
29
  /** Walk up the AST from a pattern node to find the enclosing branch container. */
21
- const findPatternBranchScope = (node) => {
30
+ const findNarrowingBranchScope = (node) => {
22
31
  let current = node.parent;
23
32
  while (current) {
24
- if (PATTERN_BRANCH_TYPES.has(current.type))
33
+ if (NARROWING_BRANCH_TYPES.has(current.type))
25
34
  return current;
26
35
  if (FUNCTION_NODE_TYPES.has(current.type))
27
36
  return undefined;
@@ -44,7 +53,7 @@ const fastStripNullable = (typeName) => {
44
53
  : stripNullable(typeName);
45
54
  };
46
55
  /** Implementation of the lookup logic — shared between TypeEnvironment and the legacy export. */
47
- const lookupInEnv = (env, varName, callNode, patternOverrides) => {
56
+ const lookupInEnv = (env, varName, callNode, patternOverrides, enclosingFunctionFinder) => {
48
57
  // Self/this receiver: resolve to enclosing class name via AST walk
49
58
  if (varName === 'self' || varName === 'this' || varName === '$this') {
50
59
  return findEnclosingClassName(callNode);
@@ -55,7 +64,7 @@ const lookupInEnv = (env, varName, callNode, patternOverrides) => {
55
64
  return findEnclosingParentClassName(callNode);
56
65
  }
57
66
  // Determine the enclosing function scope for the call
58
- const scopeKey = findEnclosingScopeKey(callNode);
67
+ const scopeKey = findEnclosingScopeKey(callNode, enclosingFunctionFinder);
59
68
  // Check position-indexed pattern overrides first (e.g., Kotlin when/is smart casts).
60
69
  // These take priority over flat scopeEnv because they represent per-branch narrowing.
61
70
  if (scopeKey && patternOverrides) {
@@ -83,23 +92,51 @@ const lookupInEnv = (env, varName, callNode, patternOverrides) => {
83
92
  const raw = fileEnv?.get(varName);
84
93
  return raw ? fastStripNullable(raw) : undefined;
85
94
  };
95
+ /** Per-file memoization caches for expensive parent-walk functions.
96
+ * Cleared at the start of each buildTypeEnv call (one call per file). */
97
+ const enclosingClassNameCache = new Map();
98
+ const enclosingParentClassNameCache = new Map();
86
99
  /**
87
100
  * Walk up the AST from a node to find the enclosing class/module name.
88
101
  * Used to resolve `self`/`this` receivers to their containing type.
102
+ * Memoized per-file: cache is cleared at buildTypeEnv entry.
89
103
  */
90
104
  const findEnclosingClassName = (node) => {
105
+ if (enclosingClassNameCache.has(node))
106
+ return enclosingClassNameCache.get(node);
91
107
  let current = node.parent;
92
108
  while (current) {
93
109
  if (CLASS_CONTAINER_TYPES.has(current.type)) {
94
110
  const nameNode = current.childForFieldName('name')
95
111
  ?? findTypeIdentifierChild(current);
96
- if (nameNode)
112
+ if (nameNode) {
113
+ enclosingClassNameCache.set(node, nameNode.text);
97
114
  return nameNode.text;
115
+ }
98
116
  }
99
117
  current = current.parent;
100
118
  }
119
+ enclosingClassNameCache.set(node, undefined);
101
120
  return undefined;
102
121
  };
122
+ /** Keywords that refer to the current instance across languages. */
123
+ const THIS_RECEIVERS = new Set(['this', 'self', '$this', 'Me']);
124
+ /**
125
+ * If a pending assignment's receiver is this/self/$this/Me, substitute the
126
+ * enclosing class name. Returns the item unchanged for non-receiver kinds
127
+ * or when the receiver is not a this-keyword. Properties are readonly in the
128
+ * discriminated union, so a new object is returned when substitution occurs.
129
+ */
130
+ const substituteThisReceiver = (item, node) => {
131
+ if (item.kind !== 'fieldAccess' && item.kind !== 'methodCallResult')
132
+ return item;
133
+ if (!THIS_RECEIVERS.has(item.receiver))
134
+ return item;
135
+ const className = findEnclosingClassName(node);
136
+ if (!className)
137
+ return item;
138
+ return { ...item, receiver: className };
139
+ };
103
140
  /**
104
141
  * Walk up the AST to find the enclosing class, then extract its parent class name
105
142
  * from the heritage/superclass AST node. Used to resolve `super`/`base`/`parent`.
@@ -115,13 +152,18 @@ const findEnclosingClassName = (node) => {
115
152
  * - Swift: unnamed `inheritance_specifier` child → user_type → type_identifier
116
153
  */
117
154
  const findEnclosingParentClassName = (node) => {
155
+ if (enclosingParentClassNameCache.has(node))
156
+ return enclosingParentClassNameCache.get(node);
118
157
  let current = node.parent;
119
158
  while (current) {
120
159
  if (CLASS_CONTAINER_TYPES.has(current.type)) {
121
- return extractParentClassFromNode(current);
160
+ const result = extractParentClassFromNode(current);
161
+ enclosingParentClassNameCache.set(node, result);
162
+ return result;
122
163
  }
123
164
  current = current.parent;
124
165
  }
166
+ enclosingParentClassNameCache.set(node, undefined);
125
167
  return undefined;
126
168
  };
127
169
  /** Extract the parent/superclass name from a class declaration AST node. */
@@ -228,8 +270,12 @@ const extractParentClassFromNode = (classNode) => {
228
270
  }
229
271
  return undefined;
230
272
  };
231
- /** Find the enclosing function name for scope lookup. */
232
- const findEnclosingScopeKey = (node) => {
273
+ /** Find the enclosing function name for scope lookup.
274
+ * When an `enclosingFunctionFinder` hook is provided (from the language provider),
275
+ * it is consulted for each ancestor before the default FUNCTION_NODE_TYPES check.
276
+ * This handles languages like Dart where the function body is a sibling of the
277
+ * signature instead of a child. */
278
+ const findEnclosingScopeKey = (node, enclosingFunctionFinder) => {
233
279
  let current = node.parent;
234
280
  while (current) {
235
281
  if (FUNCTION_NODE_TYPES.has(current.type)) {
@@ -237,6 +283,15 @@ const findEnclosingScopeKey = (node) => {
237
283
  if (funcName)
238
284
  return `${funcName}@${current.startIndex}`;
239
285
  }
286
+ // Language-specific hook (e.g., Dart function_body → sibling function_signature)
287
+ if (enclosingFunctionFinder) {
288
+ const result = enclosingFunctionFinder(current);
289
+ if (result) {
290
+ const sigNode = current.previousSibling;
291
+ const startIdx = sigNode?.startIndex ?? current.startIndex;
292
+ return `${result.funcName}@${startIdx}`;
293
+ }
294
+ }
240
295
  current = current.parent;
241
296
  }
242
297
  return undefined;
@@ -297,78 +352,321 @@ const SKIP_SUBTREE_TYPES = new Set([
297
352
  'regex', 'regex_pattern',
298
353
  ]);
299
354
  const CLASS_LIKE_TYPES = new Set(['Class', 'Struct', 'Interface']);
355
+ /** Memoize class definition lookups during fixpoint iteration.
356
+ * SymbolTable is immutable during type resolution, so results never change.
357
+ * Eliminates redundant array allocations + filter scans across iterations. */
358
+ const createClassDefCache = (symbolTable) => {
359
+ const cache = new Map();
360
+ return (typeName) => {
361
+ let result = cache.get(typeName);
362
+ if (result === undefined) {
363
+ result = symbolTable
364
+ ? symbolTable.lookupFuzzy(typeName).filter(d => CLASS_LIKE_TYPES.has(d.type))
365
+ : [];
366
+ cache.set(typeName, result);
367
+ }
368
+ return result;
369
+ };
370
+ };
371
+ /** AST node types representing constructor expressions across languages.
372
+ * Note: C# also has `implicit_object_creation_expression` (`new()` with type
373
+ * inference) which is NOT captured — the type is inferred, not explicit.
374
+ * Kotlin constructors use `call_expression` (no `new` keyword) — not detected. */
375
+ const CONSTRUCTOR_EXPR_TYPES = new Set([
376
+ 'new_expression', // TS/JS/C++: new Dog()
377
+ 'object_creation_expression', // Java/C#: new Dog()
378
+ ]);
379
+ /** Extract the constructor class name from a declaration node's initializer.
380
+ * Searches for new_expression / object_creation_expression in the node's subtree.
381
+ * Returns the class name or undefined if no constructor is found.
382
+ * Depth-limited to 5 to avoid expensive traversals. */
383
+ const extractConstructorTypeName = (node, depth = 0) => {
384
+ if (depth > 5)
385
+ return undefined;
386
+ if (CONSTRUCTOR_EXPR_TYPES.has(node.type)) {
387
+ // Java/C#: object_creation_expression has 'type' field
388
+ const typeField = node.childForFieldName('type');
389
+ if (typeField)
390
+ return extractSimpleTypeName(typeField);
391
+ // TS/JS: new_expression has 'constructor' field (but tree-sitter often just has identifier child)
392
+ const ctorField = node.childForFieldName('constructor');
393
+ if (ctorField)
394
+ return extractSimpleTypeName(ctorField);
395
+ // Fallback: first named child is often the class identifier
396
+ if (node.firstNamedChild)
397
+ return extractSimpleTypeName(node.firstNamedChild);
398
+ }
399
+ for (let i = 0; i < node.namedChildCount; i++) {
400
+ const child = node.namedChild(i);
401
+ if (!child)
402
+ continue;
403
+ // Don't descend into nested functions/classes or call expressions (prevents
404
+ // finding constructor args inside method calls, e.g. processAll(new Dog()))
405
+ if (FUNCTION_NODE_TYPES.has(child.type) || CLASS_CONTAINER_TYPES.has(child.type)
406
+ || CALL_EXPRESSION_TYPES.has(child.type))
407
+ continue;
408
+ const result = extractConstructorTypeName(child, depth + 1);
409
+ if (result)
410
+ return result;
411
+ }
412
+ return undefined;
413
+ };
414
+ /** Max depth for MRO parent chain walking. Real-world inheritance rarely exceeds 3-4 levels. */
415
+ const MAX_MRO_DEPTH = 5;
416
+ /** Check if `child` is a subclass of `parent` using the parentMap.
417
+ * BFS up from child, depth-limited (5), cycle-safe. */
418
+ export const isSubclassOf = (child, parent, parentMap) => {
419
+ if (!parentMap || child === parent)
420
+ return false;
421
+ const visited = new Set([child]);
422
+ let current = [child];
423
+ for (let depth = 0; depth < MAX_MRO_DEPTH && current.length > 0; depth++) {
424
+ const next = [];
425
+ for (const cls of current) {
426
+ const parents = parentMap.get(cls);
427
+ if (!parents)
428
+ continue;
429
+ for (const p of parents) {
430
+ if (p === parent)
431
+ return true;
432
+ if (!visited.has(p)) {
433
+ visited.add(p);
434
+ next.push(p);
435
+ }
436
+ }
437
+ }
438
+ current = next;
439
+ }
440
+ return false;
441
+ };
442
+ /** Walk up the parent class chain to find a field or method on an ancestor.
443
+ * BFS-like traversal with depth limit and cycle detection. First match wins.
444
+ * Used by resolveFieldType and resolveMethodReturnType when direct lookup fails. */
445
+ const walkParentChain = (typeName, parentMap, getClassDefs, lookupOnClass) => {
446
+ if (!parentMap)
447
+ return undefined;
448
+ const visited = new Set([typeName]);
449
+ let current = [typeName];
450
+ for (let depth = 0; depth < MAX_MRO_DEPTH && current.length > 0; depth++) {
451
+ const next = [];
452
+ for (const cls of current) {
453
+ const parents = parentMap.get(cls);
454
+ if (!parents)
455
+ continue;
456
+ for (const parent of parents) {
457
+ if (visited.has(parent))
458
+ continue;
459
+ visited.add(parent);
460
+ const parentDefs = getClassDefs(parent);
461
+ if (parentDefs.length === 1) {
462
+ const result = lookupOnClass(parentDefs[0].nodeId);
463
+ if (result !== undefined)
464
+ return result;
465
+ }
466
+ next.push(parent);
467
+ }
468
+ }
469
+ current = next;
470
+ }
471
+ return undefined;
472
+ };
300
473
  /** Resolve a field's declared type given a receiver variable and field name.
301
474
  * Uses SymbolTable to find the class nodeId for the receiver's type, then
302
- * looks up the field via the eagerly-populated fieldByOwner index. */
303
- const resolveFieldType = (receiver, field, scopeEnv, symbolTable) => {
475
+ * looks up the field via the eagerly-populated fieldByOwner index.
476
+ * Falls back to MRO parent chain walking if direct lookup fails (Phase 11A). */
477
+ const resolveFieldType = (receiver, field, scopeEnv, symbolTable, getClassDefs, parentMap) => {
304
478
  if (!symbolTable)
305
479
  return undefined;
306
480
  const receiverType = scopeEnv.get(receiver);
307
481
  if (!receiverType)
308
482
  return undefined;
309
- const classDefs = symbolTable.lookupFuzzy(receiverType)
310
- .filter(d => CLASS_LIKE_TYPES.has(d.type));
483
+ const lookup = getClassDefs
484
+ ?? ((name) => symbolTable.lookupFuzzy(name).filter(d => CLASS_LIKE_TYPES.has(d.type)));
485
+ const classDefs = lookup(receiverType);
311
486
  if (classDefs.length !== 1)
312
487
  return undefined;
488
+ // Direct lookup first
313
489
  const fieldDef = symbolTable.lookupFieldByOwner(classDefs[0].nodeId, field);
314
- if (!fieldDef?.declaredType)
315
- return undefined;
316
- return extractReturnTypeName(fieldDef.declaredType);
490
+ if (fieldDef?.declaredType)
491
+ return extractReturnTypeName(fieldDef.declaredType);
492
+ // MRO parent chain walking on miss
493
+ const inherited = walkParentChain(receiverType, parentMap, lookup, (nodeId) => {
494
+ const f = symbolTable.lookupFieldByOwner(nodeId, field);
495
+ return f?.declaredType ? extractReturnTypeName(f.declaredType) : undefined;
496
+ });
497
+ return inherited;
317
498
  };
318
499
  /** Resolve a method's return type given a receiver variable and method name.
319
500
  * Uses SymbolTable to find class nodeIds for the receiver's type, then
320
- * looks up the method via lookupFuzzyCallable filtered by ownerId. */
321
- const resolveMethodReturnType = (receiver, method, scopeEnv, symbolTable) => {
501
+ * looks up the method via lookupFuzzyCallable filtered by ownerId.
502
+ * Falls back to MRO parent chain walking if direct lookup fails (Phase 11A). */
503
+ const resolveMethodReturnType = (receiver, method, scopeEnv, symbolTable, getClassDefs, parentMap) => {
322
504
  if (!symbolTable)
323
505
  return undefined;
324
506
  const receiverType = scopeEnv.get(receiver);
325
507
  if (!receiverType)
326
508
  return undefined;
327
- const classDefs = symbolTable.lookupFuzzy(receiverType)
328
- .filter(d => CLASS_LIKE_TYPES.has(d.type));
509
+ const lookup = getClassDefs
510
+ ?? ((name) => symbolTable.lookupFuzzy(name).filter(d => CLASS_LIKE_TYPES.has(d.type)));
511
+ const classDefs = lookup(receiverType);
329
512
  if (classDefs.length === 0)
330
513
  return undefined;
514
+ // Direct lookup first
331
515
  const classNodeIds = new Set(classDefs.map(d => d.nodeId));
332
516
  const methods = symbolTable.lookupFuzzyCallable(method)
333
517
  .filter(d => d.ownerId && classNodeIds.has(d.ownerId));
334
- if (methods.length !== 1)
335
- return undefined;
336
- if (!methods[0].returnType)
337
- return undefined;
338
- return extractReturnTypeName(methods[0].returnType);
518
+ if (methods.length === 1 && methods[0].returnType) {
519
+ return extractReturnTypeName(methods[0].returnType);
520
+ }
521
+ // MRO parent chain walking on miss
522
+ if (methods.length === 0) {
523
+ const inherited = walkParentChain(receiverType, parentMap, lookup, (nodeId) => {
524
+ const parentMethods = symbolTable.lookupFuzzyCallable(method)
525
+ .filter(d => d.ownerId === nodeId);
526
+ if (parentMethods.length !== 1 || !parentMethods[0].returnType)
527
+ return undefined;
528
+ return extractReturnTypeName(parentMethods[0].returnType);
529
+ });
530
+ return inherited;
531
+ }
532
+ return undefined;
339
533
  };
340
- export const buildTypeEnv = (tree, language, symbolTable) => {
534
+ /**
535
+ * Unified fixpoint propagation: iterate over ALL pending items (copy, callResult,
536
+ * fieldAccess, methodCallResult) until no new bindings are produced.
537
+ * Handles arbitrary-depth mixed chains:
538
+ * const user = getUser(); // callResult → User
539
+ * const addr = user.address; // fieldAccess → Address (depends on user)
540
+ * const city = addr.getCity(); // methodCallResult → City (depends on addr)
541
+ * const alias = city; // copy → City (depends on city)
542
+ * Data flow: SymbolTable (immutable) + scopeEnv → resolve → scopeEnv.
543
+ * Termination: finite entries, each bound at most once (first-writer-wins), max 10 iterations.
544
+ */
545
+ const MAX_FIXPOINT_ITERATIONS = 10;
546
+ const resolveFixpointBindings = (pendingItems, env, returnTypeLookup, symbolTable, parentMap) => {
547
+ if (pendingItems.length === 0)
548
+ return;
549
+ const getClassDefs = createClassDefCache(symbolTable);
550
+ const resolved = new Set();
551
+ for (let iter = 0; iter < MAX_FIXPOINT_ITERATIONS; iter++) {
552
+ let changed = false;
553
+ for (let i = 0; i < pendingItems.length; i++) {
554
+ if (resolved.has(i))
555
+ continue;
556
+ const item = pendingItems[i];
557
+ const scopeEnv = env.get(item.scope);
558
+ if (!scopeEnv || scopeEnv.has(item.lhs)) {
559
+ resolved.add(i);
560
+ continue;
561
+ }
562
+ let typeName;
563
+ switch (item.kind) {
564
+ case 'callResult':
565
+ // Phase 9: Prefer FQN lookup when available for higher precision
566
+ typeName = item.calleeFqn
567
+ ? returnTypeLookup.lookupReturnType(item.calleeFqn)
568
+ : returnTypeLookup.lookupReturnType(item.callee);
569
+ break;
570
+ case 'copy':
571
+ typeName = scopeEnv.get(item.rhs) ?? env.get(FILE_SCOPE)?.get(item.rhs);
572
+ break;
573
+ case 'fieldAccess':
574
+ typeName = resolveFieldType(item.receiver, item.field, scopeEnv, symbolTable, getClassDefs, parentMap);
575
+ break;
576
+ case 'methodCallResult':
577
+ typeName = resolveMethodReturnType(item.receiver, item.method, scopeEnv, symbolTable, getClassDefs, parentMap);
578
+ break;
579
+ default: {
580
+ // Exhaustive check: TypeScript will error here if a new PendingAssignment
581
+ // kind is added without handling it in the switch.
582
+ const _exhaustive = item;
583
+ break;
584
+ }
585
+ }
586
+ if (typeName) {
587
+ scopeEnv.set(item.lhs, typeName);
588
+ resolved.add(i);
589
+ changed = true;
590
+ }
591
+ }
592
+ if (!changed)
593
+ break;
594
+ if (iter === MAX_FIXPOINT_ITERATIONS - 1 && process.env.GITNEXUS_DEBUG) {
595
+ const unresolved = pendingItems.length - resolved.size;
596
+ if (unresolved > 0) {
597
+ console.warn(`[type-env] fixpoint hit iteration cap (${MAX_FIXPOINT_ITERATIONS}), ${unresolved} items unresolved`);
598
+ }
599
+ }
600
+ }
601
+ };
602
+ /** Seed cross-file type bindings into the file scope.
603
+ * MUST be called AFTER walk() completes so that local declarations
604
+ * (Tier 0/1) always take precedence over imported bindings (first-writer-wins). */
605
+ function seedImportedBindings(env, importedBindings) {
606
+ let fileEnv = env.get(FILE_SCOPE);
607
+ if (!fileEnv) {
608
+ fileEnv = new Map();
609
+ env.set(FILE_SCOPE, fileEnv);
610
+ }
611
+ for (const [name, type] of importedBindings) {
612
+ if (!fileEnv.has(name)) {
613
+ fileEnv.set(name, type);
614
+ }
615
+ }
616
+ }
617
+ export const buildTypeEnv = (tree, language, options) => {
618
+ // Clear per-file memoization caches from the previous file.
619
+ enclosingClassNameCache.clear();
620
+ enclosingParentClassNameCache.clear();
621
+ const symbolTable = options?.symbolTable;
622
+ const parentMap = options?.parentMap;
341
623
  const env = new Map();
342
624
  const patternOverrides = new Map();
625
+ // Phase P: maps `scope\0varName` → constructor type when a declaration has BOTH
626
+ // a base type annotation AND a more specific constructor initializer.
627
+ // e.g., `Animal a = new Dog()` → constructorTypeMap.set('func@42\0a', 'Dog')
628
+ const constructorTypeMap = new Map();
343
629
  const localClassNames = new Set();
344
630
  const classNames = createClassNameLookup(localClassNames, symbolTable);
345
- const config = typeConfigs[language];
631
+ const provider = getProvider(language);
632
+ const config = provider.typeConfig;
346
633
  const bindings = [];
347
- // Build ReturnTypeLookup from optional SymbolTable.
348
- // Conservative: returns undefined when callee is ambiguous (0 or 2+ matches).
634
+ // Build ReturnTypeLookup: SymbolTable is authoritative when it has an unambiguous match.
635
+ // Cross-file importedReturnTypes are consulted ONLY when SymbolTable has 0 matches.
636
+ // Ambiguous (2+) → undefined, no cross-file fallback (conservative, local-first principle).
349
637
  const returnTypeLookup = {
350
638
  lookupReturnType(callee) {
351
- if (!symbolTable)
352
- return undefined;
353
- if (isBuiltInOrNoise(callee))
354
- return undefined;
355
- const callables = symbolTable.lookupFuzzyCallable(callee);
356
- if (callables.length !== 1)
357
- return undefined;
358
- const rawReturn = callables[0].returnType;
359
- if (!rawReturn)
360
- return undefined;
361
- return extractReturnTypeName(rawReturn);
639
+ // SymbolTable is authoritative when it has an unambiguous match
640
+ if (symbolTable) {
641
+ if (provider.isBuiltInName(callee))
642
+ return undefined;
643
+ const callables = symbolTable.lookupFuzzyCallable(callee);
644
+ if (callables.length === 1) {
645
+ const rawReturn = callables[0].returnType;
646
+ if (rawReturn)
647
+ return extractReturnTypeName(rawReturn);
648
+ }
649
+ // Ambiguous (2+) → return undefined (conservative, no cross-file fallback)
650
+ if (callables.length > 1)
651
+ return undefined;
652
+ }
653
+ // No match (0 results or no symbolTable) → fall back to cross-file
654
+ return options?.importedReturnTypes?.get(callee);
362
655
  },
363
656
  lookupRawReturnType(callee) {
364
- if (!symbolTable)
365
- return undefined;
366
- if (isBuiltInOrNoise(callee))
367
- return undefined;
368
- const callables = symbolTable.lookupFuzzyCallable(callee);
369
- if (callables.length !== 1)
370
- return undefined;
371
- return callables[0].returnType;
657
+ if (symbolTable) {
658
+ if (provider.isBuiltInName(callee))
659
+ return undefined;
660
+ const callables = symbolTable.lookupFuzzyCallable(callee);
661
+ if (callables.length === 1)
662
+ return callables[0].returnType;
663
+ // Ambiguous (2+) → return undefined (conservative, no cross-file fallback)
664
+ if (callables.length > 1)
665
+ return undefined;
666
+ }
667
+ // Cross-file fallback uses importedRawReturnTypes (raw declared types, e.g., 'User[]')
668
+ // NOT importedReturnTypes (which contains processed/simple types via extractReturnTypeName)
669
+ return options?.importedRawReturnTypes?.get(callee);
372
670
  }
373
671
  };
374
672
  // Pre-compute combined set of node types that need extractTypeBinding.
@@ -381,6 +679,9 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
381
679
  // methodCallResult items during walk(), then iterates until no new bindings are produced.
382
680
  // Handles arbitrary-depth mixed chains: callResult → fieldAccess → methodCallResult → copy.
383
681
  const pendingItems = [];
682
+ // For-loop nodes whose iterable was unresolved at walk-time. Replayed after the fixpoint
683
+ // resolves the iterable's type, bridging the walk-time/fixpoint gap (Phase 10 / ex-9B).
684
+ const pendingForLoops = [];
384
685
  // Maps `scope\0varName` → the type annotation AST node from the original declaration.
385
686
  // Allows pattern extractors to navigate back to the declaration's generic type arguments
386
687
  // (e.g., to extract T from Result<T, E> for `if let Ok(x) = res`).
@@ -432,7 +733,8 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
432
733
  fallbackName = child;
433
734
  }
434
735
  if (!fallbackType && (child.type === 'user_type' || child.type === 'type_identifier'
435
- || child.type === 'generic_type' || child.type === 'parameterized_type')) {
736
+ || child.type === 'generic_type' || child.type === 'parameterized_type'
737
+ || child.type === 'nullable_type')) {
436
738
  fallbackType = child;
437
739
  }
438
740
  }
@@ -450,8 +752,14 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
450
752
  // Checked before declarationNodeTypes — loop variables are not declarations.
451
753
  if (config.forLoopNodeTypes?.has(node.type)) {
452
754
  if (config.extractForLoopBinding) {
755
+ const sizeBefore = scopeEnv.size;
453
756
  const forLoopCtx = { scopeEnv, declarationTypeNodes, scope, returnTypeLookup };
454
757
  config.extractForLoopBinding(node, forLoopCtx);
758
+ // If no new binding was produced, the iterable's type may not yet be resolved.
759
+ // Store for post-fixpoint replay (Phase 10 / ex-9B loop-fixpoint bridge).
760
+ if (scopeEnv.size === sizeBefore) {
761
+ pendingForLoops.push({ node, scope });
762
+ }
455
763
  }
456
764
  return;
457
765
  }
@@ -477,8 +785,32 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
477
785
  }
478
786
  }
479
787
  }
480
- if (wrapped)
788
+ if (wrapped) {
481
789
  typeNode = wrapped.childForFieldName('type');
790
+ // Kotlin: variable_declaration stores the type as user_type / nullable_type
791
+ // child rather than a named 'type' field.
792
+ if (!typeNode) {
793
+ for (let i = 0; i < wrapped.namedChildCount; i++) {
794
+ const c = wrapped.namedChild(i);
795
+ if (c && (c.type === 'user_type' || c.type === 'nullable_type')) {
796
+ typeNode = c;
797
+ break;
798
+ }
799
+ }
800
+ }
801
+ }
802
+ // Swift: property_declaration has type_annotation as a direct child (not a 'type' field).
803
+ // Extract the inner type node (array_type, user_type, etc.) for declarationTypeNodes.
804
+ if (!typeNode) {
805
+ for (let i = 0; i < node.namedChildCount; i++) {
806
+ const c = node.namedChild(i);
807
+ if (c?.type === 'type_annotation') {
808
+ // Use the inner type (array_type, user_type) rather than the annotation wrapper
809
+ typeNode = c.firstNamedChild ?? c;
810
+ break;
811
+ }
812
+ }
813
+ }
482
814
  }
483
815
  if (typeNode) {
484
816
  const nameNode = node.childForFieldName('name')
@@ -492,13 +824,20 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
492
824
  }
493
825
  }
494
826
  // Run the language-specific declaration extractor (may or may not add to scopeEnv).
495
- const keysBefore = typeNode ? new Set(scopeEnv.keys()) : undefined;
827
+ const sizeBefore = typeNode ? scopeEnv.size : -1;
496
828
  config.extractDeclaration(node, scopeEnv);
497
829
  // Fallback: for multi-declarator languages (TS, C#, Java) where the type field
498
- // is on variable_declarator children, capture via keysBefore/keysAfter diff.
499
- if (typeNode && keysBefore) {
830
+ // is on variable_declarator children, capture newly-added keys.
831
+ // Map preserves insertion order, so new keys are always at the end —
832
+ // skip the first sizeBefore entries to find only newly-added variables.
833
+ if (sizeBefore >= 0 && scopeEnv.size > sizeBefore) {
834
+ let skip = sizeBefore;
500
835
  for (const varName of scopeEnv.keys()) {
501
- if (!keysBefore.has(varName) && !declarationTypeNodes.has(`${scope}\0${varName}`)) {
836
+ if (skip > 0) {
837
+ skip--;
838
+ continue;
839
+ }
840
+ if (!declarationTypeNodes.has(`${scope}\0${varName}`)) {
502
841
  declarationTypeNodes.set(`${scope}\0${varName}`, typeNode);
503
842
  }
504
843
  }
@@ -510,6 +849,35 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
510
849
  if (config.extractInitializer) {
511
850
  config.extractInitializer(node, scopeEnv, classNames);
512
851
  }
852
+ // Phase P: detect constructor-visible virtual dispatch.
853
+ // When a declaration has BOTH a type annotation AND a constructor initializer,
854
+ // record the constructor type for receiver override at call resolution time.
855
+ // e.g., `Animal a = new Dog()` → constructorTypeMap.set('scope\0a', 'Dog')
856
+ if (sizeBefore >= 0 && scopeEnv.size > sizeBefore) {
857
+ let ctorSkip = sizeBefore;
858
+ for (const varName of scopeEnv.keys()) {
859
+ if (ctorSkip > 0) {
860
+ ctorSkip--;
861
+ continue;
862
+ }
863
+ const declaredType = scopeEnv.get(varName);
864
+ if (!declaredType)
865
+ continue;
866
+ const ctorType = extractConstructorTypeName(node)
867
+ ?? config.detectConstructorType?.(node, classNames);
868
+ if (!ctorType || ctorType === declaredType)
869
+ continue;
870
+ // Unwrap wrapper types (e.g., C++ shared_ptr<Animal> → Animal) for an
871
+ // accurate isSubclassOf comparison. Language-specific via config hook.
872
+ const declTypeNode = declarationTypeNodes.get(`${scope}\0${varName}`);
873
+ const effectiveDeclaredType = (declTypeNode && config.unwrapDeclaredType)
874
+ ? (config.unwrapDeclaredType(declaredType, declTypeNode) ?? declaredType)
875
+ : declaredType;
876
+ if (ctorType !== effectiveDeclaredType) {
877
+ constructorTypeMap.set(`${scope}\0${varName}`, ctorType);
878
+ }
879
+ }
880
+ }
513
881
  }
514
882
  };
515
883
  const walk = (node, currentScope) => {
@@ -542,7 +910,8 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
542
910
  extractTypeBinding(node, scopeEnv, scope);
543
911
  }
544
912
  // Pattern binding extraction: handles constructs that introduce NEW typed variables
545
- // via pattern matching (e.g. `if let Some(x) = opt`, `x instanceof T t`).
913
+ // via pattern matching (e.g. `if let Some(x) = opt`, `x instanceof T t`)
914
+ // or narrow existing variables within a branch (null-check narrowing).
546
915
  // Runs after Tier 0/1 so scopeEnv already contains the source variable's type.
547
916
  // Conservative: extractor returns undefined when source type is unknown.
548
917
  if (config.extractPatternBinding && (!config.patternBindingNodeTypes || config.patternBindingNodeTypes.has(node.type))) {
@@ -552,11 +921,25 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
552
921
  const scopeEnv = env.get(scope);
553
922
  const patternBinding = config.extractPatternBinding(node, scopeEnv, declarationTypeNodes, scope);
554
923
  if (patternBinding) {
555
- if (config.allowPatternBindingOverwrite) {
924
+ if (patternBinding.narrowingRange) {
925
+ // Explicit narrowing range (null-check narrowing): always store in patternOverrides
926
+ // using the extractor-provided range (typically the if-body block).
927
+ if (!patternOverrides.has(scope))
928
+ patternOverrides.set(scope, new Map());
929
+ const varMap = patternOverrides.get(scope);
930
+ if (!varMap.has(patternBinding.varName))
931
+ varMap.set(patternBinding.varName, []);
932
+ varMap.get(patternBinding.varName).push({
933
+ rangeStart: patternBinding.narrowingRange.startIndex,
934
+ rangeEnd: patternBinding.narrowingRange.endIndex,
935
+ typeName: patternBinding.typeName,
936
+ });
937
+ }
938
+ else if (config.allowPatternBindingOverwrite) {
556
939
  // Position-indexed: store per-branch binding for smart-cast narrowing.
557
940
  // Each when arm / switch case gets its own type for the variable,
558
941
  // preventing cross-arm contamination (e.g., Kotlin when/is).
559
- const branchNode = findPatternBranchScope(node);
942
+ const branchNode = findNarrowingBranchScope(node);
560
943
  if (branchNode) {
561
944
  if (!patternOverrides.has(scope))
562
945
  patternOverrides.set(scope, new Map());
@@ -583,6 +966,7 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
583
966
  // Delegates to per-language extractPendingAssignment — AST shapes differ widely
584
967
  // (JS uses variable_declarator/name/value, Rust uses let_declaration/pattern/value,
585
968
  // Python uses assignment/left/right, Go uses short_var_declaration/expression_list).
969
+ // May return a single item or an array (for destructuring: N fieldAccess items).
586
970
  if (config.extractPendingAssignment && config.declarationNodeTypes.has(node.type)) {
587
971
  // scopeEnv is guaranteed to exist here because declarationNodeTypes is a subset
588
972
  // of interestingNodeTypes, so extractTypeBinding already created the scope map above.
@@ -590,7 +974,12 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
590
974
  if (scopeEnv) {
591
975
  const pending = config.extractPendingAssignment(node, scopeEnv);
592
976
  if (pending) {
593
- pendingItems.push({ scope, ...pending });
977
+ const items = Array.isArray(pending) ? pending : [pending];
978
+ for (const item of items) {
979
+ // Substitute this/self/$this/Me receivers with enclosing class name
980
+ const resolved = substituteThisReceiver(item, node);
981
+ pendingItems.push({ scope, ...resolved });
982
+ }
594
983
  }
595
984
  }
596
985
  }
@@ -613,55 +1002,41 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
613
1002
  }
614
1003
  };
615
1004
  walk(tree.rootNode, FILE_SCOPE);
616
- // Unified fixpoint propagation: iterate over ALL pending items (copy, callResult,
617
- // fieldAccess, methodCallResult) until no new bindings are produced.
618
- // Handles arbitrary-depth mixed chains:
619
- // const user = getUser(); // callResult → User
620
- // const addr = user.address; // fieldAccess → Address (depends on user)
621
- // const city = addr.getCity(); // methodCallResult City (depends on addr)
622
- // const alias = city; // copy City (depends on city)
623
- // Data flow: SymbolTable (immutable) + scopeEnv resolve scopeEnv.
624
- // Termination: finite entries, each bound at most once (first-writer-wins), max 10 iterations.
625
- const MAX_FIXPOINT_ITERATIONS = 10;
626
- const resolved = new Set();
627
- for (let iter = 0; iter < MAX_FIXPOINT_ITERATIONS; iter++) {
628
- let changed = false;
629
- for (let i = 0; i < pendingItems.length; i++) {
630
- if (resolved.has(i))
631
- continue;
632
- const item = pendingItems[i];
1005
+ // Phase 14: Seed cross-file bindings from upstream files AFTER walk
1006
+ // (local declarations from walk() take precedence first-writer-wins)
1007
+ if (options?.importedBindings && options.importedBindings.size > 0) {
1008
+ seedImportedBindings(env, options.importedBindings);
1009
+ }
1010
+ resolveFixpointBindings(pendingItems, env, returnTypeLookup, symbolTable, parentMap);
1011
+ // Post-fixpoint for-loop replay (Phase 10 / ex-9B loop-fixpoint bridge):
1012
+ // For-loop nodes whose iterables were unresolved at walk-time may now be
1013
+ // resolvable because the fixpoint bound the iterable's type.
1014
+ // Example: `const users = getUsers(); for (const u of users) { u.save(); }`
1015
+ // - walk-time: users untyped → u unresolved
1016
+ // - fixpoint: users User[]
1017
+ // - replay: users now typed → u → User
1018
+ if (pendingForLoops.length > 0 && config.extractForLoopBinding) {
1019
+ for (const { node, scope } of pendingForLoops) {
1020
+ if (!env.has(scope))
1021
+ env.set(scope, new Map());
1022
+ const scopeEnv = env.get(scope);
1023
+ config.extractForLoopBinding(node, { scopeEnv, declarationTypeNodes, scope, returnTypeLookup });
1024
+ }
1025
+ // Re-run the main fixpoint to resolve items that depended on loop variables.
1026
+ // Only needed if replay actually produced new bindings.
1027
+ const unresolvedBefore = pendingItems.filter((item) => {
633
1028
  const scopeEnv = env.get(item.scope);
634
- if (!scopeEnv || scopeEnv.has(item.lhs)) {
635
- resolved.add(i);
636
- continue;
637
- }
638
- let typeName;
639
- switch (item.kind) {
640
- case 'callResult':
641
- typeName = returnTypeLookup.lookupReturnType(item.callee);
642
- break;
643
- case 'copy':
644
- typeName = scopeEnv.get(item.rhs) ?? env.get(FILE_SCOPE)?.get(item.rhs);
645
- break;
646
- case 'fieldAccess':
647
- typeName = resolveFieldType(item.receiver, item.field, scopeEnv, symbolTable);
648
- break;
649
- case 'methodCallResult':
650
- typeName = resolveMethodReturnType(item.receiver, item.method, scopeEnv, symbolTable);
651
- break;
652
- }
653
- if (typeName) {
654
- scopeEnv.set(item.lhs, typeName);
655
- resolved.add(i);
656
- changed = true;
657
- }
1029
+ return scopeEnv && !scopeEnv.has(item.lhs);
1030
+ });
1031
+ if (unresolvedBefore.length > 0) {
1032
+ resolveFixpointBindings(unresolvedBefore, env, returnTypeLookup, symbolTable);
658
1033
  }
659
- if (!changed)
660
- break;
661
1034
  }
662
1035
  return {
663
- lookup: (varName, callNode) => lookupInEnv(env, varName, callNode, patternOverrides),
1036
+ lookup: (varName, callNode) => lookupInEnv(env, varName, callNode, patternOverrides, options?.enclosingFunctionFinder),
664
1037
  constructorBindings: bindings,
665
- env,
1038
+ fileScope: () => env.get(FILE_SCOPE) ?? EMPTY_FILE_SCOPE,
1039
+ allScopes: () => env,
1040
+ constructorTypeMap,
666
1041
  };
667
1042
  };