gitnexus 1.6.8-rc.2 → 1.6.8-rc.21

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 (193) hide show
  1. package/README.md +20 -0
  2. package/dist/_shared/scope-resolution/parsed-file.d.ts +21 -0
  3. package/dist/_shared/scope-resolution/parsed-file.d.ts.map +1 -1
  4. package/dist/_shared/scope-resolution/symbol-definition.d.ts +4 -0
  5. package/dist/_shared/scope-resolution/symbol-definition.d.ts.map +1 -1
  6. package/dist/cli/ai-context.js +1 -0
  7. package/dist/cli/analyze-config.js +40 -0
  8. package/dist/cli/analyze.d.ts +22 -0
  9. package/dist/cli/analyze.js +36 -6
  10. package/dist/cli/clean.d.ts +1 -0
  11. package/dist/cli/clean.js +43 -1
  12. package/dist/cli/eval-server.js +43 -0
  13. package/dist/cli/help-i18n.js +11 -0
  14. package/dist/cli/i18n/en.d.ts +13 -0
  15. package/dist/cli/i18n/en.js +13 -0
  16. package/dist/cli/i18n/resources.d.ts +26 -0
  17. package/dist/cli/i18n/zh-CN.d.ts +13 -0
  18. package/dist/cli/i18n/zh-CN.js +13 -0
  19. package/dist/cli/index.js +19 -0
  20. package/dist/cli/list.js +12 -0
  21. package/dist/cli/optional-grammars.d.ts +6 -8
  22. package/dist/cli/optional-grammars.js +8 -11
  23. package/dist/cli/skill-gen.js +1 -0
  24. package/dist/cli/status.js +26 -5
  25. package/dist/cli/tool.d.ts +12 -1
  26. package/dist/cli/tool.js +41 -1
  27. package/dist/core/embeddings/embedder.js +4 -0
  28. package/dist/core/embeddings/embedding-pipeline.js +27 -16
  29. package/dist/core/embeddings/onnxruntime-common-resolver.d.ts +6 -0
  30. package/dist/core/embeddings/onnxruntime-common-resolver.js +130 -0
  31. package/dist/core/graph/import-cycles.d.ts +10 -0
  32. package/dist/core/graph/import-cycles.js +103 -0
  33. package/dist/core/group/extractors/grpc-patterns/proto.js +10 -6
  34. package/dist/core/group/extractors/http-patterns/java.js +3 -50
  35. package/dist/core/group/extractors/http-patterns/kotlin.js +7 -7
  36. package/dist/core/group/extractors/include-extractor.js +7 -7
  37. package/dist/core/ingestion/cfg/cfg-builder.d.ts +64 -0
  38. package/dist/core/ingestion/cfg/cfg-builder.js +129 -0
  39. package/dist/core/ingestion/cfg/collect.d.ts +30 -0
  40. package/dist/core/ingestion/cfg/collect.js +34 -0
  41. package/dist/core/ingestion/cfg/control-flow-context.d.ts +97 -0
  42. package/dist/core/ingestion/cfg/control-flow-context.js +113 -0
  43. package/dist/core/ingestion/cfg/emit.d.ts +144 -0
  44. package/dist/core/ingestion/cfg/emit.js +315 -0
  45. package/dist/core/ingestion/cfg/reaching-defs.d.ts +90 -0
  46. package/dist/core/ingestion/cfg/reaching-defs.js +364 -0
  47. package/dist/core/ingestion/cfg/traversal-result.d.ts +20 -0
  48. package/dist/core/ingestion/cfg/traversal-result.js +2 -0
  49. package/dist/core/ingestion/cfg/types.d.ts +231 -0
  50. package/dist/core/ingestion/cfg/types.js +13 -0
  51. package/dist/core/ingestion/cfg/visitors/typescript-harvest.d.ts +144 -0
  52. package/dist/core/ingestion/cfg/visitors/typescript-harvest.js +1032 -0
  53. package/dist/core/ingestion/cfg/visitors/typescript.d.ts +66 -0
  54. package/dist/core/ingestion/cfg/visitors/typescript.js +584 -0
  55. package/dist/core/ingestion/language-provider.d.ts +36 -2
  56. package/dist/core/ingestion/languages/c-cpp.js +11 -4
  57. package/dist/core/ingestion/languages/cpp/arity-metadata.js +6 -2
  58. package/dist/core/ingestion/languages/cpp/captures.js +24 -1
  59. package/dist/core/ingestion/languages/cpp/query.js +23 -0
  60. package/dist/core/ingestion/languages/java.js +3 -0
  61. package/dist/core/ingestion/languages/kotlin/query.js +3 -2
  62. package/dist/core/ingestion/languages/kotlin.js +5 -1
  63. package/dist/core/ingestion/languages/typescript.js +5 -0
  64. package/dist/core/ingestion/method-extractors/configs/c-cpp.js +6 -13
  65. package/dist/core/ingestion/method-extractors/generic.js +1 -0
  66. package/dist/core/ingestion/method-types.d.ts +2 -0
  67. package/dist/core/ingestion/model/symbol-table.d.ts +1 -0
  68. package/dist/core/ingestion/model/symbol-table.js +1 -0
  69. package/dist/core/ingestion/parsing-processor.js +22 -0
  70. package/dist/core/ingestion/pipeline-phases/parse-impl.js +18 -1
  71. package/dist/core/ingestion/pipeline-phases/routes.js +64 -14
  72. package/dist/core/ingestion/pipeline.d.ts +57 -0
  73. package/dist/core/ingestion/route-extractors/spring-shared.d.ts +50 -0
  74. package/dist/core/ingestion/route-extractors/spring-shared.js +80 -0
  75. package/dist/core/ingestion/route-extractors/spring.d.ts +35 -0
  76. package/dist/core/ingestion/route-extractors/spring.js +136 -0
  77. package/dist/core/ingestion/scope-extractor.js +3 -0
  78. package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +12 -0
  79. package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +145 -24
  80. package/dist/core/ingestion/scope-resolution/pipeline/phase.js +6 -0
  81. package/dist/core/ingestion/scope-resolution/pipeline/reconcile-ownership.js +43 -3
  82. package/dist/core/ingestion/scope-resolution/pipeline/run.d.ts +21 -0
  83. package/dist/core/ingestion/scope-resolution/pipeline/run.js +207 -0
  84. package/dist/core/ingestion/scope-resolution/resolution-outcome.d.ts +1 -1
  85. package/dist/core/ingestion/taint/emit.d.ts +124 -0
  86. package/dist/core/ingestion/taint/emit.js +204 -0
  87. package/dist/core/ingestion/taint/match.d.ts +153 -0
  88. package/dist/core/ingestion/taint/match.js +278 -0
  89. package/dist/core/ingestion/taint/path-codec.d.ts +134 -0
  90. package/dist/core/ingestion/taint/path-codec.js +190 -0
  91. package/dist/core/ingestion/taint/propagate.d.ts +216 -0
  92. package/dist/core/ingestion/taint/propagate.js +664 -0
  93. package/dist/core/ingestion/taint/site-safety.d.ts +29 -0
  94. package/dist/core/ingestion/taint/site-safety.js +98 -0
  95. package/dist/core/ingestion/taint/source-sink-config.d.ts +94 -23
  96. package/dist/core/ingestion/taint/source-sink-config.js +11 -11
  97. package/dist/core/ingestion/taint/source-sink-registry.d.ts +6 -4
  98. package/dist/core/ingestion/taint/source-sink-registry.js +6 -4
  99. package/dist/core/ingestion/taint/typescript-model.d.ts +38 -0
  100. package/dist/core/ingestion/taint/typescript-model.js +102 -0
  101. package/dist/core/ingestion/utils/method-props.js +1 -0
  102. package/dist/core/ingestion/workers/clone-safety.d.ts +109 -0
  103. package/dist/core/ingestion/workers/clone-safety.js +465 -0
  104. package/dist/core/ingestion/workers/parse-worker.d.ts +11 -0
  105. package/dist/core/ingestion/workers/parse-worker.js +70 -53
  106. package/dist/core/ingestion/workers/post-result.d.ts +22 -0
  107. package/dist/core/ingestion/workers/post-result.js +87 -0
  108. package/dist/core/ingestion/workers/result-merge.d.ts +20 -0
  109. package/dist/core/ingestion/workers/result-merge.js +43 -0
  110. package/dist/core/ingestion/workers/worker-pool.d.ts +15 -0
  111. package/dist/core/ingestion/workers/worker-pool.js +33 -16
  112. package/dist/core/lbug/lbug-adapter.d.ts +19 -0
  113. package/dist/core/lbug/lbug-adapter.js +56 -1
  114. package/dist/core/run-analyze.d.ts +87 -0
  115. package/dist/core/run-analyze.js +280 -25
  116. package/dist/core/tree-sitter/parser-loader.js +5 -4
  117. package/dist/core/tree-sitter/vendored-grammars.d.ts +39 -0
  118. package/dist/core/tree-sitter/vendored-grammars.js +57 -0
  119. package/dist/mcp/core/embedder.js +4 -0
  120. package/dist/mcp/local/local-backend.d.ts +86 -2
  121. package/dist/mcp/local/local-backend.js +657 -27
  122. package/dist/mcp/resources.js +1 -0
  123. package/dist/mcp/tools.d.ts +9 -0
  124. package/dist/mcp/tools.js +109 -0
  125. package/dist/server/analyze-launch.d.ts +29 -0
  126. package/dist/server/analyze-launch.js +137 -0
  127. package/dist/server/analyze-upload.d.ts +33 -0
  128. package/dist/server/analyze-upload.js +123 -0
  129. package/dist/server/analyze-worker-ipc.d.ts +58 -0
  130. package/dist/server/analyze-worker-ipc.js +16 -0
  131. package/dist/server/analyze-worker.d.ts +17 -1
  132. package/dist/server/analyze-worker.js +7 -1
  133. package/dist/server/api.js +46 -140
  134. package/dist/server/git-clone.d.ts +1 -0
  135. package/dist/server/git-clone.js +1 -1
  136. package/dist/server/middleware.d.ts +11 -0
  137. package/dist/server/middleware.js +27 -0
  138. package/dist/server/upload-ingest.d.ts +56 -0
  139. package/dist/server/upload-ingest.js +276 -0
  140. package/dist/server/upload-paths.d.ts +31 -0
  141. package/dist/server/upload-paths.js +51 -0
  142. package/dist/server/upload-sweep.d.ts +20 -0
  143. package/dist/server/upload-sweep.js +57 -0
  144. package/dist/storage/branch-index.d.ts +52 -0
  145. package/dist/storage/branch-index.js +65 -0
  146. package/dist/storage/git.d.ts +11 -0
  147. package/dist/storage/git.js +28 -0
  148. package/dist/storage/parse-cache.d.ts +22 -1
  149. package/dist/storage/parse-cache.js +32 -10
  150. package/dist/storage/repo-manager.d.ts +122 -10
  151. package/dist/storage/repo-manager.js +162 -21
  152. package/hooks/antigravity/gitnexus-antigravity-hook.cjs +42 -8
  153. package/hooks/claude/gitnexus-hook.cjs +36 -8
  154. package/hooks/claude/hook-db-lock-probe.cjs +123 -2
  155. package/package.json +4 -2
  156. package/scripts/assert-publish-grammar-coverage.cjs +31 -0
  157. package/scripts/build-tree-sitter-grammars.cjs +16 -10
  158. package/skills/gitnexus-guide.md +11 -0
  159. package/vendor/tree-sitter-c/package.json +1 -1
  160. package/vendor/tree-sitter-dart/package.json +1 -1
  161. package/vendor/tree-sitter-kotlin/package.json +1 -1
  162. package/vendor/tree-sitter-proto/package.json +1 -1
  163. package/vendor/tree-sitter-swift/package.json +1 -1
  164. package/web/assets/{agent-CKPMqImC.js → agent-ay4LD70X.js} +1 -1
  165. package/web/assets/{architectureDiagram-UL44E2DR-D1EXI0zA.js → architectureDiagram-UL44E2DR-Dc-viYhd.js} +1 -1
  166. package/web/assets/{chunk-LCXTWHL2-Dfmux4m1.js → chunk-LCXTWHL2-4rpojOyj.js} +1 -1
  167. package/web/assets/{chunk-RG4AUYOV-CAkzcoRj.js → chunk-RG4AUYOV-BnOy944n.js} +1 -1
  168. package/web/assets/{classDiagram-KGZ6W3CR-Bv93af_b.js → classDiagram-KGZ6W3CR-Bsgpy98Q.js} +1 -1
  169. package/web/assets/{classDiagram-v2-72OJOZXJ-CLfEqHUa.js → classDiagram-v2-72OJOZXJ-D5atDGjc.js} +1 -1
  170. package/web/assets/{diagram-3NCE3AQN-BqAtKUpW.js → diagram-3NCE3AQN-Cz1OEMVi.js} +1 -1
  171. package/web/assets/{diagram-GF46GFSD-CaBG6n6o.js → diagram-GF46GFSD-CWYwzfP8.js} +1 -1
  172. package/web/assets/{diagram-QXG6HAR7-CZ-O3rcV.js → diagram-QXG6HAR7-DtBObr8L.js} +1 -1
  173. package/web/assets/{diagram-WEQXMOUZ-P4lSL4GH.js → diagram-WEQXMOUZ-BUV44Ov_.js} +1 -1
  174. package/web/assets/{erDiagram-L5TCEMPS-QN2eEP1e.js → erDiagram-L5TCEMPS-Ds9s-sRF.js} +1 -1
  175. package/web/assets/{flowDiagram-H6V6AXG4-CXbXImlN.js → flowDiagram-H6V6AXG4-CupStHQb.js} +1 -1
  176. package/web/assets/index-COMMmbxW.css +2 -0
  177. package/web/assets/{index-CG6q8eTs.js → index-Do8AE5yF.js} +85 -85
  178. package/web/assets/{infoDiagram-3YFTVSEB-DmLICZx1.js → infoDiagram-3YFTVSEB-DPTXSbgu.js} +1 -1
  179. package/web/assets/{ishikawaDiagram-BNXS4ZKH-6LKOvBfp.js → ishikawaDiagram-BNXS4ZKH-CeK-Qhet.js} +1 -1
  180. package/web/assets/{kanban-definition-75IXJCU3-DgDi9oJT.js → kanban-definition-75IXJCU3-D4tx6QQ-.js} +1 -1
  181. package/web/assets/{mindmap-definition-2TDM6QVE-CwR5sBB-.js → mindmap-definition-2TDM6QVE-Bcto_kGa.js} +1 -1
  182. package/web/assets/{pieDiagram-CU6KROY3-By8g6f6B.js → pieDiagram-CU6KROY3-DPIeBTG6.js} +1 -1
  183. package/web/assets/{requirementDiagram-JXO7QTGE-7oDcJ1_J.js → requirementDiagram-JXO7QTGE-DDGXjVKO.js} +1 -1
  184. package/web/assets/{sequenceDiagram-VS2MUI6T-FySeKCUy.js → sequenceDiagram-VS2MUI6T-BzDqpcVW.js} +1 -1
  185. package/web/assets/{stateDiagram-7D4R322I-CB2nABwH.js → stateDiagram-7D4R322I-Bx__als3.js} +1 -1
  186. package/web/assets/{stateDiagram-v2-36443NZ5-COBGd2RL.js → stateDiagram-v2-36443NZ5-QqOh2yO-.js} +1 -1
  187. package/web/assets/{timeline-definition-O6YCAMPW-Ds2CnVZK.js → timeline-definition-O6YCAMPW-C1eogTOG.js} +1 -1
  188. package/web/assets/{vennDiagram-MWXL3ELB-DUIEwXWp.js → vennDiagram-MWXL3ELB-D20F4rSW.js} +1 -1
  189. package/web/assets/{wardleyDiagram-CUQ6CDDI-DEiFPQih.js → wardleyDiagram-CUQ6CDDI-BeBHg7ST.js} +1 -1
  190. package/web/assets/{xychartDiagram-N2JHSOCM-BUbayhST.js → xychartDiagram-N2JHSOCM-CdOWeoNI.js} +1 -1
  191. package/web/index.html +2 -2
  192. package/scripts/materialize-vendor-grammars.cjs +0 -97
  193. package/web/assets/index-BKWA-m7o.css +0 -2
