gitnexus 1.4.6 → 1.4.8

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 (99) hide show
  1. package/README.md +22 -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.js +2 -1
  7. package/dist/cli/setup.js +78 -1
  8. package/dist/config/supported-languages.d.ts +30 -0
  9. package/dist/config/supported-languages.js +30 -0
  10. package/dist/core/embeddings/embedder.d.ts +6 -1
  11. package/dist/core/embeddings/embedder.js +65 -5
  12. package/dist/core/embeddings/embedding-pipeline.js +11 -9
  13. package/dist/core/embeddings/http-client.d.ts +31 -0
  14. package/dist/core/embeddings/http-client.js +179 -0
  15. package/dist/core/embeddings/index.d.ts +1 -0
  16. package/dist/core/embeddings/index.js +1 -0
  17. package/dist/core/embeddings/types.d.ts +1 -1
  18. package/dist/core/graph/types.d.ts +4 -3
  19. package/dist/core/ingestion/ast-helpers.d.ts +80 -0
  20. package/dist/core/ingestion/ast-helpers.js +738 -0
  21. package/dist/core/ingestion/call-analysis.d.ts +73 -0
  22. package/dist/core/ingestion/call-analysis.js +490 -0
  23. package/dist/core/ingestion/call-processor.d.ts +55 -2
  24. package/dist/core/ingestion/call-processor.js +673 -108
  25. package/dist/core/ingestion/call-routing.d.ts +23 -2
  26. package/dist/core/ingestion/call-routing.js +21 -0
  27. package/dist/core/ingestion/entry-point-scoring.js +36 -26
  28. package/dist/core/ingestion/framework-detection.d.ts +10 -2
  29. package/dist/core/ingestion/framework-detection.js +49 -12
  30. package/dist/core/ingestion/heritage-processor.js +47 -49
  31. package/dist/core/ingestion/import-processor.d.ts +1 -1
  32. package/dist/core/ingestion/import-processor.js +103 -194
  33. package/dist/core/ingestion/import-resolution.d.ts +101 -0
  34. package/dist/core/ingestion/import-resolution.js +251 -0
  35. package/dist/core/ingestion/language-config.d.ts +3 -0
  36. package/dist/core/ingestion/language-config.js +13 -0
  37. package/dist/core/ingestion/markdown-processor.d.ts +17 -0
  38. package/dist/core/ingestion/markdown-processor.js +124 -0
  39. package/dist/core/ingestion/mro-processor.js +8 -3
  40. package/dist/core/ingestion/named-binding-extraction.d.ts +9 -43
  41. package/dist/core/ingestion/named-binding-extraction.js +89 -79
  42. package/dist/core/ingestion/parsing-processor.d.ts +3 -2
  43. package/dist/core/ingestion/parsing-processor.js +27 -60
  44. package/dist/core/ingestion/pipeline.d.ts +10 -0
  45. package/dist/core/ingestion/pipeline.js +425 -4
  46. package/dist/core/ingestion/resolution-context.d.ts +5 -0
  47. package/dist/core/ingestion/resolution-context.js +7 -4
  48. package/dist/core/ingestion/resolvers/index.d.ts +1 -1
  49. package/dist/core/ingestion/resolvers/index.js +1 -1
  50. package/dist/core/ingestion/resolvers/jvm.d.ts +2 -1
  51. package/dist/core/ingestion/resolvers/jvm.js +25 -9
  52. package/dist/core/ingestion/resolvers/php.d.ts +14 -0
  53. package/dist/core/ingestion/resolvers/php.js +43 -3
  54. package/dist/core/ingestion/resolvers/utils.d.ts +5 -0
  55. package/dist/core/ingestion/resolvers/utils.js +16 -0
  56. package/dist/core/ingestion/symbol-table.d.ts +29 -3
  57. package/dist/core/ingestion/symbol-table.js +42 -9
  58. package/dist/core/ingestion/tree-sitter-queries.d.ts +12 -12
  59. package/dist/core/ingestion/tree-sitter-queries.js +243 -2
  60. package/dist/core/ingestion/type-env.d.ts +28 -1
  61. package/dist/core/ingestion/type-env.js +451 -72
  62. package/dist/core/ingestion/type-extractors/c-cpp.d.ts +5 -0
  63. package/dist/core/ingestion/type-extractors/c-cpp.js +146 -2
  64. package/dist/core/ingestion/type-extractors/csharp.js +189 -16
  65. package/dist/core/ingestion/type-extractors/go.js +45 -0
  66. package/dist/core/ingestion/type-extractors/index.d.ts +1 -1
  67. package/dist/core/ingestion/type-extractors/index.js +1 -1
  68. package/dist/core/ingestion/type-extractors/jvm.js +244 -69
  69. package/dist/core/ingestion/type-extractors/php.js +31 -4
  70. package/dist/core/ingestion/type-extractors/python.js +89 -17
  71. package/dist/core/ingestion/type-extractors/ruby.js +17 -2
  72. package/dist/core/ingestion/type-extractors/rust.js +72 -4
  73. package/dist/core/ingestion/type-extractors/shared.d.ts +12 -2
  74. package/dist/core/ingestion/type-extractors/shared.js +115 -13
  75. package/dist/core/ingestion/type-extractors/swift.js +7 -6
  76. package/dist/core/ingestion/type-extractors/types.d.ts +54 -11
  77. package/dist/core/ingestion/type-extractors/typescript.js +171 -9
  78. package/dist/core/ingestion/utils.d.ts +2 -95
  79. package/dist/core/ingestion/utils.js +3 -892
  80. package/dist/core/ingestion/workers/parse-worker.d.ts +36 -11
  81. package/dist/core/ingestion/workers/parse-worker.js +116 -95
  82. package/dist/core/lbug/csv-generator.js +18 -1
  83. package/dist/core/lbug/lbug-adapter.d.ts +12 -0
  84. package/dist/core/lbug/lbug-adapter.js +71 -4
  85. package/dist/core/lbug/schema.d.ts +6 -4
  86. package/dist/core/lbug/schema.js +27 -3
  87. package/dist/mcp/core/embedder.js +11 -3
  88. package/dist/mcp/core/lbug-adapter.d.ts +22 -0
  89. package/dist/mcp/core/lbug-adapter.js +178 -23
  90. package/dist/mcp/local/local-backend.d.ts +22 -0
  91. package/dist/mcp/local/local-backend.js +136 -32
  92. package/dist/mcp/resources.js +13 -0
  93. package/dist/mcp/server.js +26 -4
  94. package/dist/mcp/tools.js +17 -7
  95. package/dist/server/api.d.ts +19 -1
  96. package/dist/server/api.js +66 -6
  97. package/dist/storage/git.d.ts +12 -0
  98. package/dist/storage/git.js +21 -0
  99. package/package.json +12 -4
