gitnexus 1.4.7 → 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 (92) 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 +2 -1
  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 +48 -1
  24. package/dist/core/ingestion/call-processor.js +368 -7
  25. package/dist/core/ingestion/call-routing.d.ts +6 -0
  26. package/dist/core/ingestion/entry-point-scoring.js +36 -26
  27. package/dist/core/ingestion/framework-detection.d.ts +10 -2
  28. package/dist/core/ingestion/framework-detection.js +49 -12
  29. package/dist/core/ingestion/heritage-processor.js +47 -49
  30. package/dist/core/ingestion/import-processor.d.ts +1 -1
  31. package/dist/core/ingestion/import-processor.js +103 -194
  32. package/dist/core/ingestion/import-resolution.d.ts +101 -0
  33. package/dist/core/ingestion/import-resolution.js +251 -0
  34. package/dist/core/ingestion/language-config.d.ts +3 -0
  35. package/dist/core/ingestion/language-config.js +13 -0
  36. package/dist/core/ingestion/markdown-processor.d.ts +17 -0
  37. package/dist/core/ingestion/markdown-processor.js +124 -0
  38. package/dist/core/ingestion/mro-processor.js +8 -3
  39. package/dist/core/ingestion/named-binding-extraction.d.ts +9 -43
  40. package/dist/core/ingestion/named-binding-extraction.js +89 -79
  41. package/dist/core/ingestion/parsing-processor.d.ts +2 -2
  42. package/dist/core/ingestion/parsing-processor.js +14 -73
  43. package/dist/core/ingestion/pipeline.d.ts +10 -0
  44. package/dist/core/ingestion/pipeline.js +421 -4
  45. package/dist/core/ingestion/resolution-context.d.ts +5 -0
  46. package/dist/core/ingestion/resolution-context.js +7 -4
  47. package/dist/core/ingestion/resolvers/index.d.ts +1 -1
  48. package/dist/core/ingestion/resolvers/index.js +1 -1
  49. package/dist/core/ingestion/resolvers/jvm.d.ts +2 -1
  50. package/dist/core/ingestion/resolvers/jvm.js +25 -9
  51. package/dist/core/ingestion/resolvers/php.d.ts +14 -0
  52. package/dist/core/ingestion/resolvers/php.js +43 -3
  53. package/dist/core/ingestion/resolvers/utils.d.ts +5 -0
  54. package/dist/core/ingestion/resolvers/utils.js +16 -0
  55. package/dist/core/ingestion/symbol-table.d.ts +16 -0
  56. package/dist/core/ingestion/symbol-table.js +20 -6
  57. package/dist/core/ingestion/tree-sitter-queries.d.ts +4 -4
  58. package/dist/core/ingestion/tree-sitter-queries.js +43 -2
  59. package/dist/core/ingestion/type-env.d.ts +28 -1
  60. package/dist/core/ingestion/type-env.js +419 -96
  61. package/dist/core/ingestion/type-extractors/c-cpp.d.ts +5 -0
  62. package/dist/core/ingestion/type-extractors/c-cpp.js +119 -0
  63. package/dist/core/ingestion/type-extractors/csharp.js +149 -16
  64. package/dist/core/ingestion/type-extractors/index.d.ts +1 -1
  65. package/dist/core/ingestion/type-extractors/index.js +1 -1
  66. package/dist/core/ingestion/type-extractors/jvm.js +169 -66
  67. package/dist/core/ingestion/type-extractors/rust.js +35 -1
  68. package/dist/core/ingestion/type-extractors/shared.d.ts +0 -2
  69. package/dist/core/ingestion/type-extractors/shared.js +5 -10
  70. package/dist/core/ingestion/type-extractors/swift.js +7 -6
  71. package/dist/core/ingestion/type-extractors/types.d.ts +37 -7
  72. package/dist/core/ingestion/type-extractors/typescript.js +141 -9
  73. package/dist/core/ingestion/utils.d.ts +2 -120
  74. package/dist/core/ingestion/utils.js +3 -1051
  75. package/dist/core/ingestion/workers/parse-worker.d.ts +13 -4
  76. package/dist/core/ingestion/workers/parse-worker.js +66 -87
  77. package/dist/core/lbug/csv-generator.js +18 -1
  78. package/dist/core/lbug/lbug-adapter.d.ts +10 -0
  79. package/dist/core/lbug/lbug-adapter.js +69 -4
  80. package/dist/core/lbug/schema.d.ts +5 -3
  81. package/dist/core/lbug/schema.js +26 -2
  82. package/dist/mcp/core/embedder.js +11 -3
  83. package/dist/mcp/core/lbug-adapter.js +12 -1
  84. package/dist/mcp/local/local-backend.d.ts +22 -0
  85. package/dist/mcp/local/local-backend.js +133 -29
  86. package/dist/mcp/resources.js +2 -0
  87. package/dist/mcp/tools.js +2 -2
  88. package/dist/server/api.d.ts +19 -1
  89. package/dist/server/api.js +66 -6
  90. package/dist/storage/git.d.ts +12 -0
  91. package/dist/storage/git.js +21 -0
  92. package/package.json +10 -2
