gitnexus 1.4.0 → 1.4.5

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 (102) hide show
  1. package/README.md +19 -18
  2. package/dist/cli/analyze.js +37 -28
  3. package/dist/cli/augment.js +1 -1
  4. package/dist/cli/eval-server.d.ts +1 -1
  5. package/dist/cli/eval-server.js +1 -1
  6. package/dist/cli/index.js +1 -0
  7. package/dist/cli/mcp.js +1 -1
  8. package/dist/cli/setup.js +25 -13
  9. package/dist/cli/status.js +13 -4
  10. package/dist/cli/tool.d.ts +1 -1
  11. package/dist/cli/tool.js +2 -2
  12. package/dist/cli/wiki.js +2 -2
  13. package/dist/config/ignore-service.d.ts +25 -0
  14. package/dist/config/ignore-service.js +76 -0
  15. package/dist/config/supported-languages.d.ts +1 -0
  16. package/dist/config/supported-languages.js +1 -1
  17. package/dist/core/augmentation/engine.js +94 -67
  18. package/dist/core/embeddings/embedder.d.ts +1 -1
  19. package/dist/core/embeddings/embedder.js +1 -1
  20. package/dist/core/embeddings/embedding-pipeline.d.ts +3 -3
  21. package/dist/core/embeddings/embedding-pipeline.js +52 -25
  22. package/dist/core/embeddings/types.d.ts +1 -1
  23. package/dist/core/ingestion/call-processor.d.ts +6 -7
  24. package/dist/core/ingestion/call-processor.js +490 -127
  25. package/dist/core/ingestion/call-routing.d.ts +53 -0
  26. package/dist/core/ingestion/call-routing.js +108 -0
  27. package/dist/core/ingestion/entry-point-scoring.js +13 -2
  28. package/dist/core/ingestion/export-detection.js +1 -0
  29. package/dist/core/ingestion/filesystem-walker.js +4 -3
  30. package/dist/core/ingestion/framework-detection.js +9 -0
  31. package/dist/core/ingestion/heritage-processor.d.ts +3 -4
  32. package/dist/core/ingestion/heritage-processor.js +40 -50
  33. package/dist/core/ingestion/import-processor.d.ts +3 -5
  34. package/dist/core/ingestion/import-processor.js +41 -10
  35. package/dist/core/ingestion/parsing-processor.d.ts +2 -1
  36. package/dist/core/ingestion/parsing-processor.js +41 -4
  37. package/dist/core/ingestion/pipeline.d.ts +5 -1
  38. package/dist/core/ingestion/pipeline.js +174 -121
  39. package/dist/core/ingestion/resolution-context.d.ts +53 -0
  40. package/dist/core/ingestion/resolution-context.js +132 -0
  41. package/dist/core/ingestion/resolvers/index.d.ts +2 -0
  42. package/dist/core/ingestion/resolvers/index.js +2 -0
  43. package/dist/core/ingestion/resolvers/python.d.ts +19 -0
  44. package/dist/core/ingestion/resolvers/python.js +52 -0
  45. package/dist/core/ingestion/resolvers/ruby.d.ts +12 -0
  46. package/dist/core/ingestion/resolvers/ruby.js +15 -0
  47. package/dist/core/ingestion/resolvers/standard.js +0 -22
  48. package/dist/core/ingestion/resolvers/utils.js +2 -0
  49. package/dist/core/ingestion/symbol-table.d.ts +3 -0
  50. package/dist/core/ingestion/symbol-table.js +1 -0
  51. package/dist/core/ingestion/tree-sitter-queries.d.ts +3 -2
  52. package/dist/core/ingestion/tree-sitter-queries.js +53 -1
  53. package/dist/core/ingestion/type-env.d.ts +32 -10
  54. package/dist/core/ingestion/type-env.js +520 -47
  55. package/dist/core/ingestion/type-extractors/c-cpp.js +326 -1
  56. package/dist/core/ingestion/type-extractors/csharp.js +282 -2
  57. package/dist/core/ingestion/type-extractors/go.js +333 -2
  58. package/dist/core/ingestion/type-extractors/index.d.ts +3 -2
  59. package/dist/core/ingestion/type-extractors/index.js +3 -1
  60. package/dist/core/ingestion/type-extractors/jvm.js +537 -4
  61. package/dist/core/ingestion/type-extractors/php.js +387 -7
  62. package/dist/core/ingestion/type-extractors/python.js +356 -5
  63. package/dist/core/ingestion/type-extractors/ruby.d.ts +2 -0
  64. package/dist/core/ingestion/type-extractors/ruby.js +389 -0
  65. package/dist/core/ingestion/type-extractors/rust.js +399 -2
  66. package/dist/core/ingestion/type-extractors/shared.d.ts +116 -1
  67. package/dist/core/ingestion/type-extractors/shared.js +488 -14
  68. package/dist/core/ingestion/type-extractors/swift.js +95 -1
  69. package/dist/core/ingestion/type-extractors/types.d.ts +81 -0
  70. package/dist/core/ingestion/type-extractors/typescript.js +436 -2
  71. package/dist/core/ingestion/utils.d.ts +33 -2
  72. package/dist/core/ingestion/utils.js +399 -27
  73. package/dist/core/ingestion/workers/parse-worker.d.ts +18 -1
  74. package/dist/core/ingestion/workers/parse-worker.js +169 -19
  75. package/dist/core/{kuzu → lbug}/csv-generator.d.ts +1 -1
  76. package/dist/core/{kuzu → lbug}/csv-generator.js +1 -1
  77. package/dist/core/{kuzu/kuzu-adapter.d.ts → lbug/lbug-adapter.d.ts} +19 -19
  78. package/dist/core/{kuzu/kuzu-adapter.js → lbug/lbug-adapter.js} +70 -65
  79. package/dist/core/{kuzu → lbug}/schema.d.ts +1 -1
  80. package/dist/core/{kuzu → lbug}/schema.js +1 -1
  81. package/dist/core/search/bm25-index.d.ts +4 -4
  82. package/dist/core/search/bm25-index.js +10 -10
  83. package/dist/core/search/hybrid-search.d.ts +2 -2
  84. package/dist/core/search/hybrid-search.js +6 -6
  85. package/dist/core/tree-sitter/parser-loader.js +9 -2
  86. package/dist/core/wiki/generator.d.ts +2 -2
  87. package/dist/core/wiki/generator.js +4 -4
  88. package/dist/core/wiki/graph-queries.d.ts +4 -4
  89. package/dist/core/wiki/graph-queries.js +7 -7
  90. package/dist/mcp/core/{kuzu-adapter.d.ts → lbug-adapter.d.ts} +7 -7
  91. package/dist/mcp/core/{kuzu-adapter.js → lbug-adapter.js} +72 -43
  92. package/dist/mcp/local/local-backend.d.ts +6 -6
  93. package/dist/mcp/local/local-backend.js +25 -18
  94. package/dist/server/api.js +12 -12
  95. package/dist/server/mcp-http.d.ts +1 -1
  96. package/dist/server/mcp-http.js +1 -1
  97. package/dist/storage/repo-manager.d.ts +20 -2
  98. package/dist/storage/repo-manager.js +55 -1
  99. package/dist/types/pipeline.d.ts +1 -1
  100. package/package.json +5 -3
  101. package/dist/core/ingestion/symbol-resolver.d.ts +0 -32
  102. package/dist/core/ingestion/symbol-resolver.js +0 -83