@@ -1,4 +1,4 @@
1
- import { FUNCTION_NODE_TYPES, extractFunctionName, CLASS_CONTAINER_TYPES, isBuiltInOrNoise } from './utils.js';
1
+ import { FUNCTION_NODE_TYPES, extractFunctionName, CLASS_CONTAINER_TYPES, CALL_EXPRESSION_TYPES, isBuiltInOrNoise } from './utils.js';
2
2
  import { typeConfigs, TYPED_PARAMETER_TYPES } from './type-extractors/index.js';
3
3
  import { extractSimpleTypeName, extractVarName, stripNullable, extractReturnTypeName } from './type-extractors/shared.js';
4
4
  /** File-level scope key */
@@ -12,16 +12,21 @@ const findTypeIdentifierChild = (node) => {
12
12
  }
13
13
  return null;
14
14
  };
15
- /** AST node types that represent mutually exclusive branch containers for pattern bindings. */
16
- const PATTERN_BRANCH_TYPES = new Set([
15
+ /** AST node types that represent mutually exclusive branch containers for pattern bindings.
16
+ * Includes both multi-arm pattern-match branches AND if-statement bodies for null-check narrowing. */
17
+ const NARROWING_BRANCH_TYPES = new Set([
17
18
  'when_entry', // Kotlin when
18
19
  'switch_block_label', // Java switch (enhanced)
20
+ 'if_statement', // TS/JS, Java, C/C++
21
+ 'if_expression', // Kotlin (if is an expression)
22
+ 'statement_block', // TS/JS: { ... } body of if
23
+ 'control_structure_body', // Kotlin: body of if
19
24
  ]);
20
25
  /** Walk up the AST from a pattern node to find the enclosing branch container. */
21
- const findPatternBranchScope = (node) => {
26
+ const findNarrowingBranchScope = (node) => {
22
27
  let current = node.parent;
23
28
  while (current) {
24
- if (PATTERN_BRANCH_TYPES.has(current.type))
29
+ if (NARROWING_BRANCH_TYPES.has(current.type))
25
30
  return current;
26
31
  if (FUNCTION_NODE_TYPES.has(current.type))
27
32
  return undefined;
@@ -100,6 +105,24 @@ const findEnclosingClassName = (node) => {
100
105
  }
101
106
  return undefined;
102
107
  };
108
+ /** Keywords that refer to the current instance across languages. */
109
+ const THIS_RECEIVERS = new Set(['this', 'self', '$this', 'Me']);
110
+ /**
111
+ * If a pending assignment's receiver is this/self/$this/Me, substitute the
112
+ * enclosing class name. Returns the item unchanged for non-receiver kinds
113
+ * or when the receiver is not a this-keyword. Properties are readonly in the
114
+ * discriminated union, so a new object is returned when substitution occurs.
115
+ */
116
+ const substituteThisReceiver = (item, node) => {
117
+ if (item.kind !== 'fieldAccess' && item.kind !== 'methodCallResult')
118
+ return item;
119
+ if (!THIS_RECEIVERS.has(item.receiver))
120
+ return item;
121
+ const className = findEnclosingClassName(node);
122
+ if (!className)
123
+ return item;
124
+ return { ...item, receiver: className };
125
+ };
103
126
  /**
104
127
  * Walk up the AST to find the enclosing class, then extract its parent class name
105
128
  * from the heritage/superclass AST node. Used to resolve `super`/`base`/`parent`.
@@ -296,38 +319,315 @@ const SKIP_SUBTREE_TYPES = new Set([
296
319
  // Regex
297
320
  'regex', 'regex_pattern',
298
321
  ]);
299
- export const buildTypeEnv = (tree, language, symbolTable) => {
322
+ const CLASS_LIKE_TYPES = new Set(['Class', 'Struct', 'Interface']);
323
+ /** Memoize class definition lookups during fixpoint iteration.
324
+ * SymbolTable is immutable during type resolution, so results never change.
325
+ * Eliminates redundant array allocations + filter scans across iterations. */
326
+ const createClassDefCache = (symbolTable) => {
327
+ const cache = new Map();
328
+ return (typeName) => {
329
+ let result = cache.get(typeName);
330
+ if (result === undefined) {
331
+ result = symbolTable
332
+ ? symbolTable.lookupFuzzy(typeName).filter(d => CLASS_LIKE_TYPES.has(d.type))
333
+ : [];
334
+ cache.set(typeName, result);
335
+ }
336
+ return result;
337
+ };
338
+ };
339
+ /** AST node types representing constructor expressions across languages.
340
+ * Note: C# also has `implicit_object_creation_expression` (`new()` with type
341
+ * inference) which is NOT captured — the type is inferred, not explicit.
342
+ * Kotlin constructors use `call_expression` (no `new` keyword) — not detected. */
343
+ const CONSTRUCTOR_EXPR_TYPES = new Set([
344
+ 'new_expression', // TS/JS/C++: new Dog()
345
+ 'object_creation_expression', // Java/C#: new Dog()
346
+ ]);
347
+ /** Extract the constructor class name from a declaration node's initializer.
348
+ * Searches for new_expression / object_creation_expression in the node's subtree.
349
+ * Returns the class name or undefined if no constructor is found.
350
+ * Depth-limited to 5 to avoid expensive traversals. */
351
+ const extractConstructorTypeName = (node, depth = 0) => {
352
+ if (depth > 5)
353
+ return undefined;
354
+ if (CONSTRUCTOR_EXPR_TYPES.has(node.type)) {
355
+ // Java/C#: object_creation_expression has 'type' field
356
+ const typeField = node.childForFieldName('type');
357
+ if (typeField)
358
+ return extractSimpleTypeName(typeField);
359
+ // TS/JS: new_expression has 'constructor' field (but tree-sitter often just has identifier child)
360
+ const ctorField = node.childForFieldName('constructor');
361
+ if (ctorField)
362
+ return extractSimpleTypeName(ctorField);
363
+ // Fallback: first named child is often the class identifier
364
+ if (node.firstNamedChild)
365
+ return extractSimpleTypeName(node.firstNamedChild);
366
+ }
367
+ for (let i = 0; i < node.namedChildCount; i++) {
368
+ const child = node.namedChild(i);
369
+ if (!child)
370
+ continue;
371
+ // Don't descend into nested functions/classes or call expressions (prevents
372
+ // finding constructor args inside method calls, e.g. processAll(new Dog()))
373
+ if (FUNCTION_NODE_TYPES.has(child.type) || CLASS_CONTAINER_TYPES.has(child.type)
374
+ || CALL_EXPRESSION_TYPES.has(child.type))
375
+ continue;
376
+ const result = extractConstructorTypeName(child, depth + 1);
377
+ if (result)
378
+ return result;
379
+ }
380
+ return undefined;
381
+ };
382
+ /** Max depth for MRO parent chain walking. Real-world inheritance rarely exceeds 3-4 levels. */
383
+ const MAX_MRO_DEPTH = 5;
384
+ /** Check if `child` is a subclass of `parent` using the parentMap.
385
+ * BFS up from child, depth-limited (5), cycle-safe. */
386
+ export const isSubclassOf = (child, parent, parentMap) => {
387
+ if (!parentMap || child === parent)
388
+ return false;
389
+ const visited = new Set([child]);
390
+ let current = [child];
391
+ for (let depth = 0; depth < MAX_MRO_DEPTH && current.length > 0; depth++) {
392
+ const next = [];
393
+ for (const cls of current) {
394
+ const parents = parentMap.get(cls);
395
+ if (!parents)
396
+ continue;
397
+ for (const p of parents) {
398
+ if (p === parent)
399
+ return true;
400
+ if (!visited.has(p)) {
401
+ visited.add(p);
402
+ next.push(p);
403
+ }
404
+ }
405
+ }
406
+ current = next;
407
+ }
408
+ return false;
409
+ };
410
+ /** Walk up the parent class chain to find a field or method on an ancestor.
411
+ * BFS-like traversal with depth limit and cycle detection. First match wins.
412
+ * Used by resolveFieldType and resolveMethodReturnType when direct lookup fails. */
413
+ const walkParentChain = (typeName, parentMap, getClassDefs, lookupOnClass) => {
414
+ if (!parentMap)
415
+ return undefined;
416
+ const visited = new Set([typeName]);
417
+ let current = [typeName];
418
+ for (let depth = 0; depth < MAX_MRO_DEPTH && current.length > 0; depth++) {
419
+ const next = [];
420
+ for (const cls of current) {
421
+ const parents = parentMap.get(cls);
422
+ if (!parents)
423
+ continue;
424
+ for (const parent of parents) {
425
+ if (visited.has(parent))
426
+ continue;
427
+ visited.add(parent);
428
+ const parentDefs = getClassDefs(parent);
429
+ if (parentDefs.length === 1) {
430
+ const result = lookupOnClass(parentDefs[0].nodeId);
431
+ if (result !== undefined)
432
+ return result;
433
+ }
434
+ next.push(parent);
435
+ }
436
+ }
437
+ current = next;
438
+ }
439
+ return undefined;
440
+ };
441
+ /** Resolve a field's declared type given a receiver variable and field name.
442
+ * Uses SymbolTable to find the class nodeId for the receiver's type, then
443
+ * looks up the field via the eagerly-populated fieldByOwner index.
444
+ * Falls back to MRO parent chain walking if direct lookup fails (Phase 11A). */
445
+ const resolveFieldType = (receiver, field, scopeEnv, symbolTable, getClassDefs, parentMap) => {
446
+ if (!symbolTable)
447
+ return undefined;
448
+ const receiverType = scopeEnv.get(receiver);
449
+ if (!receiverType)
450
+ return undefined;
451
+ const lookup = getClassDefs
452
+ ?? ((name) => symbolTable.lookupFuzzy(name).filter(d => CLASS_LIKE_TYPES.has(d.type)));
453
+ const classDefs = lookup(receiverType);
454
+ if (classDefs.length !== 1)
455
+ return undefined;
456
+ // Direct lookup first
457
+ const fieldDef = symbolTable.lookupFieldByOwner(classDefs[0].nodeId, field);
458
+ if (fieldDef?.declaredType)
459
+ return extractReturnTypeName(fieldDef.declaredType);
460
+ // MRO parent chain walking on miss
461
+ const inherited = walkParentChain(receiverType, parentMap, lookup, (nodeId) => {
462
+ const f = symbolTable.lookupFieldByOwner(nodeId, field);
463
+ return f?.declaredType ? extractReturnTypeName(f.declaredType) : undefined;
464
+ });
465
+ return inherited;
466
+ };
467
+ /** Resolve a method's return type given a receiver variable and method name.
468
+ * Uses SymbolTable to find class nodeIds for the receiver's type, then
469
+ * looks up the method via lookupFuzzyCallable filtered by ownerId.
470
+ * Falls back to MRO parent chain walking if direct lookup fails (Phase 11A). */
471
+ const resolveMethodReturnType = (receiver, method, scopeEnv, symbolTable, getClassDefs, parentMap) => {
472
+ if (!symbolTable)
473
+ return undefined;
474
+ const receiverType = scopeEnv.get(receiver);
475
+ if (!receiverType)
476
+ return undefined;
477
+ const lookup = getClassDefs
478
+ ?? ((name) => symbolTable.lookupFuzzy(name).filter(d => CLASS_LIKE_TYPES.has(d.type)));
479
+ const classDefs = lookup(receiverType);
480
+ if (classDefs.length === 0)
481
+ return undefined;
482
+ // Direct lookup first
483
+ const classNodeIds = new Set(classDefs.map(d => d.nodeId));
484
+ const methods = symbolTable.lookupFuzzyCallable(method)
485
+ .filter(d => d.ownerId && classNodeIds.has(d.ownerId));
486
+ if (methods.length === 1 && methods[0].returnType) {
487
+ return extractReturnTypeName(methods[0].returnType);
488
+ }
489
+ // MRO parent chain walking on miss
490
+ if (methods.length === 0) {
491
+ const inherited = walkParentChain(receiverType, parentMap, lookup, (nodeId) => {
492
+ const parentMethods = symbolTable.lookupFuzzyCallable(method)
493
+ .filter(d => d.ownerId === nodeId);
494
+ if (parentMethods.length !== 1 || !parentMethods[0].returnType)
495
+ return undefined;
496
+ return extractReturnTypeName(parentMethods[0].returnType);
497
+ });
498
+ return inherited;
499
+ }
500
+ return undefined;
501
+ };
502
+ /**
503
+ * Unified fixpoint propagation: iterate over ALL pending items (copy, callResult,
504
+ * fieldAccess, methodCallResult) until no new bindings are produced.
505
+ * Handles arbitrary-depth mixed chains:
506
+ * const user = getUser(); // callResult → User
507
+ * const addr = user.address; // fieldAccess → Address (depends on user)
508
+ * const city = addr.getCity(); // methodCallResult → City (depends on addr)
509
+ * const alias = city; // copy → City (depends on city)
510
+ * Data flow: SymbolTable (immutable) + scopeEnv → resolve → scopeEnv.
511
+ * Termination: finite entries, each bound at most once (first-writer-wins), max 10 iterations.
512
+ */
513
+ const MAX_FIXPOINT_ITERATIONS = 10;
514
+ const resolveFixpointBindings = (pendingItems, env, returnTypeLookup, symbolTable, parentMap) => {
515
+ if (pendingItems.length === 0)
516
+ return;
517
+ const getClassDefs = createClassDefCache(symbolTable);
518
+ const resolved = new Set();
519
+ for (let iter = 0; iter < MAX_FIXPOINT_ITERATIONS; iter++) {
520
+ let changed = false;
521
+ for (let i = 0; i < pendingItems.length; i++) {
522
+ if (resolved.has(i))
523
+ continue;
524
+ const item = pendingItems[i];
525
+ const scopeEnv = env.get(item.scope);
526
+ if (!scopeEnv || scopeEnv.has(item.lhs)) {
527
+ resolved.add(i);
528
+ continue;
529
+ }
530
+ let typeName;
531
+ switch (item.kind) {
532
+ case 'callResult':
533
+ typeName = returnTypeLookup.lookupReturnType(item.callee);
534
+ break;
535
+ case 'copy':
536
+ typeName = scopeEnv.get(item.rhs) ?? env.get(FILE_SCOPE)?.get(item.rhs);
537
+ break;
538
+ case 'fieldAccess':
539
+ typeName = resolveFieldType(item.receiver, item.field, scopeEnv, symbolTable, getClassDefs, parentMap);
540
+ break;
541
+ case 'methodCallResult':
542
+ typeName = resolveMethodReturnType(item.receiver, item.method, scopeEnv, symbolTable, getClassDefs, parentMap);
543
+ break;
544
+ default: {
545
+ // Exhaustive check: TypeScript will error here if a new PendingAssignment
546
+ // kind is added without handling it in the switch.
547
+ const _exhaustive = item;
548
+ break;
549
+ }
550
+ }
551
+ if (typeName) {
552
+ scopeEnv.set(item.lhs, typeName);
553
+ resolved.add(i);
554
+ changed = true;
555
+ }
556
+ }
557
+ if (!changed)
558
+ break;
559
+ if (iter === MAX_FIXPOINT_ITERATIONS - 1 && process.env.GITNEXUS_DEBUG) {
560
+ const unresolved = pendingItems.length - resolved.size;
561
+ if (unresolved > 0) {
562
+ console.warn(`[type-env] fixpoint hit iteration cap (${MAX_FIXPOINT_ITERATIONS}), ${unresolved} items unresolved`);
563
+ }
564
+ }
565
+ }
566
+ };
567
+ /** Seed cross-file type bindings into the file scope.
568
+ * MUST be called AFTER walk() completes so that local declarations
569
+ * (Tier 0/1) always take precedence over imported bindings (first-writer-wins). */
570
+ function seedImportedBindings(env, importedBindings) {
571
+ let fileEnv = env.get(FILE_SCOPE);
572
+ if (!fileEnv) {
573
+ fileEnv = new Map();
574
+ env.set(FILE_SCOPE, fileEnv);
575
+ }
576
+ for (const [name, type] of importedBindings) {
577
+ if (!fileEnv.has(name)) {
578
+ fileEnv.set(name, type);
579
+ }
580
+ }
581
+ }
582
+ export const buildTypeEnv = (tree, language, options) => {
583
+ const symbolTable = options?.symbolTable;
584
+ const parentMap = options?.parentMap;
300
585
  const env = new Map();
301
586
  const patternOverrides = new Map();
587
+ // Phase P: maps `scope\0varName` → constructor type when a declaration has BOTH
588
+ // a base type annotation AND a more specific constructor initializer.
589
+ // e.g., `Animal a = new Dog()` → constructorTypeMap.set('func@42\0a', 'Dog')
590
+ const constructorTypeMap = new Map();
302
591
  const localClassNames = new Set();
303
592
  const classNames = createClassNameLookup(localClassNames, symbolTable);
304
593
  const config = typeConfigs[language];
305
594
  const bindings = [];
306
- // Build ReturnTypeLookup from optional SymbolTable.
307
- // Conservative: returns undefined when callee is ambiguous (0 or 2+ matches).
595
+ // Build ReturnTypeLookup: SymbolTable is authoritative when it has an unambiguous match.
596
+ // Cross-file importedReturnTypes are consulted ONLY when SymbolTable has 0 matches.
597
+ // Ambiguous (2+) → undefined, no cross-file fallback (conservative, local-first principle).
308
598
  const returnTypeLookup = {
309
599
  lookupReturnType(callee) {
310
- if (!symbolTable)
311
- return undefined;
312
- if (isBuiltInOrNoise(callee))
313
- return undefined;
314
- const callables = symbolTable.lookupFuzzyCallable(callee);
315
- if (callables.length !== 1)
316
- return undefined;
317
- const rawReturn = callables[0].returnType;
318
- if (!rawReturn)
319
- return undefined;
320
- return extractReturnTypeName(rawReturn);
600
+ // SymbolTable is authoritative when it has an unambiguous match
601
+ if (symbolTable) {
602
+ if (isBuiltInOrNoise(callee))
603
+ return undefined;
604
+ const callables = symbolTable.lookupFuzzyCallable(callee);
605
+ if (callables.length === 1) {
606
+ const rawReturn = callables[0].returnType;
607
+ if (rawReturn)
608
+ return extractReturnTypeName(rawReturn);
609
+ }
610
+ // Ambiguous (2+) → return undefined (conservative, no cross-file fallback)
611
+ if (callables.length > 1)
612
+ return undefined;
613
+ }
614
+ // No match (0 results or no symbolTable) → fall back to cross-file
615
+ return options?.importedReturnTypes?.get(callee);
321
616
  },
322
617
  lookupRawReturnType(callee) {
323
- if (!symbolTable)
324
- return undefined;
325
- if (isBuiltInOrNoise(callee))
326
- return undefined;
327
- const callables = symbolTable.lookupFuzzyCallable(callee);
328
- if (callables.length !== 1)
329
- return undefined;
330
- return callables[0].returnType;
618
+ if (symbolTable) {
619
+ if (isBuiltInOrNoise(callee))
620
+ return undefined;
621
+ const callables = symbolTable.lookupFuzzyCallable(callee);
622
+ if (callables.length === 1)
623
+ return callables[0].returnType;
624
+ // Ambiguous (2+) → return undefined (conservative, no cross-file fallback)
625
+ if (callables.length > 1)
626
+ return undefined;
627
+ }
628
+ // Cross-file fallback uses importedRawReturnTypes (raw declared types, e.g., 'User[]')
629
+ // NOT importedReturnTypes (which contains processed/simple types via extractReturnTypeName)
630
+ return options?.importedRawReturnTypes?.get(callee);
331
631
  }
332
632
  };
333
633
  // Pre-compute combined set of node types that need extractTypeBinding.
@@ -336,12 +636,13 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
336
636
  TYPED_PARAMETER_TYPES.forEach(t => interestingNodeTypes.add(t));
337
637
  config.declarationNodeTypes.forEach(t => interestingNodeTypes.add(t));
338
638
  config.forLoopNodeTypes?.forEach(t => interestingNodeTypes.add(t));
339
- // Tier 2: copy-propagation (`const b = a`) and call-result propagation (`const b = foo()`)
340
- const pendingCopies = [];
341
- // NOTE: Infrastructure-ready no language extractor currently returns { kind: 'callResult' }
342
- // from extractPendingAssignment. When one does, this array will bind variables to their
343
- // function return types at TypeEnv build time. See PendingAssignment in types.ts.
344
- const pendingCallResults = [];
639
+ // Tier 2: unified fixpoint propagation collects copy, callResult, fieldAccess, and
640
+ // methodCallResult items during walk(), then iterates until no new bindings are produced.
641
+ // Handles arbitrary-depth mixed chains: callResult fieldAccess methodCallResult copy.
642
+ const pendingItems = [];
643
+ // For-loop nodes whose iterable was unresolved at walk-time. Replayed after the fixpoint
644
+ // resolves the iterable's type, bridging the walk-time/fixpoint gap (Phase 10 / ex-9B).
645
+ const pendingForLoops = [];
345
646
  // Maps `scope\0varName` → the type annotation AST node from the original declaration.
346
647
  // Allows pattern extractors to navigate back to the declaration's generic type arguments
347
648
  // (e.g., to extract T from Result<T, E> for `if let Ok(x) = res`).
@@ -371,7 +672,9 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
371
672
  let typeNode = node.childForFieldName('type');
372
673
  if (typeNode) {
373
674
  const nameNode = node.childForFieldName('name')
374
- ?? node.childForFieldName('pattern');
675
+ ?? node.childForFieldName('pattern')
676
+ // Python typed_parameter: name is a positional child (identifier), not a named field
677
+ ?? (node.firstNamedChild?.type === 'identifier' ? node.firstNamedChild : null);
375
678
  if (nameNode) {
376
679
  const varName = extractVarName(nameNode);
377
680
  if (varName && !declarationTypeNodes.has(`${scope}\0${varName}`)) {
@@ -391,7 +694,8 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
391
694
  fallbackName = child;
392
695
  }
393
696
  if (!fallbackType && (child.type === 'user_type' || child.type === 'type_identifier'
394
- || child.type === 'generic_type' || child.type === 'parameterized_type')) {
697
+ || child.type === 'generic_type' || child.type === 'parameterized_type'
698
+ || child.type === 'nullable_type')) {
395
699
  fallbackType = child;
396
700
  }
397
701
  }
@@ -409,8 +713,14 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
409
713
  // Checked before declarationNodeTypes — loop variables are not declarations.
410
714
  if (config.forLoopNodeTypes?.has(node.type)) {
411
715
  if (config.extractForLoopBinding) {
716
+ const sizeBefore = scopeEnv.size;
412
717
  const forLoopCtx = { scopeEnv, declarationTypeNodes, scope, returnTypeLookup };
413
718
  config.extractForLoopBinding(node, forLoopCtx);
719
+ // If no new binding was produced, the iterable's type may not yet be resolved.
720
+ // Store for post-fixpoint replay (Phase 10 / ex-9B loop-fixpoint bridge).
721
+ if (scopeEnv.size === sizeBefore) {
722
+ pendingForLoops.push({ node, scope });
723
+ }
414
724
  }
415
725
  return;
416
726
  }
@@ -436,8 +746,20 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
436
746
  }
437
747
  }
438
748
  }
439
- if (wrapped)
749
+ if (wrapped) {
440
750
  typeNode = wrapped.childForFieldName('type');
751
+ // Kotlin: variable_declaration stores the type as user_type / nullable_type
752
+ // child rather than a named 'type' field.
753
+ if (!typeNode) {
754
+ for (let i = 0; i < wrapped.namedChildCount; i++) {
755
+ const c = wrapped.namedChild(i);
756
+ if (c && (c.type === 'user_type' || c.type === 'nullable_type')) {
757
+ typeNode = c;
758
+ break;
759
+ }
760
+ }
761
+ }
762
+ }
441
763
  }
442
764
  if (typeNode) {
443
765
  const nameNode = node.childForFieldName('name')
@@ -451,13 +773,20 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
451
773
  }
452
774
  }