@@ -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`.
@@ -297,78 +320,314 @@ const SKIP_SUBTREE_TYPES = new Set([
297
320
  'regex', 'regex_pattern',
298
321
  ]);
299
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
+ };
300
441
  /** Resolve a field's declared type given a receiver variable and field name.
301
442
  * 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) => {
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) => {
304
446
  if (!symbolTable)
305
447
  return undefined;
306
448
  const receiverType = scopeEnv.get(receiver);
307
449
  if (!receiverType)
308
450
  return undefined;
309
- const classDefs = symbolTable.lookupFuzzy(receiverType)
310
- .filter(d => CLASS_LIKE_TYPES.has(d.type));
451
+ const lookup = getClassDefs
452
+ ?? ((name) => symbolTable.lookupFuzzy(name).filter(d => CLASS_LIKE_TYPES.has(d.type)));
453
+ const classDefs = lookup(receiverType);
311
454
  if (classDefs.length !== 1)
312
455
  return undefined;
456
+ // Direct lookup first
313
457
  const fieldDef = symbolTable.lookupFieldByOwner(classDefs[0].nodeId, field);
314
- if (!fieldDef?.declaredType)
315
- return undefined;
316
- return extractReturnTypeName(fieldDef.declaredType);
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;
317
466
  };
318
467
  /** Resolve a method's return type given a receiver variable and method name.
319
468
  * 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) => {
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) => {
322
472
  if (!symbolTable)
323
473
  return undefined;
324
474
  const receiverType = scopeEnv.get(receiver);
325
475
  if (!receiverType)
326
476
  return undefined;
327
- const classDefs = symbolTable.lookupFuzzy(receiverType)
328
- .filter(d => CLASS_LIKE_TYPES.has(d.type));
477
+ const lookup = getClassDefs
478
+ ?? ((name) => symbolTable.lookupFuzzy(name).filter(d => CLASS_LIKE_TYPES.has(d.type)));
479
+ const classDefs = lookup(receiverType);
329
480
  if (classDefs.length === 0)
330
481
  return undefined;
482
+ // Direct lookup first
331
483
  const classNodeIds = new Set(classDefs.map(d => d.nodeId));
332
484
  const methods = symbolTable.lookupFuzzyCallable(method)
333
485
  .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);
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;
339
501
  };
340
- export const buildTypeEnv = (tree, language, symbolTable) => {
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;
341
585
  const env = new Map();
342
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();
343
591
  const localClassNames = new Set();
344
592
  const classNames = createClassNameLookup(localClassNames, symbolTable);
345
593
  const config = typeConfigs[language];
346
594
  const bindings = [];
347
- // Build ReturnTypeLookup from optional SymbolTable.
348
- // 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).
349
598
  const returnTypeLookup = {
350
599
  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);
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);
362
616
  },
363
617
  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;
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);
372
631
  }
373
632
  };
374
633
  // Pre-compute combined set of node types that need extractTypeBinding.
@@ -381,6 +640,9 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
381
640
  // methodCallResult items during walk(), then iterates until no new bindings are produced.
382
641
  // Handles arbitrary-depth mixed chains: callResult → fieldAccess → methodCallResult → copy.
383
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 = [];
384
646
  // Maps `scope\0varName` → the type annotation AST node from the original declaration.
385
647
  // Allows pattern extractors to navigate back to the declaration's generic type arguments
386
648
  // (e.g., to extract T from Result<T, E> for `if let Ok(x) = res`).
@@ -432,7 +694,8 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
432
694
  fallbackName = child;
433
695
  }
434
696
  if (!fallbackType && (child.type === 'user_type' || child.type === 'type_identifier'
435
- || child.type === 'generic_type' || child.type === 'parameterized_type')) {
697
+ || child.type === 'generic_type' || child.type === 'parameterized_type'
698
+ || child.type === 'nullable_type')) {
436
699
  fallbackType = child;
437
700
  }
438
701
  }
@@ -450,8 +713,14 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
450
713
  // Checked before declarationNodeTypes — loop variables are not declarations.
451
714
  if (config.forLoopNodeTypes?.has(node.type)) {
452
715
  if (config.extractForLoopBinding) {
716
+ const sizeBefore = scopeEnv.size;
453
717
  const forLoopCtx = { scopeEnv, declarationTypeNodes, scope, returnTypeLookup };
454
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
+ }
455
724
  }
456
725
  return;
457
726
  }
@@ -477,8 +746,20 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
477
746
  }
478
747
  }
479
748
  }
480
- if (wrapped)
749
+ if (wrapped) {
481
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
+ }
482
763
  }
483
764
  if (typeNode) {
484
765
  const nameNode = node.childForFieldName('name')
@@ -492,13 +773,20 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
492
773
  }
493
774
  }
494
775
  // Run the language-specific declaration extractor (may or may not add to scopeEnv).
495
- const keysBefore = typeNode ? new Set(scopeEnv.keys()) : undefined;
776
+ const sizeBefore = typeNode ? scopeEnv.size : -1;
496
777
  config.extractDeclaration(node, scopeEnv);
497
778
  // 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) {
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;
500
784
  for (const varName of scopeEnv.keys()) {
501
- 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}`)) {
502
790
  declarationTypeNodes.set(`${scope}\0${varName}`, typeNode);
503
791
  }