@@ -1,26 +1,232 @@
1
- import { FUNCTION_NODE_TYPES, extractFunctionName } from './utils.js';
1
+ import { FUNCTION_NODE_TYPES, extractFunctionName, CLASS_CONTAINER_TYPES } from './utils.js';
2
2
  import { typeConfigs, TYPED_PARAMETER_TYPES } from './type-extractors/index.js';
3
+ import { extractSimpleTypeName, extractVarName, stripNullable } from './type-extractors/shared.js';
3
4
  /** File-level scope key */
4
5
  const FILE_SCOPE = '';
6
+ /** Fallback for languages where class names aren't in a 'name' field (e.g. Kotlin uses type_identifier). */
7
+ const findTypeIdentifierChild = (node) => {
8
+ for (let i = 0; i < node.childCount; i++) {
9
+ const child = node.child(i);
10
+ if (child && child.type === 'type_identifier')
11
+ return child;
12
+ }
13
+ return null;
14
+ };
15
+ /** AST node types that represent mutually exclusive branch containers for pattern bindings. */
16
+ const PATTERN_BRANCH_TYPES = new Set([
17
+ 'when_entry', // Kotlin when
18
+ 'switch_block_label', // Java switch (enhanced)
19
+ ]);
20
+ /** Walk up the AST from a pattern node to find the enclosing branch container. */
21
+ const findPatternBranchScope = (node) => {
22
+ let current = node.parent;
23
+ while (current) {
24
+ if (PATTERN_BRANCH_TYPES.has(current.type))
25
+ return current;
26
+ if (FUNCTION_NODE_TYPES.has(current.type))
27
+ return undefined;
28
+ current = current.parent;
29
+ }
30
+ return undefined;
31
+ };
32
+ /** Bare nullable keywords that fastStripNullable must reject. */
33
+ const FAST_NULLABLE_KEYWORDS = new Set(['null', 'undefined', 'void', 'None', 'nil']);
5
34
  /**
6
- * Look up a variable's type in the TypeEnv, trying the call's enclosing
7
- * function scope first, then falling back to file-level scope.
35
+ * Fast-path nullable check: 90%+ of type names are simple identifiers (e.g. "User")
36
+ * that don't need the full stripNullable parse. Only call stripNullable when the
37
+ * string contains nullable markers ('|' for union types, '?' for nullable suffix).
8
38
  */