@@ -0,0 +1,66 @@
1
+ /**
2
+ * TS/JS CfgVisitor (issue #2081, M1).
3
+ *
4
+ * Walks a TypeScript/JavaScript function's tree-sitter AST and drives the
5
+ * language-agnostic {@link CfgBuilder} to produce a serializable
6
+ * {@link FunctionCfg}. TS and JS share a grammar family (tree-sitter-typescript
7
+ * reuses tree-sitter-javascript's statement nodes), so one visitor covers both.
8
+ *
9
+ * Design — a `visit_<node_type>` dispatch over the statement taxonomy. The
10
+ * classic CFG hazards (R10) are handled explicitly:
11
+ * - loops allocate a dedicated **loop-exit** block so `break` has a concrete
12
+ * target before the loop's successor is known; `continue` targets the
13
+ * header/increment; the back-edge closes the loop.
14
+ * - `switch` cases fall through naturally: a case body that does not `break`
15
+ * yields non-empty `exits`, which we wire to the next case as `fallthrough`;
16
+ * a case that `break`s wires to the switch exit (via {@link ControlFlowContext})
17
+ * and yields no fall-out.
18
+ * - `try/catch/finally` routes both normal completion AND a `throw` in the try
19
+ * through `finally` (the finally block post-dominates the try/catch); a
20
+ * `throw` with no catch propagates through finally to the enclosing handler.
21
+ * - EARLY EXITS THROUGH FINALLY (#2082 M2 U2, closes the M1 soundness gap): a
22
+ * `break`/`continue`/`return` whose jump CROSSES a `finally` is re-routed to
23
+ * the finally entry (keeping its bare jump kind), and the finally's exits
24
+ * gain a `finally-return`/`finally-break`/`finally-continue` completion edge
25
+ * to the resumed target. Threading is TARGET-RELATIVE via finalizer frames
26
+ * interleaved on the {@link ControlFlowContext} stack: only the finallys
27
+ * lexically between the jump and its target thread (a `break` whose loop is
28
+ * wholly inside the try keeps its direct edge — re-routing it would let a
29
+ * finally redefinition falsely kill in-loop defs for reaching-defs). Nested
30
+ * finallys chain inner→outer; finally-as-shared-join conflates exit paths
31
+ * (sound over-approximation; duplication-per-exit-path was rejected). An
32
+ * empty/comment-only finally pushes no frame — jumps keep direct edges.
33
+ * - labeled `break`/`continue` resolve against the labeled construct's frame:
34
+ * loops/switches carry their full label LIST (`outer: inner: for` resolves
35
+ * both), and a labeled NON-loop statement (`blk: { … break blk; … }`) gets
36
+ * a break-target frame whose target is a synthesized join after the body —
37
+ * the M1 route-to-EXIT fallback removed the real continuation and falsely
38
+ * killed defs for reaching-defs (tri-review P1).
39
+ *
40
+ * Known limitations:
41
+ * - A jump whose label STILL fails to resolve (malformed source) keeps the
42
+ * conservative route-to-EXIT + thread-all-finallys fallback in
43
+ * visitBreak/visitContinue — single-exit preserved, no finally bypassed,
44
+ * but the continuation path is approximate.
45
+ * - Exceptional flow stays the sound over-approximation: EVERY protected-region
46
+ * block edges to the handler (an exception may fire mid-block), which
47
+ * over-supplies reaching-defs facts into `catch` — extra facts, never false
48
+ * kills. Per-leader throw precision is deliberately deferred (M3 decides).
49
+ * - Def/use harvest scope (#2082 M2, see typescript-harvest.ts for the full
50
+ * v1 semantics table): member/property writes are not scalar defs; nested
51
+ * function bodies are opaque in BOTH directions (writes to and reads of
52
+ * captured outer variables are invisible — callback flows are M4 territory);
53
+ * `case x:` test uses attach to the switch dispatch block (sound
54
+ * over-approximation of in-order case evaluation).
55
+ *
56
+ * Block/edge accounting and reachability are pinned in
57
+ * `test/unit/cfg/cfg-builder.test.ts` (core) and
58
+ * `test/unit/cfg/typescript-visitor.test.ts` (this visitor, per hazard).
59
+ */
60
+ import type { SyntaxNode } from '../../utils/ast-helpers.js';
61
+ import type { CfgVisitor } from '../types.js';
62
+ /** TS/JS node types that own a CFG-bearing function body. */
63
+ declare const TS_FUNCTION_TYPES: Set<string>;
64
+ /** The TS/JS CFG visitor (shared by TypeScript and JavaScript). */
65
+ export declare function createTypeScriptCfgVisitor(): CfgVisitor<SyntaxNode>;
66
+ export { TS_FUNCTION_TYPES };
@@ -0,0 +1,584 @@
1
+ import { CfgBuilder } from '../cfg-builder.js';
2
+ import { ControlFlowContext, drainFinalizerPending, wireJumpThroughFinalizers, } from '../control-flow-context.js';
3
+ import { TsHarvester } from './typescript-harvest.js';
4
+ /** TS/JS node types that own a CFG-bearing function body. */
5
+ const TS_FUNCTION_TYPES = new Set([
6
+ 'function_declaration',
7
+ 'function_expression',
8
+ 'arrow_function',
9
+ 'method_definition',
10
+ 'generator_function_declaration',
11
+ 'generator_function',
12
+ 'async_function_declaration',
13
+ 'async_arrow_function',
14
+ ]);
15
+ /** Statement node types that break a basic block (everything else coalesces). */
16
+ const CONTROL_FLOW_TYPES = new Set([
17
+ 'if_statement',
18
+ 'while_statement',
19
+ 'do_statement',
20
+ 'for_statement',
21
+ 'for_in_statement',
22
+ 'for_of_statement',
23
+ 'switch_statement',
24
+ 'try_statement',
25
+ 'return_statement',
26
+ 'break_statement',
27
+ 'continue_statement',
28
+ 'throw_statement',
29
+ 'labeled_statement',
30
+ 'statement_block',
31
+ ]);
32
+ const LOOP_OR_SWITCH_TYPES = new Set([
33
+ 'while_statement',
34
+ 'do_statement',
35
+ 'for_statement',
36
+ 'for_in_statement',
37
+ 'for_of_statement',
38
+ 'switch_statement',
39
+ ]);
40
+ const startLineOf = (n) => n.startPosition.row + 1;
41
+ const endLineOf = (n) => n.endPosition.row + 1;
42
+ /**
43
+ * Per-function walk state. One instance is created per function so the
44
+ * {@link ControlFlowContext}, exception-handler stack, and pending label are
45
+ * scoped to that function and never leak across functions.
46
+ */
47
+ class TsCfgWalk {
48
+ builder;
49
+ harvest;
50
+ cfc = new ControlFlowContext();
51
+ /** Stack of exception-handler entry blocks (catch/finally) a `throw` jumps to. */
52
+ handlers = [];
53
+ /** Labels awaiting the construct they precede (`outer: inner: for` = both). */
54
+ pendingLabels = [];
55
+ constructor(builder,
56
+ /** Def/use fact extractor (#2082 M2 U1) — phase-2 only; its scope tree is
57
+ * already complete, so any walk order resolves names correctly. */
58
+ harvest) {
59
+ this.builder = builder;
60
+ this.harvest = harvest;
61
+ }
62
+ /** Statements of a block node, ignoring comments. */
63
+ statementsOf(block) {
64
+ return block.namedChildren.filter((c) => c.type !== 'comment');
65
+ }
66
+ /** The `body` block of a node (field, or the first statement_block child). */
67
+ bodyBlockOf(node) {
68
+ return (node.childForFieldName('body') ?? node.namedChildren.find((c) => c.type === 'statement_block'));
69
+ }
70
+ /** Visit a body that may be a `statement_block` or a single statement. */
71
+ visitBody(node) {
72
+ if (!node)
73
+ return null;
74
+ if (node.type === 'statement_block')
75
+ return this.visitSeq(this.statementsOf(node));
76
+ return this.visitStmt(node);
77
+ }
78
+ /** Wire a sequence of statements, coalescing straight-line runs into blocks. */
79
+ visitSeq(stmts) {
80
+ let entry;
81
+ let dangling = [];
82
+ let openSimple;
83
+ for (const stmt of stmts) {
84
+ if (CONTROL_FLOW_TYPES.has(stmt.type)) {
85
+ openSimple = undefined; // close any open straight-line block
86
+ const res = this.visitStmt(stmt);
87
+ if (res === null)
88
+ continue; // transparent (empty nested block)
89
+ if (entry === undefined)
90
+ entry = res.entry;
91
+ else
92
+ this.builder.connect(dangling, res.entry, 'seq');
93
+ dangling = [...res.exits];
94
+ }
95
+ else {
96
+ // Simple statement — coalesce into the current straight-line block.
97
+ if (openSimple === undefined) {
98
+ const idx = this.builder.newBlock(startLineOf(stmt), endLineOf(stmt), stmt.text, 'normal', this.harvest.facts(stmt));
99
+ if (entry === undefined)
100
+ entry = idx;
101
+ else
102
+ this.builder.connect(dangling, idx, 'seq');
103
+ openSimple = idx;
104
+ dangling = [idx];
105
+ }
106
+ else {
107
+ this.builder.extendBlock(openSimple, endLineOf(stmt), stmt.text, this.harvest.facts(stmt));
108
+ }
109
+ }
110
+ }
111
+ if (entry === undefined)
112
+ return null;
113
+ return { entry, exits: dangling };
114
+ }
115
+ /** Dispatch one statement to its handler. Non-null except for empty blocks. */
116
+ visitStmt(stmt) {
117
+ switch (stmt.type) {
118
+ case 'if_statement':
119
+ return this.visitIf(stmt);
120
+ case 'while_statement':
121
+ return this.visitWhile(stmt);
122
+ case 'do_statement':
123
+ return this.visitDoWhile(stmt);
124
+ case 'for_statement':
125
+ return this.visitFor(stmt);
126
+ case 'for_in_statement':
127
+ case 'for_of_statement':
128
+ return this.visitForIn(stmt);
129
+ case 'switch_statement':
130
+ return this.visitSwitch(stmt);
131
+ case 'try_statement':
132
+ return this.visitTry(stmt);
133
+ case 'return_statement':
134
+ return this.visitReturn(stmt);
135
+ case 'throw_statement':
136
+ return this.visitThrow(stmt);
137
+ case 'break_statement':
138
+ return this.visitBreak(stmt);
139
+ case 'continue_statement':
140
+ return this.visitContinue(stmt);
141
+ case 'labeled_statement':
142
+ return this.visitLabeled(stmt);
143
+ case 'statement_block':
144
+ return this.visitSeq(this.statementsOf(stmt));
145
+ default:
146
+ return this.visitSimple(stmt);
147
+ }
148
+ }
149
+ visitSimple(stmt) {
150
+ const idx = this.builder.newBlock(startLineOf(stmt), endLineOf(stmt), stmt.text, 'normal', this.harvest.facts(stmt));
151
+ return { entry: idx, exits: [idx] };
152
+ }
153
+ visitReturn(stmt) {
154
+ // Harvest the argument expression's uses — `return x` blocks live in this
155
+ // dedicated handler, not visitSeq, and were a silently-missed site once.
156
+ const idx = this.builder.newBlock(startLineOf(stmt), endLineOf(stmt), stmt.text, 'normal', this.harvest.facts(stmt));
157
+ // A return crosses EVERY active finally before reaching EXIT.
158
+ wireJumpThroughFinalizers(this.builder, idx, this.cfc.finalizersForReturn(), this.builder.exitIndex, 'return');
159
+ return { entry: idx, exits: [] };
160
+ }
161
+ visitThrow(stmt) {
162
+ const idx = this.builder.newBlock(startLineOf(stmt), endLineOf(stmt), stmt.text, 'normal', this.harvest.facts(stmt));
163
+ this.builder.edge(idx, this.currentHandler(), 'throw');
164
+ return { entry: idx, exits: [] };
165
+ }
166
+ visitBreak(stmt) {
167
+ const idx = this.builder.newBlock(startLineOf(stmt), endLineOf(stmt), stmt.text);
168
+ const res = this.cfc.resolveBreak(this.labelOf(stmt));
169
+ // An unresolved target — a label this visitor doesn't model (a stacked
170
+ // outer label like `outer: inner: for`, or a labeled non-loop block) —
171
+ // would otherwise leave this block with NO out-edge, stranding it and
172
+ // breaking the single-exit invariant a downstream post-dominator / PDG pass
173
+ // relies on. Conservatively route an unresolved jump to the function EXIT
174
+ // ("escapes the function") and thread ALL active finallys — a superset of
175
+ // the truly-crossed set (the real target is somewhere in the function, so
176
+ // execution provably runs every finally between the jump and wherever it
177
+ // lands... up to the ones the conservative EXIT routing over-includes).
178
+ // Sound for dataflow either way: extra paths, never a bypassed finally.
179
+ const { target, finalizers } = res ?? {
180
+ target: this.builder.exitIndex,
181
+ finalizers: this.cfc.finalizersForReturn(),
182
+ };
183
+ wireJumpThroughFinalizers(this.builder, idx, finalizers, target, 'break');
184
+ return { entry: idx, exits: [] };
185
+ }
186
+ visitContinue(stmt) {
187
+ const idx = this.builder.newBlock(startLineOf(stmt), endLineOf(stmt), stmt.text);
188
+ const res = this.cfc.resolveContinue(this.labelOf(stmt));
189
+ // See visitBreak: an unresolved label routes to EXIT (threading all
190
+ // active finallys) to preserve single-exit without bypassing a finally.
191
+ const { target, finalizers } = res ?? {
192
+ target: this.builder.exitIndex,
193
+ finalizers: this.cfc.finalizersForReturn(),
194
+ };
195
+ wireJumpThroughFinalizers(this.builder, idx, finalizers, target, 'continue');
196
+ return { entry: idx, exits: [] };
197
+ }
198
+ visitLabeled(stmt) {
199
+ const body = stmt.childForFieldName('body') ?? stmt.namedChildren[stmt.namedChildren.length - 1];
200
+ const label = this.labelOf(stmt);
201
+ if (body && (LOOP_OR_SWITCH_TYPES.has(body.type) || body.type === 'labeled_statement')) {
202
+ // Loop/switch consumes the accumulated labels via takeLabels(); a nested
203
+ // labeled_statement keeps accumulating (`outer: inner: for` → both
204
+ // labels land on the loop frame).
205
+ if (label)
206
+ this.pendingLabels.push(label);
207
+ const res = this.visitStmt(body);
208
+ this.pendingLabels = []; // clear leftovers if the construct didn't consume
209
+ return res;
210
+ }
211
+ // Labeled NON-loop statement (`blk: { … break blk; … }`): break-to-label
212
+ // targets a synthesized join after the body. Routing it to EXIT instead
213
+ // (the M1 behavior) removed the real continuation and falsely killed
214
+ // every def live at the jump for post-construct uses (tri-review P1).
215
+ const labels = [...this.pendingLabels, ...(label ? [label] : [])];
216
+ this.pendingLabels = [];
217
+ const join = this.builder.newBlock(endLineOf(stmt), endLineOf(stmt), '');
218
+ this.cfc.pushLabeledBlock(join, labels);
219
+ const res = this.visitBody(body);
220
+ this.cfc.pop();
221
+ if (res)
222
+ this.builder.connect(res.exits, join, 'seq');
223
+ return { entry: res?.entry ?? join, exits: [join] };
224
+ }
225
+ visitIf(stmt) {
226
+ const cond = stmt.childForFieldName('condition') ?? stmt;
227
+ const condBlock = this.builder.newBlock(startLineOf(stmt), endLineOf(cond), cond.text, 'normal', this.harvest.facts(cond));
228
+ const exits = [];
229
+ const thenRes = this.visitBody(stmt.childForFieldName('consequence'));
230
+ if (thenRes) {
231
+ this.builder.edge(condBlock, thenRes.entry, 'cond-true');
232
+ exits.push(...thenRes.exits);
233
+ }
234
+ else {
235
+ exits.push(condBlock); // empty then — true path falls through
236
+ }
237
+ const elseNode = this.elseBodyOf(stmt);
238
+ if (elseNode) {
239
+ const elseRes = this.visitBody(elseNode);
240
+ if (elseRes) {
241
+ this.builder.edge(condBlock, elseRes.entry, 'cond-false');
242
+ exits.push(...elseRes.exits);
243
+ }
244
+ else {
245
+ exits.push(condBlock); // empty else block
246
+ }
247
+ }
248
+ else {
249
+ exits.push(condBlock); // no else — false path falls through to the join
250
+ }
251
+ return { entry: condBlock, exits: [...new Set(exits)] };
252
+ }
253
+ /** The else body node (unwraps an `else_clause` wrapper if present). */
254
+ elseBodyOf(ifStmt) {
255
+ const alt = ifStmt.childForFieldName('alternative');
256
+ if (!alt)
257
+ return undefined;
258
+ if (alt.type === 'else_clause') {
259
+ return alt.childForFieldName('body') ?? alt.namedChildren[0];
260
+ }
261
+ return alt;
262
+ }
263
+ visitWhile(stmt) {
264
+ const labels = this.takeLabels();
265
+ const cond = stmt.childForFieldName('condition') ?? stmt;
266
+ const header = this.builder.newBlock(startLineOf(stmt), endLineOf(cond), cond.text, 'normal', this.harvest.facts(cond));
267
+ const loopExit = this.builder.newBlock(endLineOf(stmt), endLineOf(stmt), '');
268
+ this.cfc.pushLoop(header, loopExit, labels);
269
+ const body = this.visitBody(this.bodyBlockOf(stmt));
270
+ this.cfc.pop();
271
+ if (body) {
272
+ this.builder.edge(header, body.entry, 'cond-true');
273
+ this.builder.connect(body.exits, header, 'loop-back');
274
+ }
275
+ else {
276
+ this.builder.edge(header, header, 'loop-back'); // empty body re-tests
277
+ }
278
+ this.builder.edge(header, loopExit, 'cond-false');
279
+ return { entry: header, exits: [loopExit] };
280
+ }
281
+ visitDoWhile(stmt) {
282
+ const labels = this.takeLabels();
283
+ const cond = stmt.childForFieldName('condition') ?? stmt;
284
+ const condBlock = this.builder.newBlock(startLineOf(cond), endLineOf(cond), cond.text, 'normal', this.harvest.facts(cond));
285
+ const loopExit = this.builder.newBlock(endLineOf(stmt), endLineOf(stmt), '');
286
+ this.cfc.pushLoop(condBlock, loopExit, labels);
287
+ const body = this.visitBody(this.bodyBlockOf(stmt));
288
+ this.cfc.pop();
289
+ const backTarget = body ? body.entry : condBlock;
290
+ if (body)
291
+ this.builder.connect(body.exits, condBlock, 'seq');
292
+ this.builder.edge(condBlock, backTarget, 'loop-back'); // cond true → run body again
293
+ this.builder.edge(condBlock, loopExit, 'cond-false');
294
+ return { entry: backTarget, exits: [loopExit] };
295
+ }
296
+ visitFor(stmt) {
297
+ const labels = this.takeLabels();
298
+ const init = stmt.childForFieldName('initializer');
299
+ const cond = stmt.childForFieldName('condition');
300
+ const incr = stmt.childForFieldName('increment');
301
+ const header = this.builder.newBlock(startLineOf(stmt), cond ? endLineOf(cond) : startLineOf(stmt), cond ? cond.text : 'for(;;)', 'normal', cond ? this.harvest.facts(cond) : undefined);
302
+ const loopExit = this.builder.newBlock(endLineOf(stmt), endLineOf(stmt), '');
303
+ let incrBlock = header;
304
+ if (incr) {
305
+ incrBlock = this.builder.newBlock(startLineOf(incr), endLineOf(incr), incr.text, 'normal', this.harvest.facts(incr));
306
+ this.builder.edge(incrBlock, header, 'loop-back');
307
+ }
308
+ this.cfc.pushLoop(incrBlock, loopExit, labels);
309
+ const body = this.visitBody(this.bodyBlockOf(stmt));
310
+ this.cfc.pop();
311
+ if (body) {
312
+ this.builder.edge(header, body.entry, 'cond-true');
313
+ // With no increment clause the body's exits ARE the back-edge — carry
314
+ // the loop-back kind on them (mirroring visitWhile/visitForIn) instead
315
+ // of a phantom header→header self-loop that models a path which never
316
+ // executes the body. With an increment, the body falls through to the
317
+ // increment (`seq`) and the increment carries the loop-back (:338).
318
+ this.builder.connect(body.exits, incrBlock, incr ? 'seq' : 'loop-back');
319
+ }
320
+ else {
321
+ this.builder.edge(header, incrBlock, 'cond-true');
322
+ // Empty body with no increment: the header genuinely re-tests itself.
323
+ if (!incr)
324
+ this.builder.edge(header, header, 'loop-back');
325
+ }
326
+ this.builder.edge(header, loopExit, 'cond-false');
327
+ let entry = header;
328
+ if (init) {
329
+ const initBlock = this.builder.newBlock(startLineOf(init), endLineOf(init), init.text, 'normal', this.harvest.facts(init));
330
+ this.builder.edge(initBlock, header, 'seq');
331
+ entry = initBlock;
332
+ }
333
+ return { entry, exits: [loopExit] };
334
+ }
335
+ visitForIn(stmt) {
336
+ const labels = this.takeLabels();
337
+ // Header text is SYNTHESIZED, so facts come from the left/right AST nodes
338
+ // directly (the loop variable is a def, the iterated expression a use).
339
+ const header = this.builder.newBlock(startLineOf(stmt), startLineOf(stmt), this.forInHeaderText(stmt), 'normal', this.harvest.forInHeadFacts(stmt));
340
+ const loopExit = this.builder.newBlock(endLineOf(stmt), endLineOf(stmt), '');
341
+ this.cfc.pushLoop(header, loopExit, labels);
342
+ const body = this.visitBody(this.bodyBlockOf(stmt));
343
+ this.cfc.pop();
344
+ if (body) {
345
+ this.builder.edge(header, body.entry, 'cond-true');
346
+ this.builder.connect(body.exits, header, 'loop-back');
347
+ }
348
+ else {
349
+ this.builder.edge(header, header, 'loop-back');
350
+ }
351
+ this.builder.edge(header, loopExit, 'cond-false');
352
+ return { entry: header, exits: [loopExit] };
353
+ }
354
+ forInHeaderText(stmt) {
355
+ const left = stmt.childForFieldName('left')?.text ?? '';
356
+ const right = stmt.childForFieldName('right')?.text ?? '';
357
+ return left || right ? `for(${left} … ${right})` : 'for(… in/of …)';
358
+ }
359
+ visitSwitch(stmt) {
360
+ const labels = this.takeLabels();
361
+ const value = stmt.childForFieldName('value') ?? stmt;
362
+ const dispatch = this.builder.newBlock(startLineOf(stmt), endLineOf(value), value.text, 'normal', this.harvest.facts(value));
363
+ const switchExit = this.builder.newBlock(endLineOf(stmt), endLineOf(stmt), '');
364
+ this.cfc.pushSwitch(switchExit, labels);
365
+ const body = stmt.childForFieldName('body');
366
+ const cases = body
367
+ ? body.namedChildren.filter((c) => c.type === 'switch_case' || c.type === 'switch_default')
368
+ : [];
369
+ // `case x:` test expressions live in no block (caseStatements filters the
370
+ // value node out) — harvest their uses onto the dispatch block, one record
371
+ // per case in source order (a sound over-approximation of JS's in-order
372
+ // case evaluation). Conditionally: a later case test only evaluates when
373
+ // earlier cases didn't match, so any def inside one is a may-def — as a
374
+ // must-def on the always-executed dispatch block it would falsely kill
375
+ // prior defs for earlier-matching arms (tri-review).
376
+ for (const c of cases) {
377
+ const caseValue = c.childForFieldName('value');
378
+ if (caseValue)
379
+ this.builder.attachFacts(dispatch, this.harvest.factsConditional(caseValue));
380
+ }
381
+ const caseResults = cases.map((c) => this.visitSeq(this.caseStatements(c)));
382
+ const hasDefault = cases.some((c) => c.type === 'switch_default');
383
+ // entryOf[i] = block a dispatch/fallthrough INTO case i lands on (empty
384
+ // cases are transparent — they resolve to the next case, or the exit).
385
+ const entryOf = new Array(cases.length);
386
+ let after = switchExit;
387
+ for (let i = cases.length - 1; i >= 0; i--) {
388
+ entryOf[i] = caseResults[i]?.entry ?? after;
389
+ after = entryOf[i];
390
+ }
391
+ for (let i = 0; i < cases.length; i++) {
392
+ this.builder.edge(dispatch, entryOf[i], 'switch-case');
393
+ }
394
+ if (!hasDefault)
395
+ this.builder.edge(dispatch, switchExit, 'switch-case'); // no-match path
396
+ for (let i = 0; i < cases.length; i++) {
397
+ const res = caseResults[i];
398
+ if (!res)
399
+ continue;
400
+ const fallTarget = i + 1 < cases.length ? entryOf[i + 1] : switchExit;
401
+ this.builder.connect(res.exits, fallTarget, 'fallthrough');
402
+ }
403
+ this.cfc.pop();
404
+ return { entry: dispatch, exits: [switchExit] };
405
+ }
406
+ caseStatements(caseNode) {
407
+ const value = caseNode.childForFieldName('value');
408
+ return caseNode.namedChildren.filter((c) => c.id !== value?.id && c.type !== 'comment');
409
+ }
410
+ visitTry(stmt) {
411
+ const bodyNode = stmt.childForFieldName('body');
412
+ // Single pass over named children — tree-sitter's `namedChildren` getter
413
+ // allocates a fresh array on every access, so avoid the double `.find`.
414
+ let catchClause;
415
+ let finallyClause;
416
+ for (let i = 0; i < stmt.namedChildCount; i++) {
417
+ const c = stmt.namedChild(i);
418
+ if (c?.type === 'catch_clause')
419
+ catchClause = c;
420
+ else if (c?.type === 'finally_clause')
421
+ finallyClause = c;
422
+ }
423
+ // Build finally first so its entry is known as both a normal join and a
424
+ // handler target. The finally body runs in the OUTER handler context — and
425
+ // OUTSIDE this try's finalizer frame: a return inside the finally must not
426
+ // thread itself (it threads only outer finallys, matching JS semantics).
427
+ const finallyRes = finallyClause
428
+ ? this.visitSeq(this.statementsOf(this.bodyBlockOf(finallyClause)))
429
+ : null;
430
+ // Finalizer frame for early-exit threading (#2082 M2 U2): active while the
431
+ // catch and protected bodies are walked, so a crossing `return`/`break`/
432
+ // `continue` inside either routes through the finally. An empty/comment-only
433
+ // finally (`finallyRes` null — the #2099-F2 empty-catch bug shape) pushes
434
+ // NO frame: it can define nothing, so jumps soundly keep direct edges.
435
+ const finFrame = finallyRes ? this.cfc.pushFinalizer(finallyRes.entry) : null;
436
+ // A throw inside catch propagates to finally (if any), else the outer handler.
437
+ let catchRes = null;
438
+ if (catchClause) {
439
+ if (finallyRes)
440
+ this.handlers.push(finallyRes.entry);
441
+ catchRes = this.visitSeq(this.statementsOf(this.bodyBlockOf(catchClause)));
442
+ if (finallyRes)
443
+ this.handlers.pop();
444
+ if (catchRes === null) {
445
+ // Empty (or comment-only) catch body — `catch {}`. The clause still
446
+ // CATCHES: handler semantics key off the syntactic clause, not the
447
+ // traversal result. Treating it as "no catch" sent the swallowed
448
+ // exception to the outer handler/EXIT and left post-try code
449
+ // unreachable when the body always throws — a hard false-negative
450
+ // for downstream taint. Synthesize one empty block spanning the
451
+ // clause (entry == sole exit) so exception flow lands in it and
452
+ // rejoins the normal continuation. Created BEFORE the protected
453
+ // region is walked, so it never receives a spurious throw edge.
454
+ const idx = this.builder.newBlock(startLineOf(catchClause), endLineOf(catchClause), '');
455
+ catchRes = { entry: idx, exits: [idx] };
456
+ }
457
+ // `catch (e)` has no header block — the param def gets its OWN
458
+ // facts-only block in front of the body entry. It must NOT be prepended
459
+ // into the body's entry block: when the catch body STARTS with a loop,
460
+ // that entry is the loop HEADER, re-entered on every iteration — the
461
+ // param def would re-gen there and falsely KILL loop-carried
462
+ // redefinitions of the param (`catch (e) { while (c) { e = fix(e); }
463
+ // sink(e); }` would lose the fix→sink fact, a taint false negative).
464
+ // The param block becomes the handler entry, which is also semantically
465
+ // right: the binding happens exactly once, on handler entry.
466
+ const paramFacts = this.harvest.catchParamFacts(catchClause);
467
+ if (paramFacts) {
468
+ const paramBlock = this.builder.newBlock(startLineOf(catchClause), startLineOf(catchClause), '', 'normal', paramFacts);
469
+ this.builder.edge(paramBlock, catchRes.entry, 'seq');
470
+ catchRes = { entry: paramBlock, exits: catchRes.exits };
471
+ }
472
+ }
473
+ // Handler for the try body: catch if present, else finally, else outer.
474
+ const tryHandler = catchRes?.entry ?? finallyRes?.entry ?? this.currentHandler();
475
+ const protectedStart = this.builder.blockCount;
476
+ this.handlers.push(tryHandler);
477
+ const bodyRes = bodyNode ? this.visitSeq(this.statementsOf(bodyNode)) : null;
478
+ this.handlers.pop();
479
+ // Conservative exceptional edges: ANY block in the protected region may raise
480
+ // to the handler — not just an explicit `throw`, and not just the body ENTRY.
481
+ // Edging every block created during the try-body walk keeps exception flow
482
+ // sound when the body BRANCHES: an `if` / nested-try / post-branch block whose
483
+ // interior blocks would otherwise have no path to the handler — i.e. a taint
484
+ // false-negative into `catch` for the downstream PDG analysis. The
485
+ // per-function edge cap bounds the count; explicit `throw`s add their own
486
+ // (idempotent) edge to the same handler.
487
+ if (catchClause || finallyClause) {
488
+ for (let b = protectedStart; b < this.builder.blockCount; b++) {
489
+ this.builder.edge(b, tryHandler, 'throw');
490
+ }
491
+ }
492
+ // The finalizer frame closes once the protected/catch walks are done; any
493
+ // jumps that crossed it left their completion legs on `pending`, wired
494
+ // here from the finally's exits (see drainFinalizerPending for the
495
+ // finally-override semantics of an always-jumping finally).
496
+ if (finFrame && finallyRes) {
497
+ this.cfc.pop();
498
+ drainFinalizerPending(this.builder, finFrame, finallyRes.exits);
499
+ }
500
+ const exits = [];
501
+ if (finallyRes) {
502
+ // Normal completion of try AND catch both flow through finally.
503
+ if (bodyRes)
504
+ this.builder.connect(bodyRes.exits, finallyRes.entry, 'seq');
505
+ if (catchRes)
506
+ this.builder.connect(catchRes.exits, finallyRes.entry, 'seq');
507
+ exits.push(...finallyRes.exits);
508
+ // No catch → an exception re-propagates out after finally runs.
509
+ if (!catchRes)
510
+ this.builder.connect(finallyRes.exits, this.currentHandler(), 'throw');
511
+ }
512
+ else {
513
+ if (bodyRes)
514
+ exits.push(...bodyRes.exits);
515
+ if (catchRes)
516
+ exits.push(...catchRes.exits);
517
+ }
518
+ const entry = bodyRes?.entry ?? finallyRes?.entry ?? catchRes?.entry;
519
+ if (entry === undefined)
520
+ return null;
521
+ return { entry, exits: [...new Set(exits)] };
522
+ }
523
+ /** Nearest enclosing exception handler, or the function EXIT. */
524
+ currentHandler() {
525
+ return this.handlers.length ? this.handlers[this.handlers.length - 1] : this.builder.exitIndex;
526
+ }
527
+ /** Consume the labels awaiting the loop/switch this call is building. */
528
+ takeLabels() {
529
+ const labels = this.pendingLabels;
530
+ this.pendingLabels = [];
531
+ return labels;
532
+ }
533
+ labelOf(stmt) {
534
+ const id = stmt.childForFieldName('label') ??
535
+ stmt.namedChildren.find((c) => c.type === 'statement_identifier');
536
+ return id?.text;
537
+ }
538
+ }
539
+ /** Build the CFG for one TS/JS function node (or `undefined` if not a function). */
540
+ function buildFunctionCfg(fnNode, filePath) {
541
+ if (!TS_FUNCTION_TYPES.has(fnNode.type))
542
+ return undefined;
543
+ const startLine = startLineOf(fnNode);
544
+ const endLine = endLineOf(fnNode);
545
+ const startColumn = fnNode.startPosition.column;
546
+ const builder = new CfgBuilder(filePath, startLine, endLine, startColumn);
547
+ const body = fnNode.childForFieldName('body');
548
+ if (!body)
549
+ return undefined; // overload signature / abstract method — no body
550
+ // Phase-1 declaration pre-scan (#2082 M2 U1) — must complete before any
551
+ // facts are extracted; the CFG walk below is not source-order.
552
+ const harvest = new TsHarvester(fnNode);
553
+ // Parameters define at ENTRY (facts only — never touch the entry block's
554
+ // text or span: bench fingerprints and CFG snapshots include block text).
555
+ const paramFacts = harvest.paramFacts();
556
+ if (paramFacts)
557
+ builder.attachFacts(builder.entryIndex, paramFacts);
558
+ if (body.type !== 'statement_block') {
559
+ // Expression-bodied arrow: `() => expr` — one block whose value is returned.
560
+ // Lives outside the walk class, so it harvests explicitly.
561
+ const blk = builder.newBlock(startLineOf(body), endLineOf(body), body.text, 'normal', harvest.facts(body));
562
+ builder.edge(builder.entryIndex, blk, 'seq');
563
+ builder.edge(blk, builder.exitIndex, 'return');
564
+ return builder.finish(harvest.table());
565
+ }
566
+ const walk = new TsCfgWalk(builder, harvest);
567
+ const res = walk.visitSeq(body.namedChildren.filter((c) => c.type !== 'comment'));
568
+ if (!res) {
569
+ builder.edge(builder.entryIndex, builder.exitIndex, 'seq'); // empty body
570
+ return builder.finish(harvest.table());
571
+ }
572
+ builder.edge(builder.entryIndex, res.entry, 'seq');
573
+ builder.connect(res.exits, builder.exitIndex, 'seq'); // normal fall-off → EXIT
574
+ return builder.finish(harvest.table());
575
+ }
576
+ /** Whether a node is a TS/JS function this visitor builds a CFG for. */
577
+ function isFunction(node) {
578
+ return TS_FUNCTION_TYPES.has(node.type);
579
+ }
580
+ /** The TS/JS CFG visitor (shared by TypeScript and JavaScript). */
581
+ export function createTypeScriptCfgVisitor() {
582
+ return { buildFunctionCfg, isFunction };
583
+ }
584
+ export { TS_FUNCTION_TYPES };