504
792
  }
@@ -510,6 +798,35 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
510
798
  if (config.extractInitializer) {
511
799
  config.extractInitializer(node, scopeEnv, classNames);
512
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
+ }
513
830
  }
514
831
  };
515
832
  const walk = (node, currentScope) => {
@@ -542,7 +859,8 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
542
859
  extractTypeBinding(node, scopeEnv, scope);
543
860
  }
544
861
  // 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`).
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).
546
864
  // Runs after Tier 0/1 so scopeEnv already contains the source variable's type.
547
865
  // Conservative: extractor returns undefined when source type is unknown.
548
866
  if (config.extractPatternBinding && (!config.patternBindingNodeTypes || config.patternBindingNodeTypes.has(node.type))) {
@@ -552,11 +870,25 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
552
870
  const scopeEnv = env.get(scope);
553
871
  const patternBinding = config.extractPatternBinding(node, scopeEnv, declarationTypeNodes, scope);
554
872
  if (patternBinding) {
555
- 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) {
556
888
  // Position-indexed: store per-branch binding for smart-cast narrowing.
557
889
  // Each when arm / switch case gets its own type for the variable,
558
890
  // preventing cross-arm contamination (e.g., Kotlin when/is).
559
- const branchNode = findPatternBranchScope(node);
891
+ const branchNode = findNarrowingBranchScope(node);
560
892
  if (branchNode) {
561
893
  if (!patternOverrides.has(scope))
562
894
  patternOverrides.set(scope, new Map());
@@ -583,6 +915,7 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
583
915
  // Delegates to per-language extractPendingAssignment — AST shapes differ widely
584
916
  // (JS uses variable_declarator/name/value, Rust uses let_declaration/pattern/value,
585
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).
586
919
  if (config.extractPendingAssignment && config.declarationNodeTypes.has(node.type)) {
587
920
  // scopeEnv is guaranteed to exist here because declarationNodeTypes is a subset
588
921
  // of interestingNodeTypes, so extractTypeBinding already created the scope map above.
@@ -590,7 +923,12 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
590
923
  if (scopeEnv) {
591
924
  const pending = config.extractPendingAssignment(node, scopeEnv);
592
925
  if (pending) {
593
- pendingItems.push({ scope, ...pending });
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 });
931
+ }
594
932
  }
595
933
  }
596
934
  }
@@ -613,55 +951,40 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
613
951
  }
614
952
  };
615
953
  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];
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);
958
+ }
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) => {
633
977
  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
- }
978
+ return scopeEnv && !scopeEnv.has(item.lhs);
979
+ });
980
+ if (unresolvedBefore.length > 0) {
981
+ resolveFixpointBindings(unresolvedBefore, env, returnTypeLookup, symbolTable);
658
982
  }
659
- if (!changed)
660
- break;
661
983
  }
662
984
  return {
663
985
  lookup: (varName, callNode) => lookupInEnv(env, varName, callNode, patternOverrides),
664
986
  constructorBindings: bindings,
665
987
  env,
988
+ constructorTypeMap,
666
989
  };
667
990
  };