@optave/codegraph 3.11.2 → 3.12.0

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 (167) hide show
  1. package/README.md +8 -8
  2. package/dist/db/migrations.d.ts.map +1 -1
  3. package/dist/db/migrations.js +7 -0
  4. package/dist/db/migrations.js.map +1 -1
  5. package/dist/domain/analysis/module-map.d.ts +2 -0
  6. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  7. package/dist/domain/analysis/module-map.js +24 -2
  8. package/dist/domain/analysis/module-map.js.map +1 -1
  9. package/dist/domain/graph/builder/call-resolver.d.ts +4 -2
  10. package/dist/domain/graph/builder/call-resolver.d.ts.map +1 -1
  11. package/dist/domain/graph/builder/call-resolver.js +170 -8
  12. package/dist/domain/graph/builder/call-resolver.js.map +1 -1
  13. package/dist/domain/graph/builder/cha.d.ts +61 -0
  14. package/dist/domain/graph/builder/cha.d.ts.map +1 -0
  15. package/dist/domain/graph/builder/cha.js +143 -0
  16. package/dist/domain/graph/builder/cha.js.map +1 -0
  17. package/dist/domain/graph/builder/context.d.ts +3 -0
  18. package/dist/domain/graph/builder/context.d.ts.map +1 -1
  19. package/dist/domain/graph/builder/context.js +2 -0
  20. package/dist/domain/graph/builder/context.js.map +1 -1
  21. package/dist/domain/graph/builder/helpers.d.ts +17 -1
  22. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  23. package/dist/domain/graph/builder/helpers.js +159 -5
  24. package/dist/domain/graph/builder/helpers.js.map +1 -1
  25. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  26. package/dist/domain/graph/builder/incremental.js +73 -1
  27. package/dist/domain/graph/builder/incremental.js.map +1 -1
  28. package/dist/domain/graph/builder/stages/build-edges.d.ts +2 -0
  29. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  30. package/dist/domain/graph/builder/stages/build-edges.js +926 -26
  31. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  32. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  33. package/dist/domain/graph/builder/stages/detect-changes.js +2 -1
  34. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  35. package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -1
  36. package/dist/domain/graph/builder/stages/native-orchestrator.js +501 -14
  37. package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -1
  38. package/dist/domain/graph/builder/stages/resolve-imports.d.ts +1 -0
  39. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  40. package/dist/domain/graph/builder/stages/resolve-imports.js +9 -0
  41. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  42. package/dist/domain/graph/journal.js +1 -1
  43. package/dist/domain/graph/journal.js.map +1 -1
  44. package/dist/domain/graph/resolver/points-to.d.ts +53 -0
  45. package/dist/domain/graph/resolver/points-to.d.ts.map +1 -0
  46. package/dist/domain/graph/resolver/points-to.js +213 -0
  47. package/dist/domain/graph/resolver/points-to.js.map +1 -0
  48. package/dist/domain/graph/resolver/ts-resolver.d.ts +9 -0
  49. package/dist/domain/graph/resolver/ts-resolver.d.ts.map +1 -0
  50. package/dist/domain/graph/resolver/ts-resolver.js +476 -0
  51. package/dist/domain/graph/resolver/ts-resolver.js.map +1 -0
  52. package/dist/domain/parser.d.ts +10 -1
  53. package/dist/domain/parser.d.ts.map +1 -1
  54. package/dist/domain/parser.js +39 -7
  55. package/dist/domain/parser.js.map +1 -1
  56. package/dist/domain/wasm-worker-entry.js +25 -0
  57. package/dist/domain/wasm-worker-entry.js.map +1 -1
  58. package/dist/domain/wasm-worker-pool.d.ts.map +1 -1
  59. package/dist/domain/wasm-worker-pool.js +32 -0
  60. package/dist/domain/wasm-worker-pool.js.map +1 -1
  61. package/dist/domain/wasm-worker-protocol.d.ts +14 -1
  62. package/dist/domain/wasm-worker-protocol.d.ts.map +1 -1
  63. package/dist/extractors/c.js +3 -3
  64. package/dist/extractors/c.js.map +1 -1
  65. package/dist/extractors/clojure.js +1 -1
  66. package/dist/extractors/clojure.js.map +1 -1
  67. package/dist/extractors/cpp.js +3 -3
  68. package/dist/extractors/cpp.js.map +1 -1
  69. package/dist/extractors/csharp.d.ts.map +1 -1
  70. package/dist/extractors/csharp.js +37 -8
  71. package/dist/extractors/csharp.js.map +1 -1
  72. package/dist/extractors/cuda.js +3 -3
  73. package/dist/extractors/cuda.js.map +1 -1
  74. package/dist/extractors/elixir.js +6 -6
  75. package/dist/extractors/elixir.js.map +1 -1
  76. package/dist/extractors/fsharp.js +1 -1
  77. package/dist/extractors/fsharp.js.map +1 -1
  78. package/dist/extractors/go.js +5 -5
  79. package/dist/extractors/go.js.map +1 -1
  80. package/dist/extractors/haskell.js +1 -1
  81. package/dist/extractors/haskell.js.map +1 -1
  82. package/dist/extractors/java.js +2 -2
  83. package/dist/extractors/java.js.map +1 -1
  84. package/dist/extractors/javascript.d.ts +2 -0
  85. package/dist/extractors/javascript.d.ts.map +1 -1
  86. package/dist/extractors/javascript.js +1674 -64
  87. package/dist/extractors/javascript.js.map +1 -1
  88. package/dist/extractors/kotlin.js +5 -5
  89. package/dist/extractors/kotlin.js.map +1 -1
  90. package/dist/extractors/lua.js +1 -1
  91. package/dist/extractors/lua.js.map +1 -1
  92. package/dist/extractors/objc.js +3 -3
  93. package/dist/extractors/objc.js.map +1 -1
  94. package/dist/extractors/ocaml.js +1 -1
  95. package/dist/extractors/ocaml.js.map +1 -1
  96. package/dist/extractors/php.js +2 -2
  97. package/dist/extractors/php.js.map +1 -1
  98. package/dist/extractors/python.js +7 -7
  99. package/dist/extractors/python.js.map +1 -1
  100. package/dist/extractors/ruby.js +2 -2
  101. package/dist/extractors/ruby.js.map +1 -1
  102. package/dist/extractors/scala.js +1 -1
  103. package/dist/extractors/scala.js.map +1 -1
  104. package/dist/extractors/solidity.js +1 -1
  105. package/dist/extractors/solidity.js.map +1 -1
  106. package/dist/extractors/swift.js +4 -4
  107. package/dist/extractors/swift.js.map +1 -1
  108. package/dist/extractors/zig.js +4 -4
  109. package/dist/extractors/zig.js.map +1 -1
  110. package/dist/infrastructure/config.d.ts +10 -0
  111. package/dist/infrastructure/config.d.ts.map +1 -1
  112. package/dist/infrastructure/config.js +15 -0
  113. package/dist/infrastructure/config.js.map +1 -1
  114. package/dist/infrastructure/native.d.ts +11 -0
  115. package/dist/infrastructure/native.d.ts.map +1 -1
  116. package/dist/infrastructure/native.js +78 -5
  117. package/dist/infrastructure/native.js.map +1 -1
  118. package/dist/presentation/queries-cli/overview.d.ts.map +1 -1
  119. package/dist/presentation/queries-cli/overview.js +5 -0
  120. package/dist/presentation/queries-cli/overview.js.map +1 -1
  121. package/dist/types.d.ts +184 -0
  122. package/dist/types.d.ts.map +1 -1
  123. package/package.json +7 -7
  124. package/src/db/migrations.ts +7 -0
  125. package/src/domain/analysis/module-map.ts +29 -1
  126. package/src/domain/graph/builder/call-resolver.ts +177 -7
  127. package/src/domain/graph/builder/cha.ts +175 -0
  128. package/src/domain/graph/builder/context.ts +3 -0
  129. package/src/domain/graph/builder/helpers.ts +175 -5
  130. package/src/domain/graph/builder/incremental.ts +79 -1
  131. package/src/domain/graph/builder/stages/build-edges.ts +1128 -24
  132. package/src/domain/graph/builder/stages/detect-changes.ts +3 -1
  133. package/src/domain/graph/builder/stages/native-orchestrator.ts +583 -20
  134. package/src/domain/graph/builder/stages/resolve-imports.ts +14 -0
  135. package/src/domain/graph/journal.ts +1 -1
  136. package/src/domain/graph/resolver/points-to.ts +254 -0
  137. package/src/domain/graph/resolver/ts-resolver.ts +536 -0
  138. package/src/domain/parser.ts +43 -5
  139. package/src/domain/wasm-worker-entry.ts +25 -0
  140. package/src/domain/wasm-worker-pool.ts +21 -0
  141. package/src/domain/wasm-worker-protocol.ts +14 -0
  142. package/src/extractors/c.ts +3 -3
  143. package/src/extractors/clojure.ts +1 -1
  144. package/src/extractors/cpp.ts +3 -3
  145. package/src/extractors/csharp.ts +33 -9
  146. package/src/extractors/cuda.ts +3 -3
  147. package/src/extractors/elixir.ts +6 -6
  148. package/src/extractors/fsharp.ts +1 -1
  149. package/src/extractors/go.ts +5 -5
  150. package/src/extractors/haskell.ts +1 -1
  151. package/src/extractors/java.ts +2 -2
  152. package/src/extractors/javascript.ts +1802 -66
  153. package/src/extractors/kotlin.ts +5 -5
  154. package/src/extractors/lua.ts +1 -1
  155. package/src/extractors/objc.ts +3 -3
  156. package/src/extractors/ocaml.ts +1 -1
  157. package/src/extractors/php.ts +2 -2
  158. package/src/extractors/python.ts +7 -7
  159. package/src/extractors/ruby.ts +2 -2
  160. package/src/extractors/scala.ts +1 -1
  161. package/src/extractors/solidity.ts +1 -1
  162. package/src/extractors/swift.ts +4 -4
  163. package/src/extractors/zig.ts +4 -4
  164. package/src/infrastructure/config.ts +15 -0
  165. package/src/infrastructure/native.ts +87 -5
  166. package/src/presentation/queries-cli/overview.ts +15 -1
  167. package/src/types.ts +194 -0
@@ -1,12 +1,22 @@
1
1
  import { debug } from '../infrastructure/logger.js';
2
2
  import type {
3
+ ArrayCallbackBinding,
4
+ ArrayElemBinding,
3
5
  Call,
6
+ CallAssignment,
4
7
  ClassRelation,
5
8
  Definition,
6
9
  Export,
7
10
  ExtractorOutput,
11
+ FnRefBinding,
12
+ ForOfBinding,
8
13
  Import,
14
+ ObjectPropBinding,
15
+ ObjectRestParamBinding,
16
+ ParamBinding,
17
+ SpreadArgBinding,
9
18
  SubDeclaration,
19
+ ThisCallBinding,
10
20
  TreeSitterNode,
11
21
  TreeSitterQuery,
12
22
  TreeSitterTree,
@@ -76,8 +86,17 @@ const BUILTIN_GLOBALS: Set<string> = new Set([
76
86
  'Buffer',
77
87
  'EventEmitter',
78
88
  'Stream',
89
+ 'process',
90
+ 'window',
91
+ 'document',
92
+ 'globalThis',
79
93
  ]);
80
94
 
95
+ /** Maximum chain depth for inter-procedural return-type propagation (Phase 8.2). */
96
+ const MAX_PROPAGATION_DEPTH = 3;
97
+ /** Confidence penalty applied per propagation hop (1.0 → 0.9 → 0.8 → 0.7). */
98
+ export const PROPAGATION_HOP_PENALTY = 0.1;
99
+
81
100
  /**
82
101
  * Extract symbols from a JS/TS parsed AST.
83
102
  * When a compiled tree-sitter Query is provided (from parser.js),
@@ -177,7 +196,9 @@ function handleExportCapture(
177
196
  const declType = decl.type;
178
197
  const kindMap: Record<string, string> = {
179
198
  function_declaration: 'function',
199
+ generator_function_declaration: 'function',
180
200
  class_declaration: 'class',
201
+ abstract_class_declaration: 'class',
181
202
  interface_declaration: 'interface',
182
203
  type_alias_declaration: 'type',
183
204
  };
@@ -296,6 +317,7 @@ function dispatchQueryMatch(
296
317
  if (callInfo) calls.push(callInfo);
297
318
  } else if (c.assign_node) {
298
319
  handleCommonJSAssignment(c.assign_left!, c.assign_right!, c.assign_node, imports);
320
+ handleFuncPropAssignment(c.assign_left!, c.assign_right!, definitions);
299
321
  }
300
322
  }
301
323
 
@@ -306,6 +328,17 @@ function extractSymbolsQuery(tree: TreeSitterTree, query: TreeSitterQuery): Extr
306
328
  const classes: ClassRelation[] = [];
307
329
  const exps: Export[] = [];
308
330
  const typeMap: Map<string, TypeMapEntry> = new Map();
331
+ const returnTypeMap: Map<string, TypeMapEntry> = new Map();
332
+ const callAssignments: CallAssignment[] = [];
333
+ const fnRefBindings: FnRefBinding[] = [];
334
+ const paramBindings: ParamBinding[] = [];
335
+ const arrayElemBindings: ArrayElemBinding[] = [];
336
+ const spreadArgBindings: SpreadArgBinding[] = [];
337
+ const forOfBindings: ForOfBinding[] = [];
338
+ const arrayCallbackBindings: ArrayCallbackBinding[] = [];
339
+ const objectRestParamBindings: ObjectRestParamBinding[] = [];
340
+ const objectPropBindings: ObjectPropBinding[] = [];
341
+ const thisCallBindings: ThisCallBinding[] = [];
309
342
 
310
343
  const matches = query.matches(tree.rootNode);
311
344
 
@@ -319,16 +352,69 @@ function extractSymbolsQuery(tree: TreeSitterTree, query: TreeSitterQuery): Extr
319
352
  // Extract top-level constants via targeted walk (query patterns don't cover these)
320
353
  extractConstantsWalk(tree.rootNode, definitions);
321
354
 
322
- // Extract dynamic import() calls via targeted walk (query patterns don't match `import` function type)
323
- extractDynamicImportsWalk(tree.rootNode, imports);
324
-
325
- // Extract typeMap from type annotations and new expressions
326
- extractTypeMapWalk(tree.rootNode, typeMap);
355
+ // Phase 8.2: Extract function return types first runContextCollectorWalk's
356
+ // declarator handler reads the *complete* per-file map for inter-procedural
357
+ // propagation, so this cannot be folded into that pass.
358
+ extractReturnTypeMapWalk(tree.rootNode, returnTypeMap);
359
+
360
+ // Context-tracking collector pass: typeMap (with return-type propagation),
361
+ // object-rest param bindings, and spread/for-of/Array.from bindings.
362
+ runContextCollectorWalk(tree.rootNode, {
363
+ typeMap,
364
+ returnTypeMap,
365
+ callAssignments,
366
+ fnRefBindings,
367
+ objectRestParamBindings,
368
+ spreadArgBindings,
369
+ forOfBindings,
370
+ arrayCallbackBindings,
371
+ });
327
372
 
328
373
  // Extract definitions from destructured bindings (query patterns don't match object_pattern)
329
374
  extractDestructuredBindingsWalk(tree.rootNode, definitions);
330
375
 
331
- return { definitions, calls, imports, classes, exports: exps, typeMap };
376
+ // Everything without bespoke traversal semantics is collected in ONE pass:
377
+ // dynamic import() calls, prototype-method definitions, param bindings,
378
+ // array-element bindings, object-prop bindings, `new X()` names,
379
+ // Object.defineProperty receivers, class members (fields/static blocks,
380
+ // which query patterns don't capture), and this()/call/apply bindings.
381
+ const newExpressions: string[] = [];
382
+ const definePropertyReceivers: Map<string, string> = new Map();
383
+ runCollectorWalk(tree.rootNode, {
384
+ definitions,
385
+ typeMap,
386
+ paramBindings,
387
+ arrayElemBindings,
388
+ objectPropBindings,
389
+ newExpressions,
390
+ definePropertyReceivers,
391
+ imports,
392
+ calls,
393
+ thisCallBindings,
394
+ classMemberDefs: definitions,
395
+ });
396
+
397
+ return {
398
+ definitions,
399
+ calls,
400
+ imports,
401
+ classes,
402
+ exports: exps,
403
+ typeMap,
404
+ returnTypeMap,
405
+ callAssignments,
406
+ fnRefBindings,
407
+ paramBindings,
408
+ arrayElemBindings,
409
+ spreadArgBindings,
410
+ forOfBindings,
411
+ arrayCallbackBindings,
412
+ objectRestParamBindings,
413
+ objectPropBindings,
414
+ thisCallBindings,
415
+ newExpressions,
416
+ ...(definePropertyReceivers.size > 0 ? { definePropertyReceivers } : {}),
417
+ };
332
418
  }
333
419
 
334
420
  /** Node types that define a function scope — constants inside these are skipped. */
@@ -384,6 +470,11 @@ function extractConstantsWalk(node: TreeSitterNode, definitions: Definition[]):
384
470
  }