9
- export const lookupTypeEnv = (env, varName, callNode) => {
39
+ const fastStripNullable = (typeName) => {
40
+ if (FAST_NULLABLE_KEYWORDS.has(typeName))
41
+ return undefined;
42
+ return (typeName.indexOf('|') === -1 && typeName.indexOf('?') === -1)
43
+ ? typeName
44
+ : stripNullable(typeName);
45
+ };
46
+ /** Implementation of the lookup logic — shared between TypeEnvironment and the legacy export. */
47
+ const lookupInEnv = (env, varName, callNode, patternOverrides) => {
48
+ // Self/this receiver: resolve to enclosing class name via AST walk
49
+ if (varName === 'self' || varName === 'this' || varName === '$this') {
50
+ return findEnclosingClassName(callNode);
51
+ }
52
+ // Super/base/parent receiver: resolve to the parent class name via AST walk.
53
+ // Walks up to the enclosing class, then extracts the superclass from its heritage node.
54
+ if (varName === 'super' || varName === 'base' || varName === 'parent') {
55
+ return findEnclosingParentClassName(callNode);
56
+ }
10
57
  // Determine the enclosing function scope for the call
11
58
  const scopeKey = findEnclosingScopeKey(callNode);
59
+ // Check position-indexed pattern overrides first (e.g., Kotlin when/is smart casts).
60
+ // These take priority over flat scopeEnv because they represent per-branch narrowing.
61
+ if (scopeKey && patternOverrides) {
62
+ const varOverrides = patternOverrides.get(scopeKey)?.get(varName);
63
+ if (varOverrides) {
64
+ const pos = callNode.startIndex;
65
+ for (const override of varOverrides) {
66
+ if (pos >= override.rangeStart && pos <= override.rangeEnd) {
67
+ return fastStripNullable(override.typeName);
68
+ }
69
+ }
70
+ }
71
+ }
12
72
  // Try function-local scope first
13
73
  if (scopeKey) {
14
74
  const scopeEnv = env.get(scopeKey);
15
75
  if (scopeEnv) {
16
76
  const result = scopeEnv.get(varName);
17
77
  if (result)
18
- return result;
78
+ return fastStripNullable(result);
19
79
  }
20
80
  }
21
81
  // Fall back to file-level scope
22
82
  const fileEnv = env.get(FILE_SCOPE);
23
- return fileEnv?.get(varName);
83
+ const raw = fileEnv?.get(varName);
84
+ return raw ? fastStripNullable(raw) : undefined;
85
+ };
86
+ /**
87
+ * Walk up the AST from a node to find the enclosing class/module name.
88
+ * Used to resolve `self`/`this` receivers to their containing type.
89
+ */
90
+ const findEnclosingClassName = (node) => {
91
+ let current = node.parent;
92
+ while (current) {
93
+ if (CLASS_CONTAINER_TYPES.has(current.type)) {
94
+ const nameNode = current.childForFieldName('name')
95
+ ?? findTypeIdentifierChild(current);
96
+ if (nameNode)
97
+ return nameNode.text;
98
+ }
99
+ current = current.parent;
100
+ }
101
+ return undefined;
102
+ };
103
+ /**
104
+ * Walk up the AST to find the enclosing class, then extract its parent class name
105
+ * from the heritage/superclass AST node. Used to resolve `super`/`base`/`parent`.
106
+ *
107
+ * Supported patterns per tree-sitter grammar:
108
+ * - Java/Ruby: `superclass` field → type_identifier/constant
109
+ * - Python: `superclasses` field → argument_list → first identifier
110
+ * - TypeScript/JS: unnamed `class_heritage` child → `extends_clause` → identifier
111
+ * - C#: unnamed `base_list` child → first identifier
112
+ * - PHP: unnamed `base_clause` child → name
113
+ * - Kotlin: unnamed `delegation_specifier` child → constructor_invocation → user_type → type_identifier
114
+ * - C++: unnamed `base_class_clause` child → type_identifier
115
+ * - Swift: unnamed `inheritance_specifier` child → user_type → type_identifier
116
+ */
117
+ const findEnclosingParentClassName = (node) => {
118
+ let current = node.parent;
119
+ while (current) {
120
+ if (CLASS_CONTAINER_TYPES.has(current.type)) {
121
+ return extractParentClassFromNode(current);
122
+ }
123
+ current = current.parent;
124
+ }
125
+ return undefined;
126
+ };
127
+ /** Extract the parent/superclass name from a class declaration AST node. */
128
+ const extractParentClassFromNode = (classNode) => {
129
+ // 1. Named fields: Java (superclass), Ruby (superclass), Python (superclasses)
130
+ const superclassNode = classNode.childForFieldName('superclass');
131
+ if (superclassNode) {
132
+ // Java: superclass > type_identifier or generic_type, Ruby: superclass > constant
133
+ const inner = superclassNode.childForFieldName('type')
134
+ ?? superclassNode.firstNamedChild
135
+ ?? superclassNode;
136
+ return extractSimpleTypeName(inner) ?? inner.text;
137
+ }
138
+ const superclassesNode = classNode.childForFieldName('superclasses');
139
+ if (superclassesNode) {
140
+ // Python: argument_list with identifiers or attribute nodes (e.g. models.Model)
141
+ const first = superclassesNode.firstNamedChild;
142
+ if (first)
143
+ return extractSimpleTypeName(first) ?? first.text;
144
+ }
145
+ // 2. Unnamed children: walk class node's children looking for heritage nodes
146
+ for (let i = 0; i < classNode.childCount; i++) {
147
+ const child = classNode.child(i);
148
+ if (!child)
149
+ continue;
150
+ switch (child.type) {
151
+ // TypeScript: class_heritage > extends_clause > type_identifier
152
+ // JavaScript: class_heritage > identifier (no extends_clause wrapper)
153
+ case 'class_heritage': {
154
+ for (let j = 0; j < child.childCount; j++) {
155
+ const clause = child.child(j);
156
+ if (clause?.type === 'extends_clause') {
157
+ const typeNode = clause.firstNamedChild;
158
+ if (typeNode)
159
+ return extractSimpleTypeName(typeNode) ?? typeNode.text;
160
+ }
161
+ // JS: direct identifier child (no extends_clause wrapper)
162
+ if (clause?.type === 'identifier' || clause?.type === 'type_identifier') {
163
+ return clause.text;
164
+ }
165
+ }
166
+ break;
167
+ }
168
+ // C#: base_list > identifier or generic_name > identifier
169
+ case 'base_list': {
170
+ const first = child.firstNamedChild;
171
+ if (first) {
172
+ // generic_name wraps the identifier: BaseClass<T>
173
+ if (first.type === 'generic_name') {
174
+ const inner = first.childForFieldName('name') ?? first.firstNamedChild;
175
+ if (inner)
176
+ return inner.text;
177
+ }
178
+ return first.text;
179
+ }
180
+ break;
181
+ }
182
+ // PHP: base_clause > name
183
+ case 'base_clause': {
184
+ const name = child.firstNamedChild;
185
+ if (name)
186
+ return name.text;
187
+ break;
188
+ }
189
+ // C++: base_class_clause > type_identifier (with optional access_specifier before it)
190
+ case 'base_class_clause': {
191
+ for (let j = 0; j < child.childCount; j++) {
192
+ const inner = child.child(j);
193
+ if (inner?.type === 'type_identifier')
194
+ return inner.text;
195
+ }
196
+ break;
197
+ }
198
+ // Kotlin: delegation_specifier > constructor_invocation > user_type > type_identifier
199
+ case 'delegation_specifier': {
200
+ const delegate = child.firstNamedChild;
201
+ if (delegate?.type === 'constructor_invocation') {
202
+ const userType = delegate.firstNamedChild;
203
+ if (userType?.type === 'user_type') {
204
+ const typeId = userType.firstNamedChild;
205
+ if (typeId)
206
+ return typeId.text;
207
+ }
208
+ }
209
+ // Also handle plain user_type (interface conformance without parentheses)
210
+ if (delegate?.type === 'user_type') {
211
+ const typeId = delegate.firstNamedChild;
212
+ if (typeId)
213
+ return typeId.text;
214
+ }
215
+ break;
216
+ }
217
+ // Swift: inheritance_specifier > user_type > type_identifier
218
+ case 'inheritance_specifier': {
219
+ const userType = child.childForFieldName('inherits_from') ?? child.firstNamedChild;
220
+ if (userType?.type === 'user_type') {
221
+ const typeId = userType.firstNamedChild;
222
+ if (typeId)
223
+ return typeId.text;
224
+ }
225
+ break;
226
+ }
227
+ }
228
+ }
229
+ return undefined;
24
230
  };
25
231
  /** Find the enclosing function name for scope lookup. */
26
232
  const findEnclosingScopeKey = (node) => {
@@ -36,51 +242,318 @@ const findEnclosingScopeKey = (node) => {
36
242
  return undefined;
37
243
  };
38
244
  /**
39
- * Build a scoped TypeEnv from a tree-sitter AST for a given language.
40
- * Walks the tree tracking enclosing function scopes, so that variables
41
- * inside different functions don't collide.
245
+ * Create a lookup that checks both local AST class names AND the SymbolTable's
246
+ * global index. This allows extractInitializer functions to distinguish
247
+ * constructor calls from function calls (e.g. Kotlin `User()` vs `getUser()`)
248
+ * using cross-file type information when available.
249
+ *
250
+ * Only `.has()` is exposed — the SymbolTable doesn't support iteration.
251
+ * Results are memoized to avoid redundant lookupFuzzy scans across declarations.
42
252
  */
43
- export const buildTypeEnv = (tree, language) => {
44
- const env = new Map();
45
- walkForTypes(tree.rootNode, language, env, FILE_SCOPE);
46
- return env;
47
- };
48
- const walkForTypes = (node, language, env, currentScope) => {
49
- // Detect scope boundaries (function/method definitions)
50
- let scope = currentScope;
51
- if (FUNCTION_NODE_TYPES.has(node.type)) {
52
- const { funcName } = extractFunctionName(node);
53
- if (funcName)
54
- scope = `${funcName}@${node.startIndex}`;
55
- }
56
- // Get or create the sub-map for this scope
57
- if (!env.has(scope))
58
- env.set(scope, new Map());
59
- const scopeEnv = env.get(scope);
60
- // Check if this node provides type information
61
- extractTypeBinding(node, language, scopeEnv);
62
- // Recurse into children
63
- for (let i = 0; i < node.childCount; i++) {
64
- const child = node.child(i);
65
- if (child)
66
- walkForTypes(child, language, env, scope);
67
- }
253
+ const createClassNameLookup = (localNames, symbolTable) => {
254
+ if (!symbolTable)
255
+ return localNames;
256
+ const memo = new Map();
257
+ return {
258
+ has(name) {
259
+ if (localNames.has(name))
260
+ return true;
261
+ const cached = memo.get(name);
262
+ if (cached !== undefined)
263
+ return cached;
264
+ const result = symbolTable.lookupFuzzy(name).some(def => def.type === 'Class' || def.type === 'Enum' || def.type === 'Struct');
265
+ memo.set(name, result);
266
+ return result;
267
+ },
268
+ };
68
269
  };
69
270
  /**
70
- * Try to extract a (variableName typeName) binding from a single AST node.
71
- * Delegates to per-language type configurations.
271
+ * Build a TypeEnvironment from a tree-sitter AST for a given language.
272
+ * Single-pass: collects class/struct names, type bindings, AND constructor
273
+ * bindings that couldn't be resolved locally — all in one AST walk.
274
+ *
275
+ * When a symbolTable is provided (call-processor path), class names from across
276
+ * the project are available for constructor inference in languages like Kotlin
277
+ * where constructors are syntactically identical to function calls.
72
278
  */
73
- const extractTypeBinding = (node, language, env) => {
74
- // === PARAMETERS (most languages) ===
75
- // This guard eliminates 90%+ of calls before any language dispatch.
76
- if (TYPED_PARAMETER_TYPES.has(node.type)) {
77
- const config = typeConfigs[language];
78
- config.extractParameter(node, env);
79
- return;
80
- }
81
- // === Per-language declaration extraction ===
279
+ /**
280
+ * Node types whose subtrees can NEVER contain type-relevant descendants
281
+ * (declarations, parameters, for-loops, class definitions, pattern bindings).
282
+ * Conservative leaf-only set — verified safe across all 12 supported language grammars.
283
+ * IMPORTANT: Do NOT add expression containers (arguments, binary_expression, etc.) —
284
+ * they can contain arrow functions with typed parameters.
285
+ */
286
+ const SKIP_SUBTREE_TYPES = new Set([
287
+ // Plain string literals (NOT template_string — it contains interpolated expressions
288
+ // that can hold arrow functions with typed parameters, e.g. `${(x: T) => x}`)
289
+ 'string', 'string_literal',
290
+ 'string_content', 'string_fragment', 'heredoc_body',
291
+ // Comments
292
+ 'comment', 'line_comment', 'block_comment',
293
+ // Numeric/boolean/null literals
294
+ 'number', 'integer_literal', 'float_literal',
295
+ 'true', 'false', 'null',
296
+ // Regex
297
+ 'regex', 'regex_pattern',
298
+ ]);
299
+ export const buildTypeEnv = (tree, language, symbolTable) => {
300
+ const env = new Map();
301
+ const patternOverrides = new Map();
302
+ const localClassNames = new Set();
303
+ const classNames = createClassNameLookup(localClassNames, symbolTable);
82
304
  const config = typeConfigs[language];
83
- if (config.declarationNodeTypes.has(node.type)) {
84
- config.extractDeclaration(node, env);
305
+ const bindings = [];
306
+ // Pre-compute combined set of node types that need extractTypeBinding.
307
+ // Single Set.has() replaces 3 separate checks per node in walk().
308
+ const interestingNodeTypes = new Set();
309
+ TYPED_PARAMETER_TYPES.forEach(t => interestingNodeTypes.add(t));
310
+ config.declarationNodeTypes.forEach(t => interestingNodeTypes.add(t));
311
+ config.forLoopNodeTypes?.forEach(t => interestingNodeTypes.add(t));
312
+ const pendingAssignments = [];
313
+ // Maps `scope\0varName` → the type annotation AST node from the original declaration.
314
+ // Allows pattern extractors to navigate back to the declaration's generic type arguments
315
+ // (e.g., to extract T from Result<T, E> for `if let Ok(x) = res`).
316
+ // NOTE: This is a SUPERSET of scopeEnv — entries exist even when extractSimpleTypeName
317
+ // returns undefined for container types (User[], []User, List[User]). This is intentional:
318
+ // for-loop Strategy 1 needs the raw AST type node for exactly those container types.
319
+ const declarationTypeNodes = new Map();
320
+ /**
321
+ * Try to extract a (variableName → typeName) binding from a single AST node.
322
+ *
323
+ * Resolution tiers (first match wins):
324
+ * - Tier 0: explicit type annotations via extractDeclaration / extractForLoopBinding
325
+ * - Tier 1: constructor-call inference via extractInitializer (fallback)
326
+ *
327
+ * Side effect: populates declarationTypeNodes for variables that have an explicit
328
+ * type annotation field on the declaration node. This allows pattern extractors to
329
+ * retrieve generic type arguments from the original declaration (e.g., extracting T
330
+ * from Result<T, E> for `if let Ok(x) = res`).
331
+ */
332
+ const extractTypeBinding = (node, scopeEnv, scope) => {
333
+ // This guard eliminates 90%+ of calls before any language dispatch.
334
+ if (TYPED_PARAMETER_TYPES.has(node.type)) {
335
+ // Capture the raw type annotation BEFORE extractParameter.
336
+ // Most languages use 'name' field; Rust uses 'pattern'; TS uses 'pattern' for some param types.
337
+ // Kotlin `parameter` nodes use positional children instead of named fields,
338
+ // so we fall back to scanning children by type when childForFieldName returns null.
339
+ let typeNode = node.childForFieldName('type');
340
+ if (typeNode) {
341
+ const nameNode = node.childForFieldName('name')
342
+ ?? node.childForFieldName('pattern');
343
+ if (nameNode) {
344
+ const varName = extractVarName(nameNode);
345
+ if (varName && !declarationTypeNodes.has(`${scope}\0${varName}`)) {
346
+ declarationTypeNodes.set(`${scope}\0${varName}`, typeNode);
347
+ }
348
+ }
349
+ }
350
+ else {
351
+ // Fallback: positional children (Kotlin `parameter` → simple_identifier + user_type)
352
+ let fallbackName = null;
353
+ let fallbackType = null;
354
+ for (let i = 0; i < node.namedChildCount; i++) {
355
+ const child = node.namedChild(i);
356
+ if (!child)
357
+ continue;
358
+ if (!fallbackName && (child.type === 'simple_identifier' || child.type === 'identifier')) {
359
+ fallbackName = child;
360
+ }
361
+ if (!fallbackType && (child.type === 'user_type' || child.type === 'type_identifier'
362
+ || child.type === 'generic_type' || child.type === 'parameterized_type')) {
363
+ fallbackType = child;
364
+ }
365
+ }
366
+ if (fallbackName && fallbackType) {
367
+ const varName = extractVarName(fallbackName);
368
+ if (varName && !declarationTypeNodes.has(`${scope}\0${varName}`)) {
369
+ declarationTypeNodes.set(`${scope}\0${varName}`, fallbackType);
370
+ }
371
+ }
372
+ }
373
+ config.extractParameter(node, scopeEnv);
374
+ return;
375
+ }
376
+ // For-each loop variable bindings (Java/C#/Kotlin): explicit element types in the AST.
377
+ // Checked before declarationNodeTypes — loop variables are not declarations.
378
+ if (config.forLoopNodeTypes?.has(node.type)) {
379
+ config.extractForLoopBinding?.(node, scopeEnv, declarationTypeNodes, scope);
380
+ return;
381
+ }
382
+ if (config.declarationNodeTypes.has(node.type)) {
383
+ // Capture the raw type annotation AST node BEFORE extractDeclaration.
384
+ // This decouples type node capture from scopeEnv success — container types
385
+ // (User[], []User, List[User]) that fail extractSimpleTypeName still get
386
+ // their AST type node recorded for Strategy 1 for-loop resolution.
387
+ // Try direct extraction first (works for Go var_spec, Python assignment, Rust let_declaration).
388
+ // Try direct type field first, then unwrap wrapper nodes (C# field_declaration,
389
+ // local_declaration_statement wrap their type inside a variable_declaration child).
390
+ let typeNode = node.childForFieldName('type');
391
+ if (!typeNode) {
392
+ // C# field_declaration / local_declaration_statement wrap type inside variable_declaration.
393
+ // Use manual loop instead of namedChildren.find() to avoid array allocation on hot path.
394
+ let wrapped = node.childForFieldName('declaration');
395
+ if (!wrapped) {
396
+ for (let i = 0; i < node.namedChildCount; i++) {
397
+ const c = node.namedChild(i);
398
+ if (c?.type === 'variable_declaration') {
399
+ wrapped = c;
400
+ break;
401
+ }
402
+ }
403
+ }
404
+ if (wrapped)
405
+ typeNode = wrapped.childForFieldName('type');
406
+ }
407
+ if (typeNode) {
408
+ const nameNode = node.childForFieldName('name')
409
+ ?? node.childForFieldName('left')
410
+ ?? node.childForFieldName('pattern');
411
+ if (nameNode) {
412
+ const varName = extractVarName(nameNode);
413
+ if (varName && !declarationTypeNodes.has(`${scope}\0${varName}`)) {
414
+ declarationTypeNodes.set(`${scope}\0${varName}`, typeNode);
415
+ }
416
+ }
417
+ }
418
+ // Run the language-specific declaration extractor (may or may not add to scopeEnv).
419
+ const keysBefore = typeNode ? new Set(scopeEnv.keys()) : undefined;
420
+ config.extractDeclaration(node, scopeEnv);
421
+ // Fallback: for multi-declarator languages (TS, C#, Java) where the type field
422
+ // is on variable_declarator children, capture via keysBefore/keysAfter diff.
423
+ if (typeNode && keysBefore) {
424
+ for (const varName of scopeEnv.keys()) {
425
+ if (!keysBefore.has(varName) && !declarationTypeNodes.has(`${scope}\0${varName}`)) {
426
+ declarationTypeNodes.set(`${scope}\0${varName}`, typeNode);
427
+ }
428
+ }
429
+ }
430
+ // Tier 1: constructor-call inference as fallback.
431
+ // Always called when available — each language's extractInitializer
432
+ // internally skips declarators that already have explicit annotations,
433
+ // so this handles mixed cases like `const a: A = x, b = new B()`.
434
+ if (config.extractInitializer) {
435
+ config.extractInitializer(node, scopeEnv, classNames);
436
+ }
437
+ }
438
+ };
439
+ const walk = (node, currentScope) => {
440
+ // Fast skip: subtrees that can never contain type-relevant nodes (leaf-like literals).
441
+ if (SKIP_SUBTREE_TYPES.has(node.type))
442
+ return;
443
+ // Collect class/struct names as we encounter them (used by extractInitializer
444
+ // to distinguish constructor calls from function calls, e.g. C++ `User()` vs `getUser()`)
445
+ // Currently only C++ uses this locally; other languages rely on the SymbolTable path.
446
+ if (CLASS_CONTAINER_TYPES.has(node.type)) {
447
+ // Most languages use 'name' field; Kotlin uses a type_identifier child instead
448
+ const nameNode = node.childForFieldName('name')
449
+ ?? findTypeIdentifierChild(node);
450
+ if (nameNode)
451
+ localClassNames.add(nameNode.text);
452
+ }
453
+ // Detect scope boundaries (function/method definitions)
454
+ let scope = currentScope;
455
+ if (FUNCTION_NODE_TYPES.has(node.type)) {
456
+ const { funcName } = extractFunctionName(node);
457
+ if (funcName)
458
+ scope = `${funcName}@${node.startIndex}`;
459
+ }
460
+ // Only create scope map and call extractTypeBinding for interesting node types.
461
+ // Single Set.has() replaces 3 separate checks inside extractTypeBinding.
462
+ if (interestingNodeTypes.has(node.type)) {
463
+ if (!env.has(scope))
464
+ env.set(scope, new Map());
465
+ const scopeEnv = env.get(scope);
466
+ extractTypeBinding(node, scopeEnv, scope);
467
+ }
468
+ // Pattern binding extraction: handles constructs that introduce NEW typed variables
469
+ // via pattern matching (e.g. `if let Some(x) = opt`, `x instanceof T t`).
470
+ // Runs after Tier 0/1 so scopeEnv already contains the source variable's type.
471
+ // Conservative: extractor returns undefined when source type is unknown.
472
+ if (config.extractPatternBinding && (!config.patternBindingNodeTypes || config.patternBindingNodeTypes.has(node.type))) {
473
+ // Ensure scopeEnv exists for pattern binding reads/writes
474
+ if (!env.has(scope))
475
+ env.set(scope, new Map());
476
+ const scopeEnv = env.get(scope);
477
+ const patternBinding = config.extractPatternBinding(node, scopeEnv, declarationTypeNodes, scope);
478
+ if (patternBinding) {
479
+ if (config.allowPatternBindingOverwrite) {
480
+ // Position-indexed: store per-branch binding for smart-cast narrowing.
481
+ // Each when arm / switch case gets its own type for the variable,
482
+ // preventing cross-arm contamination (e.g., Kotlin when/is).
483
+ const branchNode = findPatternBranchScope(node);
484
+ if (branchNode) {
485
+ if (!patternOverrides.has(scope))
486
+ patternOverrides.set(scope, new Map());
487
+ const varMap = patternOverrides.get(scope);
488
+ if (!varMap.has(patternBinding.varName))
489
+ varMap.set(patternBinding.varName, []);
490
+ varMap.get(patternBinding.varName).push({
491
+ rangeStart: branchNode.startIndex,
492
+ rangeEnd: branchNode.endIndex,
493
+ typeName: patternBinding.typeName,
494
+ });
495
+ }
496
+ // Also store in flat scopeEnv as fallback (last arm wins — same as before
497
+ // for code that doesn't use position-indexed lookup).
498
+ scopeEnv.set(patternBinding.varName, patternBinding.typeName);
499
+ }
500
+ else if (!scopeEnv.has(patternBinding.varName)) {
501
+ // First-writer-wins for languages without smart-cast overwrite (Java instanceof, etc.)
502
+ scopeEnv.set(patternBinding.varName, patternBinding.typeName);
503
+ }
504
+ }
505
+ }
506
+ // Tier 2: collect plain-identifier RHS assignments for post-walk propagation.
507
+ // Delegates to per-language extractPendingAssignment — AST shapes differ widely
508
+ // (JS uses variable_declarator/name/value, Rust uses let_declaration/pattern/value,
509
+ // Python uses assignment/left/right, Go uses short_var_declaration/expression_list).
510
+ if (config.extractPendingAssignment && config.declarationNodeTypes.has(node.type)) {
511
+ // scopeEnv is guaranteed to exist here because declarationNodeTypes is a subset
512
+ // of interestingNodeTypes, so extractTypeBinding already created the scope map above.
513
+ const scopeEnv = env.get(scope);
514
+ if (scopeEnv) {
515
+ const pending = config.extractPendingAssignment(node, scopeEnv);
516
+ if (pending) {
517
+ pendingAssignments.push({ scope, ...pending });
518
+ }
519
+ }
520
+ }
521
+ // Scan for constructor bindings that couldn't be resolved locally.
522
+ // Only collect if TypeEnv didn't already resolve this binding.
523
+ if (config.scanConstructorBinding) {
524
+ const result = config.scanConstructorBinding(node);
525
+ if (result) {
526
+ const scopeEnv = env.get(scope);
527
+ if (!scopeEnv?.has(result.varName)) {
528
+ bindings.push({ scope, ...result });
529
+ }
530
+ }
531
+ }
532
+ // Recurse into children
533
+ for (let i = 0; i < node.childCount; i++) {
534
+ const child = node.child(i);
535
+ if (child)
536
+ walk(child, scope);
537
+ }
538
+ };
539
+ walk(tree.rootNode, FILE_SCOPE);
540
+ // Tier 2: single-pass assignment chain propagation in source order.
541
+ // Resolves `const b = a` where `a` has a known type from Tier 0/1.
542
+ // Multi-hop chains resolve when forward-declared (a→b→c in source order);
543
+ // reverse-order assignments are depth-1 only. No fixpoint iteration —
544
+ // this covers 95%+ of real-world patterns.
545
+ for (const { scope, lhs, rhs } of pendingAssignments) {
546
+ const scopeEnv = env.get(scope);
547
+ if (!scopeEnv || scopeEnv.has(lhs))
548
+ continue;
549
+ const rhsType = scopeEnv.get(rhs) ?? env.get(FILE_SCOPE)?.get(rhs);
550
+ if (rhsType) {
551
+ scopeEnv.set(lhs, rhsType);
552
+ }
85
553
  }
554
+ return {
555
+ lookup: (varName, callNode) => lookupInEnv(env, varName, callNode, patternOverrides),
556
+ constructorBindings: bindings,
557
+ env,
558
+ };
86
559
  };