453
775
  // Run the language-specific declaration extractor (may or may not add to scopeEnv).
454
- const keysBefore = typeNode ? new Set(scopeEnv.keys()) : undefined;
776
+ const sizeBefore = typeNode ? scopeEnv.size : -1;
455
777
  config.extractDeclaration(node, scopeEnv);
456
778
  // Fallback: for multi-declarator languages (TS, C#, Java) where the type field
457
- // is on variable_declarator children, capture via keysBefore/keysAfter diff.
458
- if (typeNode && keysBefore) {
779
+ // is on variable_declarator children, capture newly-added keys.
780
+ // Map preserves insertion order, so new keys are always at the end —
781
+ // skip the first sizeBefore entries to find only newly-added variables.
782
+ if (sizeBefore >= 0 && scopeEnv.size > sizeBefore) {
783
+ let skip = sizeBefore;
459
784
  for (const varName of scopeEnv.keys()) {
460
- if (!keysBefore.has(varName) && !declarationTypeNodes.has(`${scope}\0${varName}`)) {
785
+ if (skip > 0) {
786
+ skip--;
787
+ continue;
788
+ }
789
+ if (!declarationTypeNodes.has(`${scope}\0${varName}`)) {
461
790
  declarationTypeNodes.set(`${scope}\0${varName}`, typeNode);
462
791
  }
463
792
  }
@@ -469,6 +798,35 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
469
798
  if (config.extractInitializer) {
470
799
  config.extractInitializer(node, scopeEnv, classNames);
471
800
  }
801
+ // Phase P: detect constructor-visible virtual dispatch.
802
+ // When a declaration has BOTH a type annotation AND a constructor initializer,
803
+ // record the constructor type for receiver override at call resolution time.
804
+ // e.g., `Animal a = new Dog()` → constructorTypeMap.set('scope\0a', 'Dog')
805
+ if (sizeBefore >= 0 && scopeEnv.size > sizeBefore) {
806
+ let ctorSkip = sizeBefore;
807
+ for (const varName of scopeEnv.keys()) {
808
+ if (ctorSkip > 0) {
809
+ ctorSkip--;
810
+ continue;
811
+ }
812
+ const declaredType = scopeEnv.get(varName);
813
+ if (!declaredType)
814
+ continue;
815
+ const ctorType = extractConstructorTypeName(node)
816
+ ?? config.detectConstructorType?.(node, classNames);
817
+ if (!ctorType || ctorType === declaredType)
818
+ continue;
819
+ // Unwrap wrapper types (e.g., C++ shared_ptr<Animal> → Animal) for an
820
+ // accurate isSubclassOf comparison. Language-specific via config hook.
821
+ const declTypeNode = declarationTypeNodes.get(`${scope}\0${varName}`);
822
+ const effectiveDeclaredType = (declTypeNode && config.unwrapDeclaredType)
823
+ ? (config.unwrapDeclaredType(declaredType, declTypeNode) ?? declaredType)
824
+ : declaredType;
825
+ if (ctorType !== effectiveDeclaredType) {
826
+ constructorTypeMap.set(`${scope}\0${varName}`, ctorType);
827
+ }
828
+ }
829
+ }
472
830
  }
473
831
  };
474
832
  const walk = (node, currentScope) => {
@@ -501,7 +859,8 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
501
859
  extractTypeBinding(node, scopeEnv, scope);
502
860
  }
503
861
  // Pattern binding extraction: handles constructs that introduce NEW typed variables
504
- // via pattern matching (e.g. `if let Some(x) = opt`, `x instanceof T t`).
862
+ // via pattern matching (e.g. `if let Some(x) = opt`, `x instanceof T t`)
863
+ // or narrow existing variables within a branch (null-check narrowing).
505
864
  // Runs after Tier 0/1 so scopeEnv already contains the source variable's type.
506
865
  // Conservative: extractor returns undefined when source type is unknown.
507
866
  if (config.extractPatternBinding && (!config.patternBindingNodeTypes || config.patternBindingNodeTypes.has(node.type))) {
@@ -511,11 +870,25 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
511
870
  const scopeEnv = env.get(scope);
512
871
  const patternBinding = config.extractPatternBinding(node, scopeEnv, declarationTypeNodes, scope);
513
872
  if (patternBinding) {
514
- if (config.allowPatternBindingOverwrite) {
873
+ if (patternBinding.narrowingRange) {
874
+ // Explicit narrowing range (null-check narrowing): always store in patternOverrides
875
+ // using the extractor-provided range (typically the if-body block).
876
+ if (!patternOverrides.has(scope))
877
+ patternOverrides.set(scope, new Map());
878
+ const varMap = patternOverrides.get(scope);
879
+ if (!varMap.has(patternBinding.varName))
880
+ varMap.set(patternBinding.varName, []);
881
+ varMap.get(patternBinding.varName).push({
882
+ rangeStart: patternBinding.narrowingRange.startIndex,
883
+ rangeEnd: patternBinding.narrowingRange.endIndex,
884
+ typeName: patternBinding.typeName,
885
+ });
886
+ }
887
+ else if (config.allowPatternBindingOverwrite) {
515
888
  // Position-indexed: store per-branch binding for smart-cast narrowing.
516
889
  // Each when arm / switch case gets its own type for the variable,
517
890
  // preventing cross-arm contamination (e.g., Kotlin when/is).
518
- const branchNode = findPatternBranchScope(node);
891
+ const branchNode = findNarrowingBranchScope(node);
519
892
  if (branchNode) {
520
893
  if (!patternOverrides.has(scope))
521
894
  patternOverrides.set(scope, new Map());
@@ -542,6 +915,7 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
542
915
  // Delegates to per-language extractPendingAssignment — AST shapes differ widely
543
916
  // (JS uses variable_declarator/name/value, Rust uses let_declaration/pattern/value,
544
917
  // Python uses assignment/left/right, Go uses short_var_declaration/expression_list).
918
+ // May return a single item or an array (for destructuring: N fieldAccess items).
545
919
  if (config.extractPendingAssignment && config.declarationNodeTypes.has(node.type)) {
546
920
  // scopeEnv is guaranteed to exist here because declarationNodeTypes is a subset
547
921
  // of interestingNodeTypes, so extractTypeBinding already created the scope map above.
@@ -549,11 +923,11 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
549
923
  if (scopeEnv) {
550
924
  const pending = config.extractPendingAssignment(node, scopeEnv);
551
925
  if (pending) {
552
- if (pending.kind === 'copy') {
553
- pendingCopies.push({ scope, lhs: pending.lhs, rhs: pending.rhs });
554
- }
555
- else {
556
- pendingCallResults.push({ scope, lhs: pending.lhs, callee: pending.callee });
926
+ const items = Array.isArray(pending) ? pending : [pending];
927
+ for (const item of items) {
928
+ // Substitute this/self/$this/Me receivers with enclosing class name
929
+ const resolved = substituteThisReceiver(item, node);
930
+ pendingItems.push({ scope, ...resolved });
557
931
  }
558
932
  }
559
933
  }
@@ -577,35 +951,40 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
577
951
  }
578
952
  };
579
953
  walk(tree.rootNode, FILE_SCOPE);
580
- // Tier 2a: copy-propagation `const b = a` where `a` has a known type from Tier 0/1.
581
- // Multi-hop chains resolve when forward-declared (a→b→c in source order);
582
- // reverse-order assignments are depth-1 only. No fixpoint iteration —
583
- // this covers 95%+ of real-world patterns.
584
- for (const { scope, lhs, rhs } of pendingCopies) {
585
- const scopeEnv = env.get(scope);
586
- if (!scopeEnv || scopeEnv.has(lhs))
587
- continue;
588
- const rhsType = scopeEnv.get(rhs) ?? env.get(FILE_SCOPE)?.get(rhs);
589
- if (rhsType)
590
- scopeEnv.set(lhs, rhsType);
954
+ // Phase 14: Seed cross-file bindings from upstream files AFTER walk
955
+ // (local declarations from walk() take precedence first-writer-wins)
956
+ if (options?.importedBindings && options.importedBindings.size > 0) {
957
+ seedImportedBindings(env, options.importedBindings);
591
958
  }
592
- // Tier 2b: call-result propagation — `const b = foo()` where `foo` has a declared return type.
593
- // Uses ReturnTypeLookup which is backed by SymbolTable.lookupFuzzyCallable.
594
- // Conservative: only binds when exactly one callable matches (avoids overload ambiguity).
595
- // NOTE: Currently dormant no extractPendingAssignment implementation emits 'callResult' yet.
596
- // The loop is structurally complete and will activate when any language extractor starts
597
- // returning { kind: 'callResult', lhs, callee } from extractPendingAssignment.
598
- for (const { scope, lhs, callee } of pendingCallResults) {
599
- const scopeEnv = env.get(scope);
600
- if (!scopeEnv || scopeEnv.has(lhs))
601
- continue;
602
- const typeName = returnTypeLookup.lookupReturnType(callee);
603
- if (typeName)
604
- scopeEnv.set(lhs, typeName);
959
+ resolveFixpointBindings(pendingItems, env, returnTypeLookup, symbolTable, parentMap);
960
+ // Post-fixpoint for-loop replay (Phase 10 / ex-9B loop-fixpoint bridge):
961
+ // For-loop nodes whose iterables were unresolved at walk-time may now be
962
+ // resolvable because the fixpoint bound the iterable's type.
963
+ // Example: `const users = getUsers(); for (const u of users) { u.save(); }`
964
+ // - walk-time: users untyped u unresolved
965
+ // - fixpoint: users User[]
966
+ // - replay: users now typed → u → User
967
+ if (pendingForLoops.length > 0 && config.extractForLoopBinding) {
968
+ for (const { node, scope } of pendingForLoops) {
969
+ if (!env.has(scope))
970
+ env.set(scope, new Map());
971
+ const scopeEnv = env.get(scope);
972
+ config.extractForLoopBinding(node, { scopeEnv, declarationTypeNodes, scope, returnTypeLookup });
973
+ }
974
+ // Re-run the main fixpoint to resolve items that depended on loop variables.
975
+ // Only needed if replay actually produced new bindings.
976
+ const unresolvedBefore = pendingItems.filter((item) => {
977
+ const scopeEnv = env.get(item.scope);
978
+ return scopeEnv && !scopeEnv.has(item.lhs);
979
+ });
980
+ if (unresolvedBefore.length > 0) {
981
+ resolveFixpointBindings(unresolvedBefore, env, returnTypeLookup, symbolTable);
982
+ }
605
983
  }
606
984
  return {
607
985
  lookup: (varName, callNode) => lookupInEnv(env, varName, callNode, patternOverrides),
608
986
  constructorBindings: bindings,
609
987
  env,
988
+ constructorTypeMap,
610
989
  };
611
990
  };