385
471
  }
386
472
 
473
+ // Class field definitions and static initializer blocks (which query patterns
474
+ // don't capture) are collected inline in runCollectorWalk's field_definition /
475
+ // class_static_block cases when `classMemberDefs` is set. The walk-based path
476
+ // (extractSymbolsWalk) handles these node types via walkJavaScriptNode instead.
477
+
387
478
  /**
388
479
  * Walk the AST to find destructured const bindings (query patterns don't match object_pattern).
389
480
  * e.g. `const { handleToken, checkPermissions } = initAuth(config)`
@@ -407,7 +498,7 @@ function extractDestructuredBindingsWalk(node: TreeSitterNode, definitions: Defi
407
498
  ) {
408
499
  for (let j = 0; j < declNode.childCount; j++) {
409
500
  const declarator = declNode.child(j);
410
- if (!declarator || declarator.type !== 'variable_declarator') continue;
501
+ if (declarator?.type !== 'variable_declarator') continue;
411
502
  const nameN = declarator.childForFieldName('name');
412
503
  if (nameN && nameN.type === 'object_pattern') {
413
504
  extractDestructuredBindings(
@@ -434,13 +525,18 @@ function extractConstDeclarators(declNode: TreeSitterNode, definitions: Definiti
434
525
 
435
526
  for (let j = 0; j < declNode.childCount; j++) {
436
527
  const declarator = declNode.child(j);
437
- if (!declarator || declarator.type !== 'variable_declarator') continue;
528
+ if (declarator?.type !== 'variable_declarator') continue;
438
529
  const nameN = declarator.childForFieldName('name');
439
530
  const valueN = declarator.childForFieldName('value');
440
- if (!nameN || nameN.type !== 'identifier' || !valueN) continue;
531
+ if (nameN?.type !== 'identifier' || !valueN) continue;
441
532
  // Skip functions — already captured by query patterns
442
533
  const valType = valueN.type;
443
- if (valType === 'arrow_function' || valType === 'function_expression' || valType === 'function')
534
+ if (
535
+ valType === 'arrow_function' ||
536
+ valType === 'function_expression' ||
537
+ valType === 'function' ||
538
+ valType === 'generator_function'
539
+ )
444
540
  continue;
445
541
  if (isConstantValue(valueN)) {
446
542
  definitions.push({
@@ -449,6 +545,14 @@ function extractConstDeclarators(declNode: TreeSitterNode, definitions: Definiti
449
545
  line: nodeStartLine(declNode),
450
546
  endLine: nodeEndLine(declNode),
451
547
  });
548
+ // Phase 8.3f: extract function/arrow properties from object literals.
549
+ // Scope guard: extractConstDeclarators is only called from extractConstantsWalk, which
550
+ // already skips const declarations inside function scopes (line ~412). So these definitions
551
+ // are always top-level. Any new call site must add a hasFunctionScopeAncestor guard
552
+ // (the walk path at handleVariableDecl does this).
553
+ if (valueN.type === 'object') {
554
+ extractObjectLiteralFunctions(valueN, nameN.text, definitions);
555
+ }
452
556
  }
453
557
  }
454
558
  }
@@ -458,34 +562,36 @@ function extractConstDeclarators(declNode: TreeSitterNode, definitions: Definiti
458
562
  * Query patterns match call_expression with identifier/member_expression/subscript_expression
459
563
  * functions, but import() has function type `import` which none of those patterns cover.
460
564
  */
461
- function extractDynamicImportsWalk(node: TreeSitterNode, imports: Import[]): void {
462
- if (node.type === 'call_expression') {
463
- const fn = node.childForFieldName('function');
464
- if (fn && fn.type === 'import') {
465
- const args = node.childForFieldName('arguments') || findChild(node, 'arguments');
466
- if (args) {
467
- const strArg = findChild(args, 'string');
468
- if (strArg) {
469
- const modPath = strArg.text.replace(/['"]/g, '');
470
- const names = extractDynamicImportNames(node);
471
- imports.push({
472
- source: modPath,
473
- names,
474
- line: nodeStartLine(node),
475
- dynamicImport: true,
476
- });
477
- } else {
478
- debug(
479
- `Skipping non-static dynamic import() at line ${nodeStartLine(node)} (template literal or variable)`,
480
- );
481
- }
482
- }
483
- return; // no need to recurse into import() children
565
+ /**
566
+ * Collect a dynamic `import()` call at `node` (a call_expression).
567
+ * Returns true when the node *is* an import() call — the collector walk uses
568
+ * this to suppress dynamic-import collection inside the import's own argument
569
+ * subtree, preserving the former standalone walk's "don't recurse into
570
+ * import() children" behaviour without hiding those children from the other
571
+ * collectors.
572
+ */
573
+ function collectDynamicImport(node: TreeSitterNode, imports: Import[]): boolean {
574
+ const fn = node.childForFieldName('function');
575
+ if (fn?.type !== 'import') return false;
576
+ const args = node.childForFieldName('arguments') || findChild(node, 'arguments');
577
+ if (args) {
578
+ const strArg = findChild(args, 'string');
579
+ if (strArg) {
580
+ const modPath = strArg.text.replace(/['"]/g, '');
581
+ const names = extractDynamicImportNames(node);
582
+ imports.push({
583
+ source: modPath,
584
+ names,
585
+ line: nodeStartLine(node),
586
+ dynamicImport: true,
587
+ });
588
+ } else {
589
+ debug(
590
+ `Skipping non-static dynamic import() at line ${nodeStartLine(node)} (template literal or variable)`,
591
+ );
484
592
  }
485
593
  }
486
- for (let i = 0; i < node.childCount; i++) {
487
- extractDynamicImportsWalk(node.child(i)!, imports);
488
- }
594
+ return true;
489
595
  }
490
596
 
491
597
  function handleCommonJSAssignment(
@@ -556,22 +662,77 @@ function extractSymbolsWalk(tree: TreeSitterTree): ExtractorOutput {
556
662
  classes: [],
557
663
  exports: [],
558
664
  typeMap: new Map(),
665
+ returnTypeMap: new Map(),
666
+ callAssignments: [],
667
+ fnRefBindings: [],
668
+ paramBindings: [],
669
+ arrayElemBindings: [],
670
+ spreadArgBindings: [],
671
+ forOfBindings: [],
672
+ arrayCallbackBindings: [],
673
+ objectRestParamBindings: [],
674
+ objectPropBindings: [],
675
+ thisCallBindings: [],
559
676
  };
560
677
 
561
678
  walkJavaScriptNode(tree.rootNode, ctx);
562
- // Populate typeMap for variables and parameter type annotations
563
- extractTypeMapWalk(tree.rootNode, ctx.typeMap!);
679
+ // Phase 8.2: Extract function return types first — runContextCollectorWalk's
680
+ // declarator handler reads the *complete* per-file map for inter-procedural
681
+ // propagation, so this cannot be folded into that pass.
682
+ extractReturnTypeMapWalk(tree.rootNode, ctx.returnTypeMap!);
683
+ // Context-tracking collector pass: typeMap (with return-type propagation),
684
+ // object-rest param bindings, and spread/for-of/Array.from bindings.
685
+ runContextCollectorWalk(tree.rootNode, {
686
+ typeMap: ctx.typeMap!,
687
+ returnTypeMap: ctx.returnTypeMap,
688
+ callAssignments: ctx.callAssignments,
689
+ fnRefBindings: ctx.fnRefBindings!,
690
+ objectRestParamBindings: ctx.objectRestParamBindings!,
691
+ spreadArgBindings: ctx.spreadArgBindings!,
692
+ forOfBindings: ctx.forOfBindings!,
693
+ arrayCallbackBindings: ctx.arrayCallbackBindings!,
694
+ });
695
+ // Single collector pass for everything else: prototype-method and func-prop
696
+ // definitions, param bindings, array-element bindings, object-prop bindings,
697
+ // `new X()` names, and Object.defineProperty receivers. Dynamic imports,
698
+ // this()/call/apply bindings, and class members are omitted here —
699
+ // walkJavaScriptNode already covers those node types on this path.
700
+ const newExpressions: string[] = [];
701
+ const definePropertyReceivers: Map<string, string> = new Map();
702
+ runCollectorWalk(tree.rootNode, {
703
+ definitions: ctx.definitions,
704
+ typeMap: ctx.typeMap!,
705
+ paramBindings: ctx.paramBindings!,
706
+ arrayElemBindings: ctx.arrayElemBindings!,
707
+ objectPropBindings: ctx.objectPropBindings!,
708
+ newExpressions,
709
+ definePropertyReceivers,
710
+ funcPropDefs: ctx.definitions,
711
+ });
712
+ ctx.newExpressions = newExpressions;
713
+ if (definePropertyReceivers.size > 0) ctx.definePropertyReceivers = definePropertyReceivers;
564
714
  return ctx;
565
715
  }
566
716
 
567
717
  function walkJavaScriptNode(node: TreeSitterNode, ctx: ExtractorOutput): void {
568
718
  switch (node.type) {
569
719
  case 'function_declaration':
720
+ case 'generator_function_declaration':
570
721
  handleFunctionDecl(node, ctx);
571
722
  break;
572
723
  case 'class_declaration':
724
+ case 'abstract_class_declaration':
725
+ // class expressions: `return class Foo extends Bar { ... }` or `const X = class Foo { ... }`
726
+ case 'class':
573
727
  handleClassDecl(node, ctx);
574
728
  break;
729
+ case 'class_static_block':
730
+ handleStaticBlock(node, ctx.definitions);
731
+ break;
732
+ case 'field_definition':
733
+ case 'public_field_definition':
734
+ handleFieldDef(node, ctx.definitions);
735
+ break;
575
736
  case 'method_definition':
576
737
  handleMethodDef(node, ctx);
577
738
  break;
@@ -670,6 +831,69 @@ function handleMethodDef(node: TreeSitterNode, ctx: ExtractorOutput): void {
670
831
  }
671
832
  }
672
833
 
834
+ /**
835
+ * Create a synthetic `ClassName.<static:L:C>` definition for a class static block
836
+ * so that calls inside the block can be attributed to a method-kind node and
837
+ * `resolveThisDispatch` can walk up to the parent class for `super.method()`.
838
+ *
839
+ * The start line and column are appended to the name to ensure uniqueness when a
840
+ * class has multiple `static { }` blocks (each has a distinct start position even
841
+ * if on the same line).
842
+ *
843
+ * Tree-sitter uses `class_static_block` (not `static_block`) for `static { ... }`.
844
+ */
845
+ function handleStaticBlock(node: TreeSitterNode, definitions: Definition[]): void {
846
+ const parentClass = findParentClass(node);
847
+ if (!parentClass) return;
848
+ const line = nodeStartLine(node);
849
+ const col = node.startPosition.column;
850
+ definitions.push({
851
+ name: `${parentClass}.<static:${line}:${col}>`,
852
+ kind: 'method',
853
+ line,
854
+ endLine: nodeEndLine(node),
855
+ });
856
+ }
857
+
858
+ /**
859
+ * Emit a `ClassName.fieldName` definition for class fields that have an initializer.
860
+ * This lets `findCaller` attribute calls inside field initializers (e.g. static field
861
+ * side-effects) to the field rather than the enclosing class.
862
+ *
863
+ * JS `field_definition` uses the `'property'` field name; TS
864
+ * `public_field_definition` uses `'name'`. As a third fallback (Rust/TS parity) we
865
+ * also check for a positional `property_identifier` child.
866
+ */
867
+ const CALLABLE_FIELD_TYPES = new Set([
868
+ 'arrow_function',
869
+ 'function_expression',
870
+ 'generator_function',
871
+ ]);
872
+
873
+ function handleFieldDef(node: TreeSitterNode, definitions: Definition[]): void {
874
+ // JS field_definition uses 'property' field; TS public_field_definition uses 'name' field
875
+ const nameNode =
876
+ node.childForFieldName('name') ||
877
+ node.childForFieldName('property') ||
878
+ findChild(node, 'property_identifier');
879
+ const valueNode = node.childForFieldName('value');
880
+ if (!nameNode || !valueNode) return;
881
+ if (nameNode.type === 'computed_property_name') return;
882
+ // Only emit a callable definition when the initializer is a function/arrow expression.
883
+ // Scalar fields like `static x = 42` should not appear as method-kind nodes.
884
+ if (!CALLABLE_FIELD_TYPES.has(valueNode.type)) return;
885
+ const fieldName = nameNode.text;
886
+ if (!fieldName) return;
887
+ const parentClass = findParentClass(node);
888
+ if (!parentClass) return;
889
+ definitions.push({
890
+ name: `${parentClass}.${fieldName}`,
891
+ kind: 'method',
892
+ line: nodeStartLine(node),
893
+ endLine: nodeEndLine(node),
894
+ });
895
+ }
896
+
673
897
  function handleInterfaceDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
674
898
  const nameNode = node.childForFieldName('name');
675
899
  if (!nameNode) return;
@@ -746,7 +970,8 @@ function handleVariableDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
746
970
  if (
747
971
  valType === 'arrow_function' ||
748
972
  valType === 'function_expression' ||
749
- valType === 'function'
973
+ valType === 'function' ||
974
+ valType === 'generator_function'
750
975
  ) {
751
976
  const varFnChildren = extractParameters(valueN);
752
977
  ctx.definitions.push({
@@ -756,13 +981,28 @@ function handleVariableDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
756
981
  endLine: nodeEndLine(valueN),
757
982
  children: varFnChildren.length > 0 ? varFnChildren : undefined,
758
983
  });
759
- } else if (isConst && nameN.type === 'identifier' && isConstantValue(valueN)) {
984
+ } else if (
985
+ isConst &&
986
+ nameN.type === 'identifier' &&
987
+ isConstantValue(valueN) &&
988
+ !hasFunctionScopeAncestor(node)
989
+ ) {
760
990
  ctx.definitions.push({
761
991
  name: nameN.text,
762
992
  kind: 'constant',
763
993
  line: nodeStartLine(node),
764
994
  endLine: nodeEndLine(node),
765
995
  });
996
+ // Phase 8.3f: extract function/arrow properties from object literals so that
997
+ // this.method() calls inside Object.defineProperty accessors can resolve them.
998
+ // Scope guard: hasFunctionScopeAncestor mirrors the Rust path's find_parent_of_types
999
+ // check and the sibling destructured-binding branch below — skips object literals
1000
+ // inside function bodies to avoid polluting the global definition index with
1001
+ // local variable properties (e.g. `localObj.fn` from `const localObj = { fn: ... }`
1002
+ // inside a function).
1003
+ if (valueN.type === 'object') {
1004
+ extractObjectLiteralFunctions(valueN, nameN.text, ctx.definitions);
1005
+ }
766
1006
  } else if (isConst && nameN.type === 'object_pattern' && !hasFunctionScopeAncestor(node)) {
767
1007
  // Destructured bindings: const { handleToken, checkPermissions } = initAuth(...)
768
1008
  // Each destructured property becomes a function definition so it can be
@@ -783,6 +1023,59 @@ function handleVariableDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
783
1023
  }
784
1024
  }
785
1025
 
1026
+ /**
1027
+ * Phase 8.3f: extract function/arrow function properties from an object literal as standalone
1028
+ * definitions so that `this.method()` calls inside Object.defineProperty accessor functions can
1029
+ * resolve them via the same-file definition lookup.
1030
+ *
1031
+ * Definitions are emitted as qualified names (`obj.baz` rather than bare `baz`) to avoid
1032
+ * polluting the global definition index with common property names like `init`, `run`, or
1033
+ * `render`. The typeMap value stored by the caller also uses the qualified name so the resolver
1034
+ * looks up `lookup.byName('obj.baz')` rather than `lookup.byName('baz')`.
1035
+ *
1036
+ * `const obj = { baz: () => {} }` → emits Definition { name: 'obj.baz', kind: 'function' }
1037
+ */
1038
+ function extractObjectLiteralFunctions(
1039
+ objNode: TreeSitterNode,
1040
+ varName: string,
1041
+ definitions: Definition[],
1042
+ ): void {
1043
+ for (let i = 0; i < objNode.childCount; i++) {
1044
+ const child = objNode.child(i);
1045
+ if (!child) continue;
1046
+ if (child.type === 'pair') {
1047
+ const keyNode = child.childForFieldName('key');
1048
+ const valueNode = child.childForFieldName('value');
1049
+ if (!keyNode || !valueNode) continue;
1050
+ const keyName =
1051
+ keyNode.type === 'string' ? keyNode.text.replace(/^['"]|['"]$/g, '') : keyNode.text;
1052
+ if (!keyName) continue;
1053
+ if (
1054
+ valueNode.type === 'arrow_function' ||
1055
+ valueNode.type === 'function_expression' ||
1056
+ valueNode.type === 'function'
1057
+ ) {
1058
+ definitions.push({
1059
+ name: `${varName}.${keyName}`,
1060
+ kind: 'function',
1061
+ line: nodeStartLine(child),
1062
+ endLine: nodeEndLine(valueNode),
1063
+ });
1064
+ }
1065
+ } else if (child.type === 'method_definition') {
1066
+ const nameNode = child.childForFieldName('name');
1067
+ if (nameNode) {
1068
+ definitions.push({
1069
+ name: `${varName}.${nameNode.text}`,
1070
+ kind: 'function',
1071
+ line: nodeStartLine(child),
1072
+ endLine: nodeEndLine(child),
1073
+ });
1074
+ }
1075
+ }
1076
+ }
1077
+ }
1078
+
786
1079
  function handleEnumDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
787
1080
  const nameNode = node.childForFieldName('name');
788
1081
  if (!nameNode) return;
@@ -819,11 +1112,44 @@ function handleCallExpr(node: TreeSitterNode, ctx: ExtractorOutput): void {
819
1112
  if (fn.type === 'import') {
820
1113
  handleDynamicImportCall(node, ctx.imports);
821
1114
  } else {
1115
+ // this() calls: `this` used as a function (not as a receiver).
1116
+ if (fn.type === 'this') {
1117
+ ctx.calls.push({ name: 'this', line: nodeStartLine(node) });
1118
+ return; // no further processing needed for this()-style calls
1119
+ }
822
1120
  const callInfo = extractCallInfo(fn, node);
823
1121
  if (callInfo) ctx.calls.push(callInfo);
824
1122
  if (fn.type === 'member_expression') {
825
1123
  const cbDef = extractCallbackDefinition(node, fn);
826
1124
  if (cbDef) ctx.definitions.push(cbDef);
1125
+ // this-call bindings: `fn.call(namedCtx, ...)` / `fn.apply(namedCtx, ...)`
1126
+ const obj = fn.childForFieldName('object');
1127
+ const prop = fn.childForFieldName('property');
1128
+ if (
1129
+ obj?.type === 'identifier' &&
1130
+ prop &&
1131
+ (prop.text === 'call' || prop.text === 'apply') &&
1132
+ !BUILTIN_GLOBALS.has(obj.text)
1133
+ ) {
1134
+ const args = node.childForFieldName('arguments') || findChild(node, 'arguments');
1135
+ if (args) {
1136
+ for (let i = 0; i < args.childCount; i++) {
1137
+ const child = args.child(i);
1138
+ if (!child) continue;
1139
+ const t = child.type;
1140
+ if (t === '(' || t === ')' || t === ',') continue;
1141
+ if (
1142
+ t === 'identifier' &&
1143
+ !BUILTIN_GLOBALS.has(child.text) &&
1144
+ child.text !== 'undefined' &&
1145
+ child.text !== 'null'
1146
+ ) {
1147
+ ctx.thisCallBindings!.push({ callee: obj.text, thisArg: child.text });
1148
+ }
1149
+ break;
1150
+ }
1151
+ }
1152
+ }
827
1153
  }
828
1154
  ctx.calls.push(...extractCallbackReferenceCalls(node));
829
1155
  }
@@ -878,7 +1204,9 @@ function handleExportStmt(node: TreeSitterNode, ctx: ExtractorOutput): void {
878
1204
  const declType = decl.type;
879
1205
  const kindMap: Record<string, string> = {
880
1206
  function_declaration: 'function',
1207
+ generator_function_declaration: 'function',
881
1208
  class_declaration: 'class',
1209
+ abstract_class_declaration: 'class',
882
1210
  interface_declaration: 'interface',
883
1211
  type_alias_declaration: 'type',
884
1212
  };
@@ -1103,7 +1431,7 @@ function extractSimpleTypeName(typeAnnotationNode: TreeSitterNode): string | nul
1103
1431
  }
1104
1432
 
1105
1433
  function extractNewExprTypeName(newExprNode: TreeSitterNode): string | null {
1106
- if (!newExprNode || newExprNode.type !== 'new_expression') return null;
1434
+ if (newExprNode?.type !== 'new_expression') return null;
1107
1435
  const ctor = newExprNode.childForFieldName('constructor') || newExprNode.child(1);
1108
1436
  if (!ctor) return null;
1109
1437
  if (ctor.type === 'identifier') return ctor.text;
@@ -1114,43 +1442,513 @@ function extractNewExprTypeName(newExprNode: TreeSitterNode): string | null {
1114
1442
  return null;
1115
1443
  }
1116
1444
 
1445
+ // ── Phase 8.2: Inter-Procedural Return Type Propagation ─────────────────────
1446
+
1117
1447
  /**
1118
- * Extract variable-to-type assignments into a per-file type map.
1448
+ * Walk the AST and record the return type of every function/method definition.
1119
1449
  *
1120
- * Values are `{ type: string, confidence: number }`:
1121
- * - 1.0: explicit constructor (`new Foo()`)
1122
- * - 0.9: type annotation (`: Foo`) or typed parameter
1123
- * - 0.7: factory method call (`Foo.create()` uppercase-first heuristic)
1450
+ * Keys: plain name (e.g. "createUser") or "ClassName.methodName" for methods.
1451
+ * Confidence:
1452
+ * - 1.0: explicit TypeScript return type annotation
1453
+ * - 0.85: inferred from the first `return new Constructor()` in the body
1454
+ */
1455
+ function extractReturnTypeMapWalk(
1456
+ rootNode: TreeSitterNode,
1457
+ returnTypeMap: Map<string, TypeMapEntry>,
1458
+ ): void {
1459
+ function walk(node: TreeSitterNode, depth: number, currentClass: string | null): void {
1460
+ if (depth >= MAX_WALK_DEPTH) return;
1461
+ const t = node.type;
1462
+
1463
+ if (t === 'class_declaration' || t === 'abstract_class_declaration' || t === 'class') {
1464
+ const nameNode = node.childForFieldName('name');
1465
+ const className = nameNode?.text ?? null;
1466
+ for (let i = 0; i < node.childCount; i++) {
1467
+ walk(node.child(i)!, depth + 1, className);
1468
+ }
1469
+ return;
1470
+ }
1471
+
1472
+ if (t === 'function_declaration' || t === 'generator_function_declaration') {
1473
+ const nameNode = node.childForFieldName('name');
1474
+ if (nameNode?.type === 'identifier' && nameNode.text !== 'constructor') {
1475
+ const fnName = currentClass ? `${currentClass}.${nameNode.text}` : nameNode.text;
1476
+ storeReturnType(node, fnName, returnTypeMap);
1477
+ }
1478
+ // Recurse into the function body with null currentClass so nested
1479
+ // function declarations are not stored under the enclosing class name.
1480
+ for (let i = 0; i < node.childCount; i++) {
1481
+ walk(node.child(i)!, depth + 1, null);
1482
+ }
1483
+ return;
1484
+ } else if (t === 'method_definition') {
1485
+ const nameNode = node.childForFieldName('name');
1486
+ if (nameNode && currentClass && nameNode.text !== 'constructor') {
1487
+ storeReturnType(node, `${currentClass}.${nameNode.text}`, returnTypeMap);
1488
+ }
1489
+ // Recurse into the method body with null currentClass so nested
1490
+ // function declarations are not stored under the enclosing class name.
1491
+ for (let i = 0; i < node.childCount; i++) {
1492
+ walk(node.child(i)!, depth + 1, null);
1493
+ }
1494
+ return;
1495
+ } else if (t === 'variable_declarator') {
1496
+ // const foo = (): ReturnType => … or const foo = function(): ReturnType { … }
1497
+ const nameN = node.childForFieldName('name');
1498
+ const valueN = node.childForFieldName('value');
1499
+ if (nameN?.type === 'identifier' && valueN) {
1500
+ const vt = valueN.type;
1501
+ if (
1502
+ vt === 'arrow_function' ||
1503
+ vt === 'function_expression' ||
1504
+ vt === 'generator_function'
1505
+ ) {
1506
+ const fnName = currentClass ? `${currentClass}.${nameN.text}` : nameN.text;
1507
+ storeReturnType(valueN, fnName, returnTypeMap);
1508
+ }
1509
+ }
1510
+ }
1511
+
1512
+ for (let i = 0; i < node.childCount; i++) {
1513
+ walk(node.child(i)!, depth + 1, currentClass);
1514
+ }
1515
+ }
1516
+ walk(rootNode, 0, null);
1517
+ }
1518
+
1519
+ /** Extract the return type of a function node and store it in the returnTypeMap. */
1520
+ function storeReturnType(
1521
+ fnNode: TreeSitterNode,
1522
+ fnName: string,
1523
+ returnTypeMap: Map<string, TypeMapEntry>,
1524
+ ): void {
1525
+ const returnTypeNode = fnNode.childForFieldName('return_type');
1526
+ if (returnTypeNode) {
1527
+ const typeName = extractSimpleTypeName(returnTypeNode);
1528
+ if (typeName) {
1529
+ const existing = returnTypeMap.get(fnName);
1530
+ if (!existing || existing.confidence < 1.0)
1531
+ returnTypeMap.set(fnName, { type: typeName, confidence: 1.0 });
1532
+ return;
1533
+ }
1534
+ }
1535
+ // Infer from first `return new Constructor()` in the function body
1536
+ const body = fnNode.childForFieldName('body');
1537
+ if (body) {
1538
+ const inferred = findReturnNewExprType(body);
1539
+ if (inferred) {
1540
+ const existing = returnTypeMap.get(fnName);
1541
+ if (!existing || 0.85 > existing.confidence)
1542
+ returnTypeMap.set(fnName, { type: inferred, confidence: 0.85 });
1543
+ }
1544
+ }
1545
+ }
1546
+
1547
+ /** Return the constructor name from the first `return new Constructor()` in a body, or null. */
1548
+ function findReturnNewExprType(bodyNode: TreeSitterNode): string | null {
1549
+ for (let i = 0; i < bodyNode.childCount; i++) {
1550
+ const child = bodyNode.child(i);
1551
+ if (child?.type !== 'return_statement') continue;
1552
+ for (let j = 0; j < child.childCount; j++) {
1553
+ const expr = child.child(j);
1554
+ if (expr?.type === 'new_expression') return extractNewExprTypeName(expr);
1555
+ }
1556
+ }
1557
+ return null;
1558
+ }
1559
+
1560
+ /**
1561
+ * Resolve the return type of a call_expression node using returnTypeMap.
1562
+ * Handles: createUser() (identifier), service.getRepo() (member), and
1563
+ * getService().getRepo() (chained call) up to MAX_PROPAGATION_DEPTH hops.
1564
+ *
1565
+ * `depth` tracks total chain hops consumed so far. Each call boundary — both
1566
+ * resolving the receiver and resolving the final return type — costs one hop.
1567
+ * Confidence = annotated return type confidence − 0.1 × (depth + 1).
1568
+ *
1569
+ * Examples (annotated sources → confidence 1.0):
1570
+ * createUser() depth=0 → 1.0 − 0.1 = 0.9 (1 hop)
1571
+ * svc.getUser() depth=0 → 1.0 − 0.1 = 0.9 (1 hop; receiver from typeMap)
1572
+ * getService().getRepo() depth=0 → inner resolved at depth=1, outer at depth+1 → 0.8 (2 hops)
1573
+ */
1574
+ function resolveCallExprReturnType(
1575
+ callNode: TreeSitterNode,
1576
+ typeMap: Map<string, TypeMapEntry>,
1577
+ returnTypeMap: Map<string, TypeMapEntry>,
1578
+ depth: number,
1579
+ ): TypeMapEntry | null {
1580
+ if (depth >= MAX_PROPAGATION_DEPTH) return null;
1581
+
1582
+ const fn = callNode.childForFieldName('function');
1583
+ if (!fn) return null;
1584
+
1585
+ if (fn.type === 'identifier') {
1586
+ const entry = returnTypeMap.get(fn.text);
1587
+ if (!entry) return null;
1588
+ const confidence = entry.confidence - PROPAGATION_HOP_PENALTY * (depth + 1);
1589
+ return confidence > 0 ? { type: entry.type, confidence } : null;
1590
+ }
1591
+
1592
+ if (fn.type === 'member_expression') {
1593
+ const obj = fn.childForFieldName('object');
1594
+ const prop = fn.childForFieldName('property');
1595
+ if (!obj || !prop) return null;
1596
+
1597
+ let receiverType: string | null = null;
1598
+ // effectiveDepth tracks the depth at which THIS call's return type is charged.
1599
+ // When the receiver is itself a call expression (chain), we've already consumed
1600
+ // a hop resolving it, so charge this call at depth+1.
1601
+ let effectiveDepth = depth;
1602
+
1603
+ if (obj.type === 'identifier') {
1604
+ const typeEntry = typeMap.get(obj.text);
1605
+ receiverType = typeEntry ? typeEntry.type : null;
1606
+ } else if (obj.type === 'call_expression') {
1607
+ // Each link in a call chain costs an extra hop.
1608
+ const innerResult = resolveCallExprReturnType(obj, typeMap, returnTypeMap, depth + 1);
1609
+ receiverType = innerResult ? innerResult.type : null;
1610
+ effectiveDepth = depth + 1;
1611
+ }
1612
+
1613
+ if (receiverType) {
1614
+ const entry = returnTypeMap.get(`${receiverType}.${prop.text}`);
1615
+ if (entry) {
1616
+ const confidence = entry.confidence - PROPAGATION_HOP_PENALTY * (effectiveDepth + 1);
1617
+ return confidence > 0 ? { type: entry.type, confidence } : null;
1618
+ }
1619
+ }
1620
+ }
1621
+
1622
+ return null;
1623
+ }
1624
+
1625
+ /**
1626
+ * Record a call assignment into callAssignments for cross-file propagation.
1627
+ * Only records cases where the callee is a simple identifier or a method call
1628
+ * on a known-typed variable — chain expressions are skipped (handled locally).
1629
+ */
1630
+ function recordCallAssignment(
1631
+ callNode: TreeSitterNode,
1632
+ varName: string,
1633
+ typeMap: Map<string, TypeMapEntry>,
1634
+ callAssignments: CallAssignment[],
1635
+ ): void {
1636
+ const fn = callNode.childForFieldName('function');
1637
+ if (!fn) return;
1638
+ if (fn.type === 'identifier') {
1639
+ callAssignments.push({ varName, calleeName: fn.text });
1640
+ } else if (fn.type === 'member_expression') {
1641
+ const obj = fn.childForFieldName('object');
1642
+ const prop = fn.childForFieldName('property');
1643
+ if (obj?.type === 'identifier' && prop) {
1644
+ const receiverEntry = typeMap.get(obj.text);
1645
+ callAssignments.push({
1646
+ varName,
1647
+ calleeName: prop.text,
1648
+ receiverTypeName: receiverEntry?.type,
1649
+ });
1650
+ }
1651
+ }
1652
+ }
1653
+
1654
+ /**
1655
+ * Phase 8.5 (RTA): collect all constructor names from `new X()` expressions
1656
+ * in the file. Captures both assigned (`const x = new Foo()`) and unassigned
1657
+ * (`doSomething(new Foo())`) usages that the typeMap-based approach would miss.
1658
+ */
1659
+ // `new X()` constructor-name collection (Phase 8.5 RTA instantiation tracking)
1660
+ // happens inline in runCollectorWalk's new_expression case.
1661
+
1662
+ /**
1663
+ * Walk the AST to find `Object.defineProperty(obj, "bar", { get: getter })` patterns
1664
+ * and record which functions are used as getter/setter accessors for which objects.
1665
+ *
1666
+ * Result is stored in the provided map as `funcName → receiverVarName`.
1667
+ */
1668
+ function collectDefinePropertyReceiver(node: TreeSitterNode, out: Map<string, string>): void {
1669
+ const fn = node.childForFieldName('function');
1670
+ // Match `Object.defineProperty`
1671
+ if (fn?.type !== 'member_expression') return;
1672
+ const obj = fn.childForFieldName('object');
1673
+ const prop = fn.childForFieldName('property');
1674
+ if (obj?.type !== 'identifier' || obj.text !== 'Object' || prop?.text !== 'defineProperty') {
1675
+ return;
1676
+ }
1677
+ const argsNode = node.childForFieldName('arguments') ?? findChild(node, 'arguments');
1678
+ if (!argsNode) return;
1679
+ // Collect non-punctuation children: arg0 (target obj), arg1 (prop name string), arg2 (descriptor)
1680
+ const argChildren: TreeSitterNode[] = [];
1681
+ for (let i = 0; i < argsNode.childCount; i++) {
1682
+ const c = argsNode.child(i);
1683
+ if (!c) continue;
1684
+ if (c.type === ',' || c.type === '(' || c.type === ')') continue;
1685
+ argChildren.push(c);
1686
+ }
1687
+ if (argChildren.length < 3) return;
1688
+ const targetObj = argChildren[0];
1689
+ const descriptor = argChildren[2];
1690
+ if (targetObj?.type !== 'identifier' || descriptor?.type !== 'object') return;
1691
+ const targetName = targetObj.text;
1692
+ // Walk the descriptor object's pair children looking for get/set
1693
+ for (let i = 0; i < descriptor.childCount; i++) {
1694
+ const pair = descriptor.child(i);
1695
+ if (pair?.type !== 'pair') continue;
1696
+ const key = pair.childForFieldName('key');
1697
+ const val = pair.childForFieldName('value');
1698
+ if (
1699
+ key &&
1700
+ (key.text === 'get' || key.text === 'set') &&
1701
+ val?.type === 'identifier' &&
1702
+ !BUILTIN_GLOBALS.has(val.text)
1703
+ ) {
1704
+ // Known limitation: if the same function is registered as an
1705
+ // accessor on multiple objects, last-write-wins — only the
1706
+ // last target object is retained. This is an unusual pattern
1707
+ // (sharing one function across multiple defineProperty calls)
1708
+ // and covering it would require Map<string, string[]> which
1709
+ // changes the consumer API. Tracked as a known edge case.
1710
+ out.set(val.text, targetName);
1711
+ }
1712
+ }
1713
+ }
1714
+
1715
+ /** Outputs for {@link runContextCollectorWalk}. */
1716
+ interface ContextCollectorOutputs {
1717
+ typeMap: Map<string, TypeMapEntry>;
1718
+ returnTypeMap?: Map<string, TypeMapEntry>;
1719
+ callAssignments?: CallAssignment[];
1720
+ fnRefBindings: FnRefBinding[];
1721
+ objectRestParamBindings: ObjectRestParamBinding[];
1722
+ spreadArgBindings: SpreadArgBinding[];
1723
+ forOfBindings: ForOfBinding[];
1724
+ arrayCallbackBindings: ArrayCallbackBinding[];
1725
+ }
1726
+
1727
+ /**
1728
+ * Single context-tracking pass combining what were three separate full-tree
1729
+ * walks (typeMap, object-rest params, spread/for-of) — see runCollectorWalk
1730
+ * for why traversal count dominates extraction cost on WASM trees.
1731
+ *
1732
+ * Each concern keeps its own enclosing-class register because their reset
1733
+ * rules intentionally differ:
1734
+ *
1735
+ * - typeMap (`typeMapClass`): extracts variable-to-type assignments.
1736
+ * Values are `{ type: string, confidence: number }`:
1737
+ * - 1.0: explicit constructor (`new Foo()`)
1738
+ * - 0.9: type annotation (`: Foo`) or typed parameter
1739
+ * - 0.85: property write (`obj.prop = fn` — Phase 8.3d pts tracking)
1740
+ * - 0.7–0.9: inter-procedural propagation from return-type map (Phase 8.2)
1741
+ * - 0.7: factory method call (`Foo.create()` — uppercase-first heuristic)
1742
+ * Higher-confidence entries take priority when the same variable is seen
1743
+ * twice. Class declarations propagate their name into the subtree; class
1744
+ * *expressions* (`const Foo = class Bar { … }`) propagate null because the
1745
+ * expression-internal name is never visible to the resolver, preserving the
1746
+ * `this.prop` fallback in resolveByMethodOrGlobal. No reset at function
1747
+ * boundaries.
1124
1748
  *
1125
- * Higher-confidence entries take priority when the same variable is seen twice.
1749
+ * - object-rest params (`objectRestClass`, Phase 8.3f): context flows only
1750
+ * class_declaration/class → class_body → method_definition so methods are
1751
+ * keyed "ClassName.method"; every other node type resets to null, and
1752
+ * function/method bodies recurse with null so nested declarations don't
1753
+ * inherit the class context.
1754
+ *
1755
+ * - spread/for-of (`funcStack`/`classStack`, Phase 8.3e): tracks the
1756
+ * enclosing *function* (not just class) via push/pop so for-of bindings
1757
+ * record the qualified enclosing callable (e.g. 'Foo.bar', 'obj.method',
1758
+ * or '<module>' at top level).
1759
+ *
1760
+ * NOTE: returnTypeMap population stays a separate, earlier pass
1761
+ * (extractReturnTypeMapWalk) — handleVarDeclaratorTypeMap reads it for
1762
+ * inter-procedural propagation, so it must be complete for the whole file
1763
+ * before any declarator is processed (a function declared *after* its first
1764
+ * use would otherwise be missed).
1126
1765
  */
1127
- function extractTypeMapWalk(rootNode: TreeSitterNode, typeMap: Map<string, TypeMapEntry>): void {
1128
- function walk(node: TreeSitterNode, depth: number): void {
1766
+ function runContextCollectorWalk(rootNode: TreeSitterNode, out: ContextCollectorOutputs): void {
1767
+ const funcStack: string[] = [];
1768
+ const classStack: string[] = [];
1769
+
1770
+ const walk = (
1771
+ node: TreeSitterNode,
1772
+ depth: number,
1773
+ typeMapClass: string | null,
1774
+ objectRestClass: string | null,
1775
+ ): void => {
1129
1776
  if (depth >= MAX_WALK_DEPTH) return;
1130
1777
  const t = node.type;
1778
+
1779
+ const isClassDecl = t === 'class_declaration' || t === 'abstract_class_declaration';
1780
+ const isClassExpr = t === 'class';
1781
+ const isFnDecl = t === 'function_declaration' || t === 'generator_function_declaration';
1782
+
1783
+ // Class name read once, shared by every concern that needs it below.
1784
+ let className: string | null = null;
1785
+ let classNameIsIdentifier = false;
1786
+ if (isClassDecl || isClassExpr) {
1787
+ const nameNode = node.childForFieldName('name');
1788
+ className = nameNode?.text ?? null;
1789
+ classNameIsIdentifier = nameNode?.type === 'identifier';
1790
+ }
1791
+
1792
+ // ── spread/for-of enclosing-context stacks (push on enter, pop after children) ──
1793
+ let pushedFunc = false;
1794
+ let pushedClass = false;
1795
+ if (isClassDecl || isClassExpr) {
1796
+ // The stack push keeps the original walk's `identifier`-only check (TS
1797
+ // class names parse as type_identifier and were never pushed), while
1798
+ // typeMapClass/objectRestClass below use the bare text like their
1799
+ // original walks did.
1800
+ if (className && classNameIsIdentifier) {
1801
+ classStack.push(className);
1802
+ pushedClass = true;
1803
+ }
1804
+ } else if (isFnDecl) {
1805
+ const nameNode = node.childForFieldName('name');
1806
+ if (nameNode?.type === 'identifier') {
1807
+ funcStack.push(nameNode.text);
1808
+ pushedFunc = true;
1809
+ }
1810
+ } else if (t === 'method_definition') {
1811
+ const nameNode = node.childForFieldName('name');
1812
+ if (nameNode) {
1813
+ // Qualify with the enclosing class name so the PTS key matches
1814
+ // callerName from findCaller (which uses def.name = 'ClassName.method').
1815
+ const enclosingClass = classStack.length > 0 ? classStack[classStack.length - 1] : null;
1816
+ const qualifiedName = enclosingClass ? `${enclosingClass}.${nameNode.text}` : nameNode.text;
1817
+ funcStack.push(qualifiedName);
1818
+ pushedFunc = true;
1819
+ }
1820
+ } else if (t === 'variable_declarator') {
1821
+ // `const process = (arr) => { ... }` — arrow/expression functions assigned
1822
+ // to a variable have no `name` field on the function node itself.
1823
+ const nameNode = node.childForFieldName('name');
1824
+ const valueNode = node.childForFieldName('value');
1825
+ if (
1826
+ nameNode?.type === 'identifier' &&
1827
+ (valueNode?.type === 'arrow_function' || valueNode?.type === 'function_expression')
1828
+ ) {
1829
+ funcStack.push(nameNode.text);
1830
+ pushedFunc = true;
1831
+ }
1832
+ } else if (t === 'assignment_expression') {
1833
+ // `obj.method = function() { ... }` — func-prop assignment.
1834
+ // Mirror handleFuncPropAssignment's logic so for-of loops inside the
1835
+ // body get the correct enclosingFunc (e.g. 'obj.method') instead of
1836
+ // '<module>' or the wrong outer function name.
1837
+ const lhs = node.childForFieldName('left');
1838
+ const rhs = node.childForFieldName('right');
1839
+ if (
1840
+ lhs?.type === 'member_expression' &&
1841
+ (rhs?.type === 'function_expression' || rhs?.type === 'arrow_function')
1842
+ ) {
1843
+ const obj = lhs.childForFieldName('object');
1844
+ const prop = lhs.childForFieldName('property');
1845
+ if (
1846
+ obj?.type === 'identifier' &&
1847
+ (prop?.type === 'property_identifier' || prop?.type === 'identifier') &&
1848
+ !BUILTIN_GLOBALS.has(obj.text) &&
1849
+ prop.text !== 'prototype'
1850
+ ) {
1851
+ funcStack.push(`${obj.text}.${prop.text}`);
1852
+ pushedFunc = true;
1853
+ }
1854
+ }
1855
+ }
1856
+
1857
+ // ── per-node collectors (class nodes match none of these types) ──
1131
1858
  if (t === 'variable_declarator') {
1132
- handleVarDeclaratorTypeMap(node, typeMap);
1859
+ handleVarDeclaratorTypeMap(
1860
+ node,
1861
+ out.typeMap,
1862
+ out.returnTypeMap,
1863
+ out.callAssignments,
1864
+ out.fnRefBindings,
1865
+ );
1866
+ collectCollectionWrapBinding(node, out.fnRefBindings);
1133
1867
  } else if (t === 'required_parameter' || t === 'optional_parameter') {
1134
- handleParamTypeMap(node, typeMap);
1868
+ handleParamTypeMap(node, out.typeMap);
1869
+ } else if (t === 'assignment_expression') {
1870
+ handlePropWriteTypeMap(node, out.typeMap, typeMapClass);
1871
+ } else if (t === 'call_expression') {
1872
+ handleDefinePropertyTypeMap(node, out.typeMap);
1873
+ collectSpreadAndArrayFromBindings(node, out.spreadArgBindings, out.arrayCallbackBindings);
1874
+ } else if (t === 'for_in_statement') {
1875
+ const enclosingFunc = funcStack.length > 0 ? funcStack[funcStack.length - 1]! : '<module>';
1876
+ collectForOfBinding(node, enclosingFunc, out.forOfBindings);
1135
1877
  }
1878
+ collectObjectRestParams(node, t, objectRestClass, out.objectRestParamBindings);
1879
+
1880
+ // ── child context per concern ──
1881
+ const childTypeMapClass = isClassDecl ? className : isClassExpr ? null : typeMapClass;
1882
+ let childObjectRestClass: string | null = null;
1883
+ if (t === 'class_declaration' || t === 'class') {
1884
+ childObjectRestClass = className;
1885
+ } else if (t === 'class_body') {
1886
+ childObjectRestClass = objectRestClass;
1887
+ }
1888
+
1136
1889
  for (let i = 0; i < node.childCount; i++) {
1137
- walk(node.child(i)!, depth + 1);
1890
+ walk(node.child(i)!, depth + 1, childTypeMapClass, childObjectRestClass);
1138
1891
  }
1139
- }
1140
- walk(rootNode, 0);
1892
+
1893
+ if (pushedFunc) funcStack.pop();
1894
+ if (pushedClass) classStack.pop();
1895
+ };
1896
+
1897
+ walk(rootNode, 0, null, null);
1141
1898
  }
1142
1899
 
1143
1900
  /** Extract type info from a variable_declarator: type annotation, constructor, or factory. */
1144
1901
  function handleVarDeclaratorTypeMap(
1145
1902
  node: TreeSitterNode,
1146
1903
  typeMap: Map<string, TypeMapEntry>,
1904
+ returnTypeMap?: Map<string, TypeMapEntry>,
1905
+ callAssignments?: CallAssignment[],
1906
+ fnRefBindings?: FnRefBinding[],
1147
1907
  ): void {
1148
1908
  const nameN = node.childForFieldName('name');
1149
- if (!nameN || nameN.type !== 'identifier') return;
1909
+ if (nameN?.type !== 'identifier') return;
1150
1910
 
1151
1911
  const typeAnno = findChild(node, 'type_annotation');
1152
1912
  const valueN = node.childForFieldName('value');
1153
1913
 
1914
+ // Phase 8.3: record function-reference bindings before any type-analysis early returns.
1915
+ // Captures `const fn = handler` (identifier) and `const fn = obj.method` (member_expression).
1916
+ // Also handles `const f = fn.bind(ctx)` — bind returns a new function aliasing fn.
1917
+ if (fnRefBindings && valueN) {
1918
+ if (valueN.type === 'identifier' && !BUILTIN_GLOBALS.has(valueN.text)) {
1919
+ fnRefBindings.push({ lhs: nameN.text, rhs: valueN.text });
1920
+ } else if (valueN.type === 'member_expression') {
1921
+ const prop = valueN.childForFieldName('property');
1922
+ const obj = valueN.childForFieldName('object');
1923
+ // Guard: only static property access (property_identifier or identifier), not
1924
+ // computed subscript expressions like obj[expr] where prop.text would be the
1925
+ // full expression rather than a simple name — those can never match pts keys.
1926
+ if (
1927
+ prop &&
1928
+ (prop.type === 'property_identifier' || prop.type === 'identifier') &&
1929
+ obj?.type === 'identifier' &&
1930
+ !BUILTIN_GLOBALS.has(obj.text)
1931
+ ) {
1932
+ fnRefBindings.push({ lhs: nameN.text, rhs: prop.text, rhsReceiver: obj.text });
1933
+ }
1934
+ } else if (valueN.type === 'call_expression') {
1935
+ // `const f = fn.bind(ctx)` — bind returns a bound copy of fn; track f → fn so
1936
+ // pts(f) ⊇ pts(fn) and subsequent `f(args)` calls resolve to fn.
1937
+ // Note: only flat-identifier binds (fn.bind) are tracked here; method-receiver
1938
+ // binds like `obj.method.bind(ctx)` are not captured (boundFn must be an identifier).
1939
+ const callFn = valueN.childForFieldName('function');
1940
+ if (callFn?.type === 'member_expression') {
1941
+ const bindProp = callFn.childForFieldName('property');
1942
+ if (bindProp?.text === 'bind') {
1943
+ const boundFn = callFn.childForFieldName('object');
1944
+ if (boundFn?.type === 'identifier' && !BUILTIN_GLOBALS.has(boundFn.text)) {
1945
+ fnRefBindings.push({ lhs: nameN.text, rhs: boundFn.text });
1946
+ }
1947
+ }
1948
+ }
1949
+ }
1950
+ }
1951
+
1154
1952
  // Constructor on the same declaration wins over annotation: the runtime type is
1155
1953
  // what matters for call resolution (e.g. `const x: Base = new Derived()` should
1156
1954
  // resolve `x.render()` to `Derived.render`, not `Base.render`).
@@ -1173,15 +1971,50 @@ function handleVarDeclaratorTypeMap(
1173
1971
  }
1174
1972
 
1175
1973
  if (!valueN) return;
1176
-
1177
- // Constructor already handled above — only factory path remains.
1178
1974
  if (valueN.type === 'new_expression') return;
1179
- // Factory method: const x = Foo.create() → confidence 0.7
1180
- else if (valueN.type === 'call_expression') {
1975
+
1976
+ if (valueN.type === 'call_expression') {
1977
+ // Phase 8.3e: Object.create({ f1, f2 }) — seed composite pts keys obj.f1 → f1, etc.
1978
+ const createFn = valueN.childForFieldName('function');
1979
+ if (createFn?.type === 'member_expression') {
1980
+ const createObj = createFn.childForFieldName('object');
1981
+ const createProp = createFn.childForFieldName('property');
1982
+ if (createObj?.text === 'Object' && createProp?.text === 'create') {
1983
+ const createArgs = valueN.childForFieldName('arguments') || findChild(valueN, 'arguments');
1984
+ if (createArgs) {
1985
+ let proto: TreeSitterNode | null = null;
1986
+ for (let i = 0; i < createArgs.childCount; i++) {
1987
+ const n = createArgs.child(i);
1988
+ if (n && n.type !== '(' && n.type !== ')' && n.type !== ',') {
1989
+ proto = n;
1990
+ break;
1991
+ }
1992
+ }
1993
+ if (proto?.type === 'object') {
1994
+ seedProtoProperties(nameN.text, proto, typeMap);
1995
+ }
1996
+ }
1997
+ return;
1998
+ }
1999
+ }
2000
+ // Phase 8.2: inter-procedural propagation — try to resolve return type from
2001
+ // the local returnTypeMap before falling back to factory heuristics.
2002
+ if (returnTypeMap) {
2003
+ const result = resolveCallExprReturnType(valueN, typeMap, returnTypeMap, 0);
2004
+ if (result) {
2005
+ setTypeMapEntry(typeMap, nameN.text, result.type, result.confidence);
2006
+ return;
2007
+ }
2008
+ }
2009
+ // Record for cross-file resolution in build-edges.ts (imported functions)
2010
+ if (callAssignments) {
2011
+ recordCallAssignment(valueN, nameN.text, typeMap, callAssignments);
2012
+ }
2013
+ // Factory method heuristic: const x = Foo.create() → type Foo, confidence 0.7
1181
2014
  const fn = valueN.childForFieldName('function');
1182
- if (fn && fn.type === 'member_expression') {
2015
+ if (fn?.type === 'member_expression') {
1183
2016
  const obj = fn.childForFieldName('object');
1184
- if (obj && obj.type === 'identifier') {
2017
+ if (obj?.type === 'identifier') {
1185
2018
  const objName = obj.text;
1186
2019
  if (
1187
2020
  objName[0] &&
@@ -1193,13 +2026,63 @@ function handleVarDeclaratorTypeMap(
1193
2026
  }
1194
2027
  }
1195
2028
  }
2029
+
2030
+ // Phase 8.3f: seed composite pts keys for object literal properties.
2031
+ // `const obj = { baz: () => {} }` → typeMap['obj.baz'] = 'obj.baz'
2032
+ // `const obj = { baz }` (shorthand) → typeMap['obj.baz'] = 'baz' (bare identifier target)
2033
+ // `const obj = { baz: otherFn }` → typeMap['obj.baz'] = 'otherFn' (identifier alias)
2034
+ //
2035
+ // For function/arrow values, the value is the qualified name ('obj.baz') because
2036
+ // extractObjectLiteralFunctions now registers definitions under that qualified name to avoid
2037
+ // polluting the global index with bare property names like 'init', 'run', or 'render'.
2038
+ // Enables accessor this-dispatch: when typeMap['getter:this'] = 'obj',
2039
+ // resolving this.baz() inside getter → typeMap['obj.baz'] → 'obj.baz' → lookup.byName('obj.baz').
2040
+ //
2041
+ // Scope guard: mirrors Rust handle_var_decl's find_parent_of_types check — skip object literals
2042
+ // inside function bodies so function-scoped `const localObj = { fn: ... }` never seeds
2043
+ // the typeMap (which would shadow a module-level `const obj` with the same property names).
2044
+ if (valueN.type === 'object' && !hasFunctionScopeAncestor(node)) {
2045
+ for (let i = 0; i < valueN.childCount; i++) {
2046
+ const child = valueN.child(i);
2047
+ if (!child) continue;
2048
+ if (child.type === 'shorthand_property_identifier') {
2049
+ setTypeMapEntry(typeMap, `${nameN.text}.${child.text}`, child.text, 0.85);
2050
+ } else if (child.type === 'pair') {
2051
+ const keyNode = child.childForFieldName('key');
2052
+ const valNode = child.childForFieldName('value');
2053
+ if (!keyNode || !valNode) continue;
2054
+ const keyName =
2055
+ keyNode.type === 'string' ? keyNode.text.replace(/^['"]|['"]$/g, '') : keyNode.text;
2056
+ if (!keyName) continue;
2057
+ const qualifiedKey = `${nameN.text}.${keyName}`;
2058
+ if (
2059
+ valNode.type === 'arrow_function' ||
2060
+ valNode.type === 'function_expression' ||
2061
+ valNode.type === 'function'
2062
+ ) {
2063
+ // Store the qualified name so the resolver finds the qualified definition.
2064
+ setTypeMapEntry(typeMap, qualifiedKey, qualifiedKey, 0.85);
2065
+ } else if (valNode.type === 'identifier') {
2066
+ setTypeMapEntry(typeMap, qualifiedKey, valNode.text, 0.85);
2067
+ }
2068
+ } else if (child.type === 'method_definition') {
2069
+ // Method shorthand: `const obj = { baz() {} }` → typeMap['obj.baz'] = 'obj.baz'
2070
+ // extractObjectLiteralFunctions registers a definition under the qualified name;
2071
+ // seed the matching typeMap entry so the two-step accessor dispatch finds it.
2072
+ const nameNode = child.childForFieldName('name');
2073
+ if (!nameNode) continue;
2074
+ const qualifiedKey = `${nameN.text}.${nameNode.text}`;
2075
+ setTypeMapEntry(typeMap, qualifiedKey, qualifiedKey, 0.85);
2076
+ }
2077
+ }
2078
+ }
1196
2079
  }
1197
2080
 
1198
2081
  /** Extract type info from a required_parameter or optional_parameter. */
1199
2082
  function handleParamTypeMap(node: TreeSitterNode, typeMap: Map<string, TypeMapEntry>): void {
1200
2083
  const nameNode =
1201
2084
  node.childForFieldName('pattern') || node.childForFieldName('left') || node.child(0);
1202
- if (!nameNode || nameNode.type !== 'identifier') return;
2085
+ if (nameNode?.type !== 'identifier') return;
1203
2086
  const typeAnno = findChild(node, 'type_annotation');
1204
2087
  if (typeAnno) {
1205
2088
  const typeName = extractSimpleTypeName(typeAnno);
@@ -1207,10 +2090,528 @@ function handleParamTypeMap(node: TreeSitterNode, typeMap: Map<string, TypeMapEn
1207
2090
  }
1208
2091
  }
1209
2092
 
2093
+ /**
2094
+ * Phase 8.3d: seed the pts map from object property writes.
2095
+ *
2096
+ * `handlers.auth = authMiddleware` → typeMap.set('handlers.auth', { type: 'authMiddleware', confidence: 0.85 })
2097
+ * `this.logger = new Logger(...)` → typeMap.set('UserService.logger', { type: 'Logger', confidence: 1.0 })
2098
+ * (keyed as ClassName.prop when currentClass is known, to avoid collisions across classes)
2099
+ *
2100
+ * Only simple `obj.prop = identifier` and `this.prop = new Ctor()` writes are tracked
2101
+ * (not chained `a.b.c = x`). BUILTIN_GLOBALS are skipped (e.g. `console.log = fn`).
2102
+ */
2103
+ function handlePropWriteTypeMap(
2104
+ node: TreeSitterNode,
2105
+ typeMap: Map<string, TypeMapEntry>,
2106
+ currentClass: string | null,
2107
+ ): void {
2108
+ const lhsN = node.childForFieldName('left');
2109
+ const rhsN = node.childForFieldName('right');
2110
+ if (!lhsN || !rhsN) return;
2111
+ if (lhsN.type !== 'member_expression') return;
2112
+
2113
+ const obj = lhsN.childForFieldName('object');
2114
+ const prop = lhsN.childForFieldName('property');
2115
+ if (!obj || !prop) return;
2116
+ // Guard: only static property access (property_identifier or identifier), not
2117
+ // computed subscript expressions — consistent with the adjacent fnRefBindings block.
2118
+ if (prop.type !== 'property_identifier' && prop.type !== 'identifier') return;
2119
+
2120
+ // this.prop = new ClassName(...) — constructor-assigned property type.
2121
+ // Key as ClassName.prop (class-scoped) so two classes with identically-named
2122
+ // properties don't overwrite each other's typeMap entry.
2123
+ if (obj.type === 'this' && rhsN.type === 'new_expression') {
2124
+ const ctorType = extractNewExprTypeName(rhsN);
2125
+ if (ctorType) {
2126
+ const key = currentClass ? `${currentClass}.${prop.text}` : `this.${prop.text}`;
2127
+ setTypeMapEntry(typeMap, key, ctorType, 1.0);
2128
+ }
2129
+ return;
2130
+ }
2131
+
2132
+ // obj.prop = identifier — existing behaviour (skip chained a.b.c = x and builtins)
2133
+ if (rhsN.type !== 'identifier') return;
2134
+ if (obj.type !== 'identifier') return;
2135
+ const objName = obj.text;
2136
+ if (BUILTIN_GLOBALS.has(objName)) return;
2137
+ setTypeMapEntry(typeMap, `${objName}.${prop.text}`, rhsN.text, 0.85);
2138
+ }
2139
+
2140
+ /**
2141
+ * Phase 8.3e/8.3f: seed composite pts keys from Object.defineProperty / defineProperties.
2142
+ *
2143
+ * `Object.defineProperty(obj, "key", { value: fn })` → typeMap.set('obj.key', fn, 0.85)
2144
+ * `Object.defineProperties(obj, { "k1": { value: v1 } })` → typeMap.set('obj.k1', v1, 0.85)
2145
+ * `Object.defineProperty(obj, "key", { get: getter })` → typeMap.set('getter:this', obj, 0.85)
2146
+ */
2147
+ function handleDefinePropertyTypeMap(
2148
+ node: TreeSitterNode,
2149
+ typeMap: Map<string, TypeMapEntry>,
2150
+ ): void {
2151
+ const fn = node.childForFieldName('function');
2152
+ if (fn?.type !== 'member_expression') return;
2153
+ const fnObj = fn.childForFieldName('object');
2154
+ const fnProp = fn.childForFieldName('property');
2155
+ if (fnObj?.text !== 'Object') return;
2156
+ const method = fnProp?.text;
2157
+ if (method !== 'defineProperty' && method !== 'defineProperties') return;
2158
+
2159
+ const argsNode = node.childForFieldName('arguments') || findChild(node, 'arguments');
2160
+ if (!argsNode) return;
2161
+
2162
+ const args: TreeSitterNode[] = [];
2163
+ for (let i = 0; i < argsNode.childCount; i++) {
2164
+ const n = argsNode.child(i);
2165
+ if (n && n.type !== '(' && n.type !== ')' && n.type !== ',') args.push(n);
2166
+ }
2167
+
2168
+ if (method === 'defineProperty') {
2169
+ if (args.length < 3) return;
2170
+ const arg0 = args[0]!,
2171
+ arg1 = args[1]!,
2172
+ arg2 = args[2]!;
2173
+ if (arg0.type !== 'identifier') return;
2174
+ if (arg1.type !== 'string') return;
2175
+ const key = arg1.text.replace(/^['"]|['"]$/g, '');
2176
+ if (!key) return;
2177
+ // Phase 8.3e: { value: fn } → obj.key pts to fn
2178
+ const target = findDescriptorValue(arg2);
2179
+ if (target) {
2180
+ setTypeMapEntry(typeMap, `${arg0.text}.${key}`, target, 0.85);
2181
+ }
2182
+ // Phase 8.3f: { get: getter } and/or { set: setter } → this inside each accessor is arg0 (obj)
2183
+ // Key format: '<accessorName>:this' — colon is a reserved separator used only by this phase.
2184
+ // JS identifiers cannot contain ':', so this key never collides with real variable names.
2185
+ for (const accessor of findDescriptorAccessors(arg2)) {
2186
+ setTypeMapEntry(typeMap, `${accessor}:this`, arg0.text, 0.85);
2187
+ }
2188
+ } else {
2189
+ // defineProperties
2190
+ if (args.length < 2) return;
2191
+ const arg0 = args[0]!,
2192
+ arg1 = args[1]!;
2193
+ if (arg0.type !== 'identifier') return;
2194
+ if (arg1.type !== 'object') return;
2195
+ for (let i = 0; i < arg1.childCount; i++) {
2196
+ const pair = arg1.child(i);
2197
+ if (pair?.type !== 'pair') continue;
2198
+ const keyN = pair.childForFieldName('key');
2199
+ const valN = pair.childForFieldName('value');
2200
+ if (!keyN || !valN) continue;
2201
+ const key = keyN.type === 'string' ? keyN.text.replace(/^['"]|['"]$/g, '') : keyN.text;
2202
+ const target = findDescriptorValue(valN);
2203
+ if (!target) continue;
2204
+ setTypeMapEntry(typeMap, `${arg0.text}.${key}`, target, 0.85);
2205
+ }
2206
+ }
2207
+ }
2208
+
2209
+ /** Return the identifier text of the `value` field in a property descriptor object. */
2210
+ function findDescriptorValue(desc: TreeSitterNode): string | undefined {
2211
+ if (desc.type !== 'object') return undefined;
2212
+ for (let i = 0; i < desc.childCount; i++) {
2213
+ const pair = desc.child(i);
2214
+ if (pair?.type !== 'pair') continue;
2215
+ const key = pair.childForFieldName('key');
2216
+ const val = pair.childForFieldName('value');
2217
+ if (key?.text === 'value' && val?.type === 'identifier') return val.text;
2218
+ }
2219
+ return undefined;
2220
+ }
2221
+
2222
+ /**
2223
+ * Phase 8.3f: return the identifier texts of all `get` and `set` accessors in a property
2224
+ * descriptor. `{ get: getter, set: setter }` → ['getter', 'setter'].
2225
+ * Returns all accessors so that each one gets a `callerName:this = obj` typeMap entry.
2226
+ */
2227
+ function findDescriptorAccessors(desc: TreeSitterNode): string[] {
2228
+ if (desc.type !== 'object') return [];
2229
+ const result: string[] = [];
2230
+ for (let i = 0; i < desc.childCount; i++) {
2231
+ const pair = desc.child(i);
2232
+ if (pair?.type !== 'pair') continue;
2233
+ const key = pair.childForFieldName('key');
2234
+ const val = pair.childForFieldName('value');
2235
+ if ((key?.text === 'get' || key?.text === 'set') && val?.type === 'identifier') {
2236
+ result.push(val.text);
2237
+ }
2238
+ }
2239
+ return result;
2240
+ }
2241
+
2242
+ /** Seed composite pts keys for each property in a prototype object literal. */
2243
+ function seedProtoProperties(
2244
+ varName: string,
2245
+ proto: TreeSitterNode,
2246
+ typeMap: Map<string, TypeMapEntry>,
2247
+ ): void {
2248
+ for (let i = 0; i < proto.childCount; i++) {
2249
+ const child = proto.child(i);
2250
+ if (!child) continue;
2251
+ if (child.type === 'shorthand_property_identifier') {
2252
+ setTypeMapEntry(typeMap, `${varName}.${child.text}`, child.text, 0.85);
2253
+ } else if (child.type === 'pair') {
2254
+ const keyN = child.childForFieldName('key');
2255
+ const valN = child.childForFieldName('value');
2256
+ if (!keyN || !valN || valN.type !== 'identifier') continue;
2257
+ const key = keyN.type === 'string' ? keyN.text.replace(/^['"]|['"]$/g, '') : keyN.text;
2258
+ setTypeMapEntry(typeMap, `${varName}.${key}`, valN.text, 0.85);
2259
+ }
2260
+ }
2261
+ }
2262
+
2263
+ /**
2264
+ * Phase 8.3c: record argument-to-parameter bindings at call sites.
2265
+ *
2266
+ * For each `f(x, y)` where the callee is a simple identifier and an argument
2267
+ * is a simple identifier, emits a ParamBinding so the pts solver can add
2268
+ * constraint: pts(param_i_of_f) ⊇ pts(arg_i). The solver uses the
2269
+ * definitionParams map to resolve the actual parameter names.
2270
+ *
2271
+ * Scope: intra-module only (the solver only materialises constraints for
2272
+ * locally-defined callees, so cross-module calls produce no spurious flow).
2273
+ */
2274
+ function collectParamBindings(node: TreeSitterNode, paramBindings: ParamBinding[]): void {
2275
+ const fn = node.childForFieldName('function');
2276
+ const args = node.childForFieldName('arguments') ?? findChild(node, 'arguments');
2277
+ if (fn?.type === 'identifier' && !BUILTIN_GLOBALS.has(fn.text) && args) {
2278
+ let argIdx = 0;
2279
+ for (let i = 0; i < args.childCount; i++) {
2280
+ const child = args.child(i);
2281
+ if (!child) continue;
2282
+ const ct = child.type;
2283
+ if (ct === ',' || ct === '(' || ct === ')') continue;
2284
+ if (ct === 'identifier' && !BUILTIN_GLOBALS.has(child.text)) {
2285
+ paramBindings.push({ callee: fn.text, argIndex: argIdx, argName: child.text });
2286
+ } else if (ct === 'spread_element') {
2287
+ // f(...[a, b]) — inline array literal: expand each element as a direct param binding.
2288
+ const inner =
2289
+ child.childForFieldName('argument') ?? (child.childCount > 1 ? child.child(1) : null);
2290
+ if (inner?.type === 'array') {
2291
+ let elemCount = 0;
2292
+ for (let j = 0; j < inner.childCount; j++) {
2293
+ const elem = inner.child(j);
2294
+ if (!elem) continue;
2295
+ if (elem.type === ',' || elem.type === '[' || elem.type === ']') continue;
2296
+ if (elem.type === 'identifier' && !BUILTIN_GLOBALS.has(elem.text)) {
2297
+ paramBindings.push({
2298
+ callee: fn.text,
2299
+ argIndex: argIdx + elemCount,
2300
+ argName: elem.text,
2301
+ });
2302
+ }
2303
+ elemCount++;
2304
+ }
2305
+ // Advance by the exact number of slots this spread occupies and skip
2306
+ // the unconditional argIdx++ below so that zero-element spreads (...[])
2307
+ // do not shift subsequent argument indices.
2308
+ argIdx += elemCount;
2309
+ continue;
2310
+ }
2311
+ }
2312
+ argIdx++;
2313
+ }
2314
+ }
2315
+ }
2316
+
2317
+ /** Collection constructors whose argument is treated as an element source. */
2318
+ const COLLECTION_CTOR_SET = new Set(['Set', 'Map']);
2319
+
2320
+ /**
2321
+ * Phase 8.3e: Extract array-element bindings from `const arr = [fn1, fn2]` patterns.
2322
+ * Emits an ArrayElemBinding for each identifier element in an array literal assigned
2323
+ * to a variable.
2324
+ */
2325
+ function collectArrayElemBindings(
2326
+ node: TreeSitterNode,
2327
+ arrayElemBindings: ArrayElemBinding[],
2328
+ ): void {
2329
+ const nameN = node.childForFieldName('name');
2330
+ const valueN = node.childForFieldName('value');
2331
+ if (nameN?.type === 'identifier' && valueN?.type === 'array') {
2332
+ let idx = 0;
2333
+ for (let i = 0; i < valueN.childCount; i++) {
2334
+ const elem = valueN.child(i);
2335
+ if (!elem) continue;
2336
+ if (elem.type === ',' || elem.type === '[' || elem.type === ']') continue;
2337
+ if (elem.type === 'identifier' && !BUILTIN_GLOBALS.has(elem.text)) {
2338
+ arrayElemBindings.push({ arrayName: nameN.text, index: idx, elemName: elem.text });
2339
+ }
2340
+ idx++;
2341
+ }
2342
+ }
2343
+ }
2344
+
2345
+ /**
2346
+ * Phase 8.3e collectors (spread-argument, Array.from, collection-wrap, for-of
2347
+ * bindings), invoked from runContextCollectorWalk:
2348
+ *
2349
+ * - Spread: `f(...arr)` → SpreadArgBinding
2350
+ * - Array.from: `Array.from(src, cb)` → ArrayCallbackBinding
2351
+ * - Collection wrap: `new Set(arr)` / `new Map(arr)` → FnRefBinding lhs=s[*] rhs=arr[*]
2352
+ * - For-of: `for (const x of arr)` → ForOfBinding
2353
+ */
2354
+ function collectSpreadAndArrayFromBindings(
2355
+ node: TreeSitterNode,
2356
+ spreadArgBindings: SpreadArgBinding[],
2357
+ arrayCallbackBindings: ArrayCallbackBinding[],
2358
+ ): void {
2359
+ const fn = node.childForFieldName('function');
2360
+ const argsNode = node.childForFieldName('arguments') ?? findChild(node, 'arguments');
2361
+
2362
+ // Spread: f(...arr)
2363
+ if (fn?.type === 'identifier' && !BUILTIN_GLOBALS.has(fn.text) && argsNode) {
2364
+ let argIdx = 0;
2365
+ for (let i = 0; i < argsNode.childCount; i++) {
2366
+ const child = argsNode.child(i);
2367
+ if (!child) continue;
2368
+ if (child.type === ',' || child.type === '(' || child.type === ')') continue;
2369
+ if (child.type === 'spread_element') {
2370
+ const spreadTarget =
2371
+ child.childForFieldName('argument') ?? (child.childCount > 1 ? child.child(1) : null);
2372
+ if (spreadTarget?.type === 'identifier' && !BUILTIN_GLOBALS.has(spreadTarget.text)) {
2373
+ spreadArgBindings.push({
2374
+ callee: fn.text,
2375
+ arrayName: spreadTarget.text,
2376
+ startIndex: argIdx,
2377
+ });
2378
+ }
2379
+ }
2380
+ argIdx++;
2381
+ }
2382
+ }
2383
+
2384
+ // Array.from(source, cb)
2385
+ if (fn?.type === 'member_expression' && argsNode) {
2386
+ const obj = fn.childForFieldName('object');
2387
+ const prop = fn.childForFieldName('property');
2388
+ if (obj?.text === 'Array' && prop?.text === 'from') {
2389
+ const fnArgs: TreeSitterNode[] = [];
2390
+ for (let i = 0; i < argsNode.childCount; i++) {
2391
+ const child = argsNode.child(i);
2392
+ if (!child) continue;
2393
+ if (child.type === ',' || child.type === '(' || child.type === ')') continue;
2394
+ fnArgs.push(child);
2395
+ }
2396
+ if (fnArgs.length >= 2) {
2397
+ const srcArg = fnArgs[0]!;
2398
+ const cbArg = fnArgs[1]!;
2399
+ if (
2400
+ srcArg.type === 'identifier' &&
2401
+ !BUILTIN_GLOBALS.has(srcArg.text) &&
2402
+ cbArg.type === 'identifier' &&
2403
+ !BUILTIN_GLOBALS.has(cbArg.text)
2404
+ ) {
2405
+ arrayCallbackBindings.push({ sourceName: srcArg.text, calleeName: cbArg.text });
2406
+ }
2407
+ }
2408
+ }
2409
+ }
2410
+ }
2411
+
2412
+ /** Collection wrap: `const s = new Set(arr)` or `new Map(arr)` (variable_declarator). */
2413
+ function collectCollectionWrapBinding(node: TreeSitterNode, fnRefBindings: FnRefBinding[]): void {
2414
+ const nameN = node.childForFieldName('name');
2415
+ const valueN = node.childForFieldName('value');
2416
+ if (nameN?.type === 'identifier' && valueN?.type === 'new_expression') {
2417
+ const ctor = valueN.childForFieldName('constructor');
2418
+ const args = valueN.childForFieldName('arguments');
2419
+ if (ctor && COLLECTION_CTOR_SET.has(ctor.text) && args) {
2420
+ for (let i = 0; i < args.childCount; i++) {
2421
+ const arg = args.child(i);
2422
+ if (!arg || arg.type === '(' || arg.type === ')') continue;
2423
+ if (arg.type === 'identifier' && !BUILTIN_GLOBALS.has(arg.text)) {
2424
+ fnRefBindings.push({ lhs: `${nameN.text}[*]`, rhs: `${arg.text}[*]` });
2425
+ break;
2426
+ }
2427
+ }
2428
+ }
2429
+ }
2430
+ }
2431
+
2432
+ /** For-of: `for (const x of arr)` (for_in_statement with an `of` keyword). */
2433
+ function collectForOfBinding(
2434
+ node: TreeSitterNode,
2435
+ enclosingFunc: string,
2436
+ forOfBindings: ForOfBinding[],
2437
+ ): void {
2438
+ let isForOf = false;
2439
+ for (let i = 0; i < node.childCount; i++) {
2440
+ if (node.child(i)?.text === 'of') {
2441
+ isForOf = true;
2442
+ break;
2443
+ }
2444
+ }
2445
+ if (!isForOf) return;
2446
+ const right = node.childForFieldName('right');
2447
+ if (right?.type !== 'identifier' || BUILTIN_GLOBALS.has(right.text)) return;
2448
+ const left = node.childForFieldName('left');
2449
+ let varName: string | null = null;
2450
+ if (left?.type === 'identifier') {
2451
+ varName = left.text;
2452
+ } else if (left) {
2453
+ for (let i = 0; i < left.childCount; i++) {
2454
+ const lc = left.child(i);
2455
+ if (lc?.type === 'variable_declarator') {
2456
+ const nc = lc.childForFieldName('name');
2457
+ if (nc?.type === 'identifier') {
2458
+ varName = nc.text;
2459
+ break;
2460
+ }
2461
+ } else if (
2462
+ lc?.type === 'identifier' &&
2463
+ lc.text !== 'const' &&
2464
+ lc.text !== 'let' &&
2465
+ lc.text !== 'var'
2466
+ ) {
2467
+ varName = lc.text;
2468
+ break;
2469
+ }
2470
+ }
2471
+ }
2472
+ if (varName && !BUILTIN_GLOBALS.has(varName)) {
2473
+ forOfBindings.push({ varName, sourceName: right.text, enclosingFunc });
2474
+ }
2475
+ }
2476
+
2477
+ /**
2478
+ * Phase 8.3f: record object-destructuring rest-parameter bindings from function definitions.
2479
+ *
2480
+ * For each `function f({ a, ...rest })` (or arrow/function-expression equivalent),
2481
+ * records { callee: 'f', restName: 'rest', argIndex: N }. Also covers class methods
2482
+ * (`callee: 'ClassName.method'`) and object-literal methods (`callee: 'method'`).
2483
+ * The edge builder uses these to seed typeMap[rest] = { type: argName } when f(obj)
2484
+ * is called with an identifier, enabling `rest.method()` calls to resolve.
2485
+ */
2486
+ function collectObjectRestParams(
2487
+ node: TreeSitterNode,
2488
+ t: string,
2489
+ currentClass: string | null,
2490
+ bindings: ObjectRestParamBinding[],
2491
+ ): void {
2492
+ let fnName: string | null = null;
2493
+ let paramsNode: TreeSitterNode | null = null;
2494
+
2495
+ if (t === 'function_declaration' || t === 'generator_function_declaration') {
2496
+ const nameN = node.childForFieldName('name');
2497
+ if (nameN?.type === 'identifier') fnName = nameN.text;
2498
+ paramsNode = node.childForFieldName('parameters') ?? findChild(node, 'formal_parameters');
2499
+ } else if (t === 'variable_declarator') {
2500
+ const nameN = node.childForFieldName('name');
2501
+ const valueN = node.childForFieldName('value');
2502
+ if (nameN?.type === 'identifier' && valueN) {
2503
+ const vt = valueN.type;
2504
+ if (vt === 'arrow_function' || vt === 'function_expression' || vt === 'generator_function') {
2505
+ fnName = nameN.text;
2506
+ paramsNode =
2507
+ valueN.childForFieldName('parameters') ?? findChild(valueN, 'formal_parameters');
2508
+ }
2509
+ }
2510
+ } else if (t === 'method_definition') {
2511
+ // class method: `class Foo { bar({ a, ...rest }) {} }`
2512
+ // object-literal shorthand method: `{ bar({ a, ...rest }) {} }`
2513
+ const nameN = node.childForFieldName('name');
2514
+ if (nameN) {
2515
+ fnName = currentClass ? `${currentClass}.${nameN.text}` : nameN.text;
2516
+ paramsNode = node.childForFieldName('parameters') ?? findChild(node, 'formal_parameters');
2517
+ }
2518
+ } else if (t === 'pair') {
2519
+ // object-literal method: `{ bar: function({ a, ...rest }) {} }`
2520
+ // Skip computed property keys (e.g. `{ [Symbol.iterator]: function({ ...rest }) {} }`)
2521
+ // because `callee: '[Symbol.iterator]'` can never match a paramBinding callee.
2522
+ const keyN = node.childForFieldName('key');
2523
+ const valueN = node.childForFieldName('value');
2524
+ if (keyN && valueN && keyN.type !== 'computed_property_name') {
2525
+ const vt = valueN.type;
2526
+ if (vt === 'arrow_function' || vt === 'function_expression' || vt === 'generator_function') {
2527
+ fnName = keyN.type === 'string' ? keyN.text.slice(1, -1) : keyN.text;
2528
+ paramsNode =
2529
+ valueN.childForFieldName('parameters') ?? findChild(valueN, 'formal_parameters');
2530
+ }
2531
+ }
2532
+ }
2533
+
2534
+ if (fnName && paramsNode) {
2535
+ let paramIdx = 0;
2536
+ for (let i = 0; i < paramsNode.childCount; i++) {
2537
+ const child = paramsNode.child(i);
2538
+ if (!child) continue;
2539
+ const ct = child.type;
2540
+ if (ct === ',' || ct === '(' || ct === ')') continue;
2541
+ if (ct === 'object_pattern') {
2542
+ for (let j = 0; j < child.childCount; j++) {
2543
+ const inner = child.child(j);
2544
+ if (!inner) continue;
2545
+ if (inner.type === 'rest_pattern' || inner.type === 'rest_element') {
2546
+ // rest_pattern node: `...identifier` — the identifier is at child index 1
2547
+ const restId = inner.child(1) ?? inner.childForFieldName('name');
2548
+ if (restId?.type === 'identifier') {
2549
+ bindings.push({ callee: fnName, restName: restId.text, argIndex: paramIdx });
2550
+ }
2551
+ }
2552
+ }
2553
+ }
2554
+ paramIdx++;
2555
+ }
2556
+ }
2557
+ }
2558
+
2559
+ /**
2560
+ * Phase 8.3f: collect object-property bindings from object literals.
2561
+ *
2562
+ * `const obj = { e4 }` → `{ objectName: "obj", propName: "e4", valueName: "e4" }`
2563
+ * `const obj = { e1: fn }` → `{ objectName: "obj", propName: "e1", valueName: "fn" }`
2564
+ *
2565
+ * Only tracks shorthand and `key: identifier` pairs; skips function literals.
2566
+ */
2567
+ function collectObjectPropBindings(node: TreeSitterNode, bindings: ObjectPropBinding[]): void {
2568
+ const nameN = node.childForFieldName('name');
2569
+ const valueN = node.childForFieldName('value');
2570
+ if (nameN?.type === 'identifier' && valueN?.type === 'object') {
2571
+ const objectName = nameN.text;
2572
+ for (let i = 0; i < valueN.childCount; i++) {
2573
+ const child = valueN.child(i);
2574
+ if (!child) continue;
2575
+ if (child.type === 'shorthand_property_identifier') {
2576
+ bindings.push({ objectName, propName: child.text, valueName: child.text });
2577
+ } else if (child.type === 'pair') {
2578
+ const keyN = child.childForFieldName('key');
2579
+ const valN = child.childForFieldName('value');
2580
+ if (
2581
+ keyN?.type === 'property_identifier' &&
2582
+ valN?.type === 'identifier' &&
2583
+ !BUILTIN_GLOBALS.has(valN.text)
2584
+ ) {
2585
+ bindings.push({ objectName, propName: keyN.text, valueName: valN.text });
2586
+ }
2587
+ }
2588
+ }
2589
+ }
2590
+ }
2591
+
1210
2592
  function extractReceiverName(objNode: TreeSitterNode | null): string | undefined {
1211
2593
  if (!objNode) return undefined;
1212
2594
  const t = objNode.type;
1213
2595
  if (t === 'identifier' || t === 'this' || t === 'super') return objNode.text;
2596
+ // `(new Foo(...)).method()` — extract the constructor name so the resolver can
2597
+ // look up `Foo.method` directly without relying on a text-based regex heuristic.
2598
+ if (t === 'new_expression') {
2599
+ const name = extractNewExprTypeName(objNode);
2600
+ if (name) return name;
2601
+ }
2602
+ if (t === 'parenthesized_expression') {
2603
+ // Only one level of parentheses is unwrapped here. Doubly-nested parens
2604
+ // (e.g. `((new Dog())).bark()`) and cast expressions inside parens
2605
+ // (e.g. `(new Dog() as Animal).bark()`) fall through to raw-text handling
2606
+ // below and are caught by the regex fallback in call-resolver.ts.
2607
+ for (let i = 0; i < objNode.childCount; i++) {
2608
+ const child = objNode.child(i);
2609
+ if (child?.type === 'new_expression') {
2610
+ const name = extractNewExprTypeName(child);
2611
+ if (name) return name;
2612
+ }
2613
+ }
2614
+ }
1214
2615
  return objNode.text;
1215
2616
  }
1216
2617
 
@@ -1421,12 +2822,23 @@ function firstArgIsStringLiteral(argsNode: TreeSitterNode): boolean {
1421
2822
  * member-expr args are only emitted when the first argument is a string
1422
2823
  * literal route path — matching Express/router shape and skipping
1423
2824
  * `cache.get(user.id)`-style calls.
2825
+ *
2826
+ * `.call()` / `.apply()` / `.bind()` — the first arg is the `this` context (not a callback of
2827
+ * the enclosing function) and subsequent args flow into the delegated function's parameters.
2828
+ * Emitting them here would produce false-positive edges from the *calling* function.
2829
+ * This-rebinding (fn::this → ctx) is handled separately by extractThisCallBindingsWalk.
1424
2830
  */
1425
2831
  function extractCallbackReferenceCalls(callNode: TreeSitterNode): Call[] {
1426
2832
  const args = callNode.childForFieldName('arguments') || findChild(callNode, 'arguments');
1427
2833
  if (!args) return [];
1428
2834
 
1429
2835
  const calleeName = extractCalleeName(callNode);
2836
+ // .call() / .apply() / .bind() — the first arg is the `this` context (not a callback of
2837
+ // the enclosing function) and subsequent args flow into the delegated function's parameters.
2838
+ // Emitting them here would produce false-positive edges from the *calling* function.
2839
+ // This-rebinding (fn::this → ctx) is handled separately by extractThisCallBindingsWalk.
2840
+ if (calleeName === 'call' || calleeName === 'apply' || calleeName === 'bind') return [];
2841
+
1430
2842
  let memberExprArgsAllowed = calleeName !== null && CALLBACK_ACCEPTING_CALLEES.has(calleeName);
1431
2843
  if (memberExprArgsAllowed && calleeName !== null && HTTP_VERB_CALLEES.has(calleeName)) {
1432
2844
  // HTTP verbs require a string-literal route path to be treated as a
@@ -1457,6 +2869,151 @@ function extractCallbackReferenceCalls(callNode: TreeSitterNode): Call[] {
1457
2869
  return result;
1458
2870
  }
1459
2871
 
2872
+ /**
2873
+ * Collect, from a call_expression node:
2874
+ * - `this(args)` call expressions → `{name: 'this', ...}` entries in `calls`
2875
+ * (where `this` is used as a function, not as a receiver)
2876
+ * - `fn.call(namedCtx, ...)` / `fn.apply(namedCtx, ...)` bindings →
2877
+ * `{ callee: 'fn', thisArg: 'namedCtx' }` entries in `thisCallBindings`
2878
+ */
2879
+ function collectThisCallAndBindings(
2880
+ node: TreeSitterNode,
2881
+ calls: Call[],
2882
+ thisCallBindings: ThisCallBinding[],
2883
+ ): void {
2884
+ const fn = node.childForFieldName('function');
2885
+ if (fn?.type === 'this') {
2886
+ calls.push({ name: 'this', line: nodeStartLine(node) });
2887
+ } else if (fn?.type === 'member_expression') {
2888
+ const obj = fn.childForFieldName('object');
2889
+ const prop = fn.childForFieldName('property');
2890
+ if (
2891
+ obj?.type === 'identifier' &&
2892
+ prop &&
2893
+ (prop.text === 'call' || prop.text === 'apply') &&
2894
+ !BUILTIN_GLOBALS.has(obj.text)
2895
+ ) {
2896
+ const args = node.childForFieldName('arguments') || findChild(node, 'arguments');
2897
+ if (args) {
2898
+ for (let i = 0; i < args.childCount; i++) {
2899
+ const child = args.child(i);
2900
+ if (!child) continue;
2901
+ const t = child.type;
2902
+ if (t === '(' || t === ')' || t === ',') continue;
2903
+ // First real argument: only bind if it's a plain identifier
2904
+ if (
2905
+ t === 'identifier' &&
2906
+ !BUILTIN_GLOBALS.has(child.text) &&
2907
+ child.text !== 'undefined' &&
2908
+ child.text !== 'null'
2909
+ ) {
2910
+ thisCallBindings.push({ callee: obj.text, thisArg: child.text });
2911
+ }
2912
+ break;
2913
+ }
2914
+ }
2915
+ }
2916
+ }
2917
+ }
2918
+
2919
+ /**
2920
+ * Outputs for {@link runCollectorWalk}. Required targets are collected on both
2921
+ * extraction paths; optional targets are path-specific:
2922
+ * - `imports` / `calls`+`thisCallBindings` / `classMemberDefs` — query path only
2923
+ * (the walk path's walkJavaScriptNode covers those node types itself).
2924
+ * - `funcPropDefs` — walk path only (the query path captures `fn.method = …`
2925
+ * assignments via the `assign_left`/`assign_right` query pattern).
2926
+ */
2927
+ interface CollectorWalkTargets {
2928
+ definitions: Definition[];
2929
+ typeMap: Map<string, TypeMapEntry>;
2930
+ paramBindings: ParamBinding[];
2931
+ arrayElemBindings: ArrayElemBinding[];
2932
+ objectPropBindings: ObjectPropBinding[];
2933
+ newExpressions: string[];
2934
+ definePropertyReceivers: Map<string, string>;
2935
+ imports?: Import[];
2936
+ calls?: Call[];
2937
+ thisCallBindings?: ThisCallBinding[];
2938
+ classMemberDefs?: Definition[];
2939
+ funcPropDefs?: Definition[];
2940
+ }
2941
+
2942
+ /**
2943
+ * Single-pass collector walk: one DFS that dispatches each node to every
2944
+ * collector interested in its type.
2945
+ *
2946
+ * This replaces what had grown to ten independent full-tree traversals (one
2947
+ * per collector). On WASM trees every node access (`child(i)`, `.type`,
2948
+ * `childForFieldName`) marshals through the JS↔WASM boundary, so traversal
2949
+ * count — not collector work — dominated extraction cost: the accumulated
2950
+ * per-collector walks made extraction ~2.4× slower between v3.11.2 and
2951
+ * v3.12.0 (7.5 → 17.7 ms/file on codegraph's own corpus).
2952
+ *
2953
+ * Collectors with bespoke traversal semantics stay separate:
2954
+ * - extractConstantsWalk / extractDestructuredBindingsWalk prune function
2955
+ * scopes and unwrap export statements on the way down;
2956
+ * - extractReturnTypeMapWalk / extractTypeMapWalk / extractSpreadForOfWalk /
2957
+ * extractObjectRestParamBindingsWalk thread enclosing-class context with
2958
+ * per-walk reset rules that intentionally differ (see each walk's comments).
2959
+ */
2960
+ function runCollectorWalk(rootNode: TreeSitterNode, targets: CollectorWalkTargets): void {
2961
+ const walk = (node: TreeSitterNode, depth: number, inDynamicImport: boolean): void => {
2962
+ if (depth >= MAX_WALK_DEPTH) return;
2963
+ let childInDynamicImport = inDynamicImport;
2964
+ switch (node.type) {
2965
+ case 'call_expression': {
2966
+ // Matched import() calls suppress *dynamic-import* collection in their
2967
+ // argument subtree (mirrors the old walk's early return) while leaving
2968
+ // the subtree visible to every other collector. The !inDynamicImport
2969
+ // check runs first so nested import() calls are neither collected nor
2970
+ // re-matched.
2971
+ if (targets.imports && !inDynamicImport && collectDynamicImport(node, targets.imports)) {
2972
+ childInDynamicImport = true;
2973
+ }
2974
+ if (targets.calls && targets.thisCallBindings) {
2975
+ collectThisCallAndBindings(node, targets.calls, targets.thisCallBindings);
2976
+ }
2977
+ collectParamBindings(node, targets.paramBindings);
2978
+ collectDefinePropertyReceiver(node, targets.definePropertyReceivers);
2979
+ break;
2980
+ }
2981
+ case 'variable_declarator':
2982
+ collectArrayElemBindings(node, targets.arrayElemBindings);
2983
+ collectObjectPropBindings(node, targets.objectPropBindings);
2984
+ break;
2985
+ case 'expression_statement': {
2986
+ const expr = node.child(0);
2987
+ if (expr?.type === 'assignment_expression') {
2988
+ const lhs = expr.childForFieldName('left');
2989
+ const rhs = expr.childForFieldName('right');
2990
+ if (lhs && rhs) {
2991
+ handlePrototypeAssignment(lhs, rhs, targets.definitions, targets.typeMap);
2992
+ if (targets.funcPropDefs) handleFuncPropAssignment(lhs, rhs, targets.funcPropDefs);
2993
+ }
2994
+ }
2995
+ break;
2996
+ }
2997
+ case 'new_expression': {
2998
+ const name = extractNewExprTypeName(node);
2999
+ if (name) targets.newExpressions.push(name);
3000
+ break;
3001
+ }
3002
+ case 'field_definition':
3003
+ case 'public_field_definition':
3004
+ if (targets.classMemberDefs) handleFieldDef(node, targets.classMemberDefs);
3005
+ break;
3006
+ case 'class_static_block':
3007
+ if (targets.classMemberDefs) handleStaticBlock(node, targets.classMemberDefs);
3008
+ break;
3009
+ }
3010
+ for (let i = 0; i < node.childCount; i++) {
3011
+ walk(node.child(i)!, depth + 1, childInDynamicImport);
3012
+ }
3013
+ };
3014
+ walk(rootNode, 0, false);
3015
+ }
3016
+
1460
3017
  function findAnonymousCallback(argsNode: TreeSitterNode): TreeSitterNode | null {
1461
3018
  for (let i = 0; i < argsNode.childCount; i++) {
1462
3019
  const child = argsNode.child(i);
@@ -1517,7 +3074,7 @@ function extractCallbackDefinition(
1517
3074
  fn?: TreeSitterNode | null,
1518
3075
  ): Definition | null {
1519
3076
  if (!fn) fn = callNode.childForFieldName('function');
1520
- if (!fn || fn.type !== 'member_expression') return null;
3077
+ if (fn?.type !== 'member_expression') return null;
1521
3078
 
1522
3079
  const prop = fn.childForFieldName('property');
1523
3080
  if (!prop) return null;
@@ -1549,7 +3106,7 @@ function extractCallbackDefinition(
1549
3106
  // Express: app.get('/path', callback)
1550
3107
  if (EXPRESS_METHODS.has(method)) {
1551
3108
  const strArg = findFirstStringArg(args);
1552
- if (!strArg || !strArg.startsWith('/')) return null;
3109
+ if (!strArg?.startsWith('/')) return null;
1553
3110
  const cb = findAnonymousCallback(args);
1554
3111
  if (!cb) return null;
1555
3112
  return {
@@ -1588,7 +3145,7 @@ function extractSuperclass(heritage: TreeSitterNode): string | null {
1588
3145
  return null;
1589
3146
  }
1590
3147
 
1591
- const JS_CLASS_TYPES = ['class_declaration', 'class'] as const;
3148
+ const JS_CLASS_TYPES = ['class_declaration', 'abstract_class_declaration', 'class'] as const;
1592
3149
  function findParentClass(node: TreeSitterNode): string | null {
1593
3150
  return findParentNode(node, JS_CLASS_TYPES);
1594
3151
  }
@@ -1628,7 +3185,7 @@ function extractDynamicImportNames(callNode: TreeSitterNode): string[] {
1628
3185
  // Skip await_expression wrapper if present
1629
3186
  if (current && current.type === 'await_expression') current = current.parent;
1630
3187
  // We should now be at a variable_declarator (or not, if standalone import())
1631
- if (!current || current.type !== 'variable_declarator') return [];
3188
+ if (current?.type !== 'variable_declarator') return [];
1632
3189
 
1633
3190
  const nameNode = current.childForFieldName('name');
1634
3191
  if (!nameNode) return [];
@@ -1671,3 +3228,182 @@ function extractDynamicImportNames(callNode: TreeSitterNode): string[] {
1671
3228
 
1672
3229
  return [];
1673
3230
  }
3231
+
3232
+ // ── Phase 8.X: Prototype-based method extraction ────────────────────────────
3233
+
3234
+ /**
3235
+ * Walk the AST and extract prototype-based method definitions and aliases.
3236
+ *
3237
+ * Handles three patterns:
3238
+ * 1. `Foo.prototype.bar = function(){...}` — emits Foo.bar as method definition
3239
+ * 2. `Foo.prototype.bar = identifier` — sets typeMap['Foo.bar'] = { type: identifier }
3240
+ * 3. `Foo.prototype = { bar: fn, ... }` — emits defs and typeMap entries per property
3241
+ *
3242
+ * Emitting definitions under the canonical `ClassName.methodName` name lets the
3243
+ * existing typeMap-based call resolver find them when a typed receiver dispatches
3244
+ * `instance.method()` (lookup.byName('C.foo') in resolveByMethodOrGlobal).
3245
+ *
3246
+ * typeMap entries for identifier aliases (`Foo.bar → { type: 'someId' }`) are
3247
+ * consumed by the prototype-alias fallback added to resolveByMethodOrGlobal.
3248
+ */
3249
+ // Prototype-method assignments (`Foo.prototype.bar = fn`) are collected inline
3250
+ // in runCollectorWalk's expression_statement case via handlePrototypeAssignment.
3251
+
3252
+ /**
3253
+ * Handle an assignment_expression that may be a prototype assignment.
3254
+ *
3255
+ * Matches:
3256
+ * - `Foo.prototype.bar = rhs` (lhs ends in .prototype.bar)
3257
+ * - `Foo.prototype = { ... }` (lhs ends in .prototype, rhs is object literal)
3258
+ */
3259
+ function handlePrototypeAssignment(
3260
+ lhs: TreeSitterNode,
3261
+ rhs: TreeSitterNode,
3262
+ definitions: Definition[],
3263
+ typeMap: Map<string, TypeMapEntry>,
3264
+ ): void {
3265
+ if (lhs.type !== 'member_expression') return;
3266
+
3267
+ const lhsObj = lhs.childForFieldName('object');
3268
+ const lhsProp = lhs.childForFieldName('property');
3269
+ if (!lhsObj || !lhsProp) return;
3270
+
3271
+ // Pattern 1: `Foo.prototype.bar = rhs`
3272
+ // lhs.object is `Foo.prototype` (member_expression), lhs.property is `bar`
3273
+ if (
3274
+ lhsObj.type === 'member_expression' &&
3275
+ (lhsProp.type === 'property_identifier' || lhsProp.type === 'identifier')
3276
+ ) {
3277
+ const protoObj = lhsObj.childForFieldName('object');
3278
+ const protoProp = lhsObj.childForFieldName('property');
3279
+ if (
3280
+ protoObj?.type === 'identifier' &&
3281
+ protoProp?.text === 'prototype' &&
3282
+ !BUILTIN_GLOBALS.has(protoObj.text)
3283
+ ) {
3284
+ emitPrototypeMethod(protoObj.text, lhsProp.text, rhs, definitions, typeMap);
3285
+ }
3286
+ return;
3287
+ }
3288
+
3289
+ // Pattern 2: `Foo.prototype = { bar: fn, ... }`
3290
+ // lhs.object is `Foo` (identifier), lhs.property is `prototype`
3291
+ if (
3292
+ lhsObj.type === 'identifier' &&
3293
+ lhsProp.text === 'prototype' &&
3294
+ !BUILTIN_GLOBALS.has(lhsObj.text) &&
3295
+ rhs.type === 'object'
3296
+ ) {
3297
+ extractPrototypeObjectLiteral(lhsObj.text, rhs, definitions, typeMap);
3298
+ }
3299
+ }
3300
+
3301
+ /** Emit one prototype method definition or typeMap alias for `ClassName.methodName = rhs`. */
3302
+ function emitPrototypeMethod(
3303
+ className: string,
3304
+ methodName: string,
3305
+ rhs: TreeSitterNode,
3306
+ definitions: Definition[],
3307
+ typeMap: Map<string, TypeMapEntry>,
3308
+ ): void {
3309
+ const fullName = `${className}.${methodName}`;
3310
+ if (rhs.type === 'function_expression' || rhs.type === 'arrow_function') {
3311
+ definitions.push({
3312
+ name: fullName,
3313
+ kind: 'method',
3314
+ line: nodeStartLine(rhs),
3315
+ endLine: nodeEndLine(rhs),
3316
+ });
3317
+ } else if (rhs.type === 'identifier' && !BUILTIN_GLOBALS.has(rhs.text)) {
3318
+ // Prototype alias: `A.prototype.t = f` → typeMap['A.t'] = { type: 'f' }
3319
+ // Consumed by the prototype-alias fallback in resolveByMethodOrGlobal.
3320
+ setTypeMapEntry(typeMap, fullName, rhs.text, 0.9);
3321
+ }
3322
+ }
3323
+
3324
+ /**
3325
+ * Extract function-as-object property method definitions.
3326
+ *
3327
+ * Handles `fn.method = function() {}` and `fn.method = () => {}` patterns.
3328
+ * Emits a `method` definition named `fn.method` so that:
3329
+ * 1. `findCaller` attributes calls inside the body to `fn.method`
3330
+ * 2. `resolveByMethodOrGlobal` resolves `this.other()` inside `fn.method` to `fn.other`
3331
+ *
3332
+ * Excludes BUILTIN_GLOBALS objects and `.prototype` (handled by extractPrototypeMethodsWalk).
3333
+ */
3334
+ // Function-as-object-property assignments (`fn.method = function(){}`) are
3335
+ // collected inline in runCollectorWalk's expression_statement case (walk path
3336
+ // only — the query path captures them via the `assign_left`/`assign_right`
3337
+ // query pattern in dispatchQueryMatch).
3338
+
3339
+ function handleFuncPropAssignment(
3340
+ lhs: TreeSitterNode,
3341
+ rhs: TreeSitterNode,
3342
+ definitions: Definition[],
3343
+ ): void {
3344
+ if (lhs.type !== 'member_expression') return;
3345
+ if (rhs.type !== 'function_expression' && rhs.type !== 'arrow_function') return;
3346
+
3347
+ const obj = lhs.childForFieldName('object');
3348
+ const prop = lhs.childForFieldName('property');
3349
+ if (!obj || !prop) return;
3350
+ if (obj.type !== 'identifier') return;
3351
+ if (prop.type !== 'property_identifier' && prop.type !== 'identifier') return;
3352
+ if (BUILTIN_GLOBALS.has(obj.text)) return;
3353
+ if (prop.text === 'prototype') return;
3354
+
3355
+ const params = extractParameters(rhs);
3356
+ definitions.push({
3357
+ name: `${obj.text}.${prop.text}`,
3358
+ kind: 'method',
3359
+ line: nodeStartLine(rhs),
3360
+ endLine: nodeEndLine(rhs),
3361
+ children: params.length > 0 ? params : undefined,
3362
+ });
3363
+ }
3364
+
3365
+ /** Iterate over an object literal assigned to `Foo.prototype` and emit defs/aliases. */
3366
+ function extractPrototypeObjectLiteral(
3367
+ className: string,
3368
+ objNode: TreeSitterNode,
3369
+ definitions: Definition[],
3370
+ typeMap: Map<string, TypeMapEntry>,
3371
+ ): void {
3372
+ for (let i = 0; i < objNode.childCount; i++) {
3373
+ const child = objNode.child(i);
3374
+ if (!child) continue;
3375
+
3376
+ if (child.type === 'method_definition') {
3377
+ // Shorthand method: `Foo.prototype = { bar() {} }`
3378
+ const nameNode = child.childForFieldName('name');
3379
+ if (nameNode) {
3380
+ definitions.push({
3381
+ name: `${className}.${nameNode.text}`,
3382
+ kind: 'method',
3383
+ line: nodeStartLine(child),
3384
+ endLine: nodeEndLine(child),
3385
+ });
3386
+ }
3387
+ continue;
3388
+ }
3389
+
3390
+ if (child.type === 'shorthand_property_identifier') {
3391
+ // ES6 shorthand: `Foo.prototype = { bar }` → alias typeMap['Foo.bar'] = { type: 'bar' }
3392
+ if (!BUILTIN_GLOBALS.has(child.text)) {
3393
+ setTypeMapEntry(typeMap, `${className}.${child.text}`, child.text, 0.9);
3394
+ }
3395
+ continue;
3396
+ }
3397
+
3398
+ if (child.type !== 'pair') continue;
3399
+
3400
+ const keyNode = child.childForFieldName('key');
3401
+ const valueNode = child.childForFieldName('value');
3402
+ if (!keyNode || !valueNode) continue;
3403
+
3404
+ const methodName = keyNode.type === 'string' ? keyNode.text.replace(/['"]/g, '') : keyNode.text;
3405
+ if (!methodName) continue;
3406
+
3407
+ emitPrototypeMethod(className, methodName, valueNode, definitions, typeMap);
3408
+ }
3409
+ }