gitnexus 1.6.8-rc.11 → 1.6.8-rc.13

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 (54) hide show
  1. package/dist/_shared/scope-resolution/parsed-file.d.ts +21 -0
  2. package/dist/_shared/scope-resolution/parsed-file.d.ts.map +1 -1
  3. package/dist/_shared/scope-resolution/symbol-definition.d.ts +4 -0
  4. package/dist/_shared/scope-resolution/symbol-definition.d.ts.map +1 -1
  5. package/dist/cli/analyze-config.js +1 -0
  6. package/dist/cli/analyze.d.ts +6 -0
  7. package/dist/cli/analyze.js +2 -0
  8. package/dist/cli/index.js +2 -0
  9. package/dist/core/ingestion/cfg/cfg-builder.d.ts +51 -0
  10. package/dist/core/ingestion/cfg/cfg-builder.js +100 -0
  11. package/dist/core/ingestion/cfg/collect.d.ts +30 -0
  12. package/dist/core/ingestion/cfg/collect.js +34 -0
  13. package/dist/core/ingestion/cfg/control-flow-context.d.ts +27 -0
  14. package/dist/core/ingestion/cfg/control-flow-context.js +49 -0
  15. package/dist/core/ingestion/cfg/emit.d.ts +64 -0
  16. package/dist/core/ingestion/cfg/emit.js +95 -0
  17. package/dist/core/ingestion/cfg/traversal-result.d.ts +20 -0
  18. package/dist/core/ingestion/cfg/traversal-result.js +2 -0
  19. package/dist/core/ingestion/cfg/types.d.ts +66 -0
  20. package/dist/core/ingestion/cfg/types.js +13 -0
  21. package/dist/core/ingestion/cfg/visitors/typescript.d.ts +49 -0
  22. package/dist/core/ingestion/cfg/visitors/typescript.js +492 -0
  23. package/dist/core/ingestion/language-provider.d.ts +11 -0
  24. package/dist/core/ingestion/languages/cpp/arity-metadata.js +6 -2
  25. package/dist/core/ingestion/languages/cpp/captures.js +24 -1
  26. package/dist/core/ingestion/languages/cpp/query.js +23 -0
  27. package/dist/core/ingestion/languages/typescript.js +5 -0
  28. package/dist/core/ingestion/method-extractors/configs/c-cpp.js +6 -13
  29. package/dist/core/ingestion/method-extractors/generic.js +1 -0
  30. package/dist/core/ingestion/method-types.d.ts +2 -0
  31. package/dist/core/ingestion/model/symbol-table.d.ts +1 -0
  32. package/dist/core/ingestion/model/symbol-table.js +1 -0
  33. package/dist/core/ingestion/parsing-processor.js +1 -0
  34. package/dist/core/ingestion/pipeline-phases/parse-impl.js +18 -1
  35. package/dist/core/ingestion/pipeline.d.ts +23 -0
  36. package/dist/core/ingestion/scope-extractor.js +3 -0
  37. package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +12 -0
  38. package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +145 -24
  39. package/dist/core/ingestion/scope-resolution/pipeline/phase.js +3 -0
  40. package/dist/core/ingestion/scope-resolution/pipeline/reconcile-ownership.js +43 -3
  41. package/dist/core/ingestion/scope-resolution/pipeline/run.d.ts +10 -0
  42. package/dist/core/ingestion/scope-resolution/pipeline/run.js +62 -0
  43. package/dist/core/ingestion/scope-resolution/resolution-outcome.d.ts +1 -1
  44. package/dist/core/ingestion/utils/method-props.js +1 -0
  45. package/dist/core/ingestion/workers/parse-worker.d.ts +1 -0
  46. package/dist/core/ingestion/workers/parse-worker.js +43 -1
  47. package/dist/core/ingestion/workers/worker-pool.d.ts +9 -0
  48. package/dist/core/ingestion/workers/worker-pool.js +5 -2
  49. package/dist/core/run-analyze.d.ts +32 -0
  50. package/dist/core/run-analyze.js +75 -1
  51. package/dist/storage/parse-cache.d.ts +22 -1
  52. package/dist/storage/parse-cache.js +20 -10
  53. package/dist/storage/repo-manager.d.ts +31 -7
  54. package/package.json +1 -1
@@ -0,0 +1,13 @@
1
+ /**
2
+ * CFG data model — plain, JSON-serializable types (issue #2081, M1).
3
+ *
4
+ * These cross the worker→main boundary and the disk-backed/durable ParsedFile
5
+ * store, so they must contain NO tree-sitter AST references, class instances,
6
+ * or anything that does not survive `JSON.stringify` → `JSON.parse`. Block and
7
+ * edge endpoints are referenced by integer index within a function's CFG.
8
+ *
9
+ * The per-language `CfgVisitor` (built in the parse worker, where the AST
10
+ * lives — see the M1 plan KTD1/KTD7) produces a `FunctionCfg` per function; the
11
+ * array of them is what rides on `ParsedFile.cfgSideChannel`.
12
+ */
13
+ export {};
@@ -0,0 +1,49 @@
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
+ * - labeled `break`/`continue` resolve against the labeled loop's frame.
22
+ *
23
+ * Known M1 limitations:
24
+ * - SOUNDNESS GAP (M2 blocker, not mere precision): a non-local jump
25
+ * (`break`/`continue`/`return`) out of a `try` that has a `finally` edges
26
+ * directly to its target rather than routing THROUGH the `finally` block
27
+ * first. A future taint/PDG pass will therefore MISS flow mediated by a
28
+ * `finally` on the early-exit path (e.g. a value the `finally` taints or
29
+ * sanitizes before the `return` reaches its target) — a false negative. The
30
+ * general fix duplicates `finally` per exit path; deferred past M1 and
31
+ * tracked for M2. Normal completion and `throw` DO route through `finally`.
32
+ * - A `break`/`continue` to a label on a non-loop/non-switch block, and the
33
+ * OUTER label of a doubly-labeled construct (`outer: inner: for (...)`), are
34
+ * not modeled. The jump is conservatively routed to the function EXIT (a
35
+ * sound over-approximation that keeps the graph single-exit — see visitBreak)
36
+ * rather than left as a dangling sink; only the precise labeled target is
37
+ * unmodeled. Single-labeled loops/switches resolve correctly.
38
+ *
39
+ * Block/edge accounting and reachability are pinned in
40
+ * `test/unit/cfg/cfg-builder.test.ts` (core) and
41
+ * `test/unit/cfg/typescript-visitor.test.ts` (this visitor, per hazard).
42
+ */
43
+ import type { SyntaxNode } from '../../utils/ast-helpers.js';
44
+ import type { CfgVisitor } from '../types.js';
45
+ /** TS/JS node types that own a CFG-bearing function body. */
46
+ declare const TS_FUNCTION_TYPES: Set<string>;
47
+ /** The TS/JS CFG visitor (shared by TypeScript and JavaScript). */
48
+ export declare function createTypeScriptCfgVisitor(): CfgVisitor<SyntaxNode>;
49
+ export { TS_FUNCTION_TYPES };
@@ -0,0 +1,492 @@
1
+ import { CfgBuilder } from '../cfg-builder.js';
2
+ import { ControlFlowContext } from '../control-flow-context.js';
3
+ /** TS/JS node types that own a CFG-bearing function body. */
4
+ const TS_FUNCTION_TYPES = new Set([
5
+ 'function_declaration',
6
+ 'function_expression',
7
+ 'arrow_function',
8
+ 'method_definition',
9
+ 'generator_function_declaration',
10
+ 'generator_function',
11
+ 'async_function_declaration',
12
+ 'async_arrow_function',
13
+ ]);
14
+ /** Statement node types that break a basic block (everything else coalesces). */
15
+ const CONTROL_FLOW_TYPES = new Set([
16
+ 'if_statement',
17
+ 'while_statement',
18
+ 'do_statement',
19
+ 'for_statement',
20
+ 'for_in_statement',
21
+ 'for_of_statement',
22
+ 'switch_statement',
23
+ 'try_statement',
24
+ 'return_statement',
25
+ 'break_statement',
26
+ 'continue_statement',
27
+ 'throw_statement',
28
+ 'labeled_statement',
29
+ 'statement_block',
30
+ ]);
31
+ const LOOP_OR_SWITCH_TYPES = new Set([
32
+ 'while_statement',
33
+ 'do_statement',
34
+ 'for_statement',
35
+ 'for_in_statement',
36
+ 'for_of_statement',
37
+ 'switch_statement',
38
+ ]);
39
+ const startLineOf = (n) => n.startPosition.row + 1;
40
+ const endLineOf = (n) => n.endPosition.row + 1;
41
+ /**
42
+ * Per-function walk state. One instance is created per function so the
43
+ * {@link ControlFlowContext}, exception-handler stack, and pending label are
44
+ * scoped to that function and never leak across functions.
45
+ */
46
+ class TsCfgWalk {
47
+ builder;
48
+ cfc = new ControlFlowContext();
49
+ /** Stack of exception-handler entry blocks (catch/finally) a `throw` jumps to. */
50
+ handlers = [];
51
+ /** Label awaiting the loop/switch it immediately precedes (labeled_statement). */
52
+ pendingLabel;
53
+ constructor(builder) {
54
+ this.builder = builder;
55
+ }
56
+ /** Statements of a block node, ignoring comments. */
57
+ statementsOf(block) {
58
+ return block.namedChildren.filter((c) => c.type !== 'comment');
59
+ }
60
+ /** The `body` block of a node (field, or the first statement_block child). */
61
+ bodyBlockOf(node) {
62
+ return (node.childForFieldName('body') ?? node.namedChildren.find((c) => c.type === 'statement_block'));
63
+ }
64
+ /** Visit a body that may be a `statement_block` or a single statement. */
65
+ visitBody(node) {
66
+ if (!node)
67
+ return null;
68
+ if (node.type === 'statement_block')
69
+ return this.visitSeq(this.statementsOf(node));
70
+ return this.visitStmt(node);
71
+ }
72
+ /** Wire a sequence of statements, coalescing straight-line runs into blocks. */
73
+ visitSeq(stmts) {
74
+ let entry;
75
+ let dangling = [];
76
+ let openSimple;
77
+ for (const stmt of stmts) {
78
+ if (CONTROL_FLOW_TYPES.has(stmt.type)) {
79
+ openSimple = undefined; // close any open straight-line block
80
+ const res = this.visitStmt(stmt);
81
+ if (res === null)
82
+ continue; // transparent (empty nested block)
83
+ if (entry === undefined)
84
+ entry = res.entry;
85
+ else
86
+ this.builder.connect(dangling, res.entry, 'seq');
87
+ dangling = [...res.exits];
88
+ }
89
+ else {
90
+ // Simple statement — coalesce into the current straight-line block.
91
+ if (openSimple === undefined) {
92
+ const idx = this.builder.newBlock(startLineOf(stmt), endLineOf(stmt), stmt.text);
93
+ if (entry === undefined)
94
+ entry = idx;
95
+ else
96
+ this.builder.connect(dangling, idx, 'seq');
97
+ openSimple = idx;
98
+ dangling = [idx];
99
+ }
100
+ else {
101
+ this.builder.extendBlock(openSimple, endLineOf(stmt), stmt.text);
102
+ }
103
+ }
104
+ }
105
+ if (entry === undefined)
106
+ return null;
107
+ return { entry, exits: dangling };
108
+ }
109
+ /** Dispatch one statement to its handler. Non-null except for empty blocks. */
110
+ visitStmt(stmt) {
111
+ switch (stmt.type) {
112
+ case 'if_statement':
113
+ return this.visitIf(stmt);
114
+ case 'while_statement':
115
+ return this.visitWhile(stmt);
116
+ case 'do_statement':
117
+ return this.visitDoWhile(stmt);
118
+ case 'for_statement':
119
+ return this.visitFor(stmt);
120
+ case 'for_in_statement':
121
+ case 'for_of_statement':
122
+ return this.visitForIn(stmt);
123
+ case 'switch_statement':
124
+ return this.visitSwitch(stmt);
125
+ case 'try_statement':
126
+ return this.visitTry(stmt);
127
+ case 'return_statement':
128
+ return this.visitReturn(stmt);
129
+ case 'throw_statement':
130
+ return this.visitThrow(stmt);
131
+ case 'break_statement':
132
+ return this.visitBreak(stmt);
133
+ case 'continue_statement':
134
+ return this.visitContinue(stmt);
135
+ case 'labeled_statement':
136
+ return this.visitLabeled(stmt);
137
+ case 'statement_block':
138
+ return this.visitSeq(this.statementsOf(stmt));
139
+ default:
140
+ return this.visitSimple(stmt);
141
+ }
142
+ }
143
+ visitSimple(stmt) {
144
+ const idx = this.builder.newBlock(startLineOf(stmt), endLineOf(stmt), stmt.text);
145
+ return { entry: idx, exits: [idx] };
146
+ }
147
+ visitReturn(stmt) {
148
+ const idx = this.builder.newBlock(startLineOf(stmt), endLineOf(stmt), stmt.text);
149
+ this.builder.edge(idx, this.builder.exitIndex, 'return');
150
+ return { entry: idx, exits: [] };
151
+ }
152
+ visitThrow(stmt) {
153
+ const idx = this.builder.newBlock(startLineOf(stmt), endLineOf(stmt), stmt.text);
154
+ this.builder.edge(idx, this.currentHandler(), 'throw');
155
+ return { entry: idx, exits: [] };
156
+ }
157
+ visitBreak(stmt) {
158
+ const idx = this.builder.newBlock(startLineOf(stmt), endLineOf(stmt), stmt.text);
159
+ const target = this.cfc.breakTarget(this.labelOf(stmt));
160
+ // An unresolved target — a label this M1 visitor doesn't model (a stacked
161
+ // outer label like `outer: inner: for`, or a labeled non-loop block) —
162
+ // would otherwise leave this block with NO out-edge, stranding it and
163
+ // breaking the single-exit invariant a downstream post-dominator / PDG pass
164
+ // relies on. Conservatively route an unresolved jump to the function EXIT
165
+ // ("escapes the function"): sound over-approximation, keeps single-exit.
166
+ this.builder.edge(idx, target ?? this.builder.exitIndex, 'break');
167
+ return { entry: idx, exits: [] };
168
+ }
169
+ visitContinue(stmt) {
170
+ const idx = this.builder.newBlock(startLineOf(stmt), endLineOf(stmt), stmt.text);
171
+ const target = this.cfc.continueTarget(this.labelOf(stmt));
172
+ // See visitBreak: an unresolved label routes to EXIT to preserve single-exit.
173
+ this.builder.edge(idx, target ?? this.builder.exitIndex, 'continue');
174
+ return { entry: idx, exits: [] };
175
+ }
176
+ visitLabeled(stmt) {
177
+ const body = stmt.childForFieldName('body') ?? stmt.namedChildren[stmt.namedChildren.length - 1];
178
+ if (body && LOOP_OR_SWITCH_TYPES.has(body.type)) {
179
+ this.pendingLabel = this.labelOf(stmt);
180
+ const res = this.visitStmt(body);
181
+ this.pendingLabel = undefined; // clear even if the construct didn't consume it
182
+ return res;
183
+ }
184
+ // Labeled non-loop blocks (break-to-block-label) are not modeled in M1.
185
+ return this.visitBody(body);
186
+ }
187
+ visitIf(stmt) {
188
+ const cond = stmt.childForFieldName('condition') ?? stmt;
189
+ const condBlock = this.builder.newBlock(startLineOf(stmt), endLineOf(cond), cond.text);
190
+ const exits = [];
191
+ const thenRes = this.visitBody(stmt.childForFieldName('consequence'));
192
+ if (thenRes) {
193
+ this.builder.edge(condBlock, thenRes.entry, 'cond-true');
194
+ exits.push(...thenRes.exits);
195
+ }
196
+ else {
197
+ exits.push(condBlock); // empty then — true path falls through
198
+ }
199
+ const elseNode = this.elseBodyOf(stmt);
200
+ if (elseNode) {
201
+ const elseRes = this.visitBody(elseNode);
202
+ if (elseRes) {
203
+ this.builder.edge(condBlock, elseRes.entry, 'cond-false');
204
+ exits.push(...elseRes.exits);
205
+ }
206
+ else {
207
+ exits.push(condBlock); // empty else block
208
+ }
209
+ }
210
+ else {
211
+ exits.push(condBlock); // no else — false path falls through to the join
212
+ }
213
+ return { entry: condBlock, exits: [...new Set(exits)] };
214
+ }
215
+ /** The else body node (unwraps an `else_clause` wrapper if present). */
216
+ elseBodyOf(ifStmt) {
217
+ const alt = ifStmt.childForFieldName('alternative');
218
+ if (!alt)
219
+ return undefined;
220
+ if (alt.type === 'else_clause') {
221
+ return alt.childForFieldName('body') ?? alt.namedChildren[0];
222
+ }
223
+ return alt;
224
+ }
225
+ visitWhile(stmt) {
226
+ const label = this.takeLabel();
227
+ const cond = stmt.childForFieldName('condition') ?? stmt;
228
+ const header = this.builder.newBlock(startLineOf(stmt), endLineOf(cond), cond.text);
229
+ const loopExit = this.builder.newBlock(endLineOf(stmt), endLineOf(stmt), '');
230
+ this.cfc.pushLoop(header, loopExit, label);
231
+ const body = this.visitBody(this.bodyBlockOf(stmt));
232
+ this.cfc.pop();
233
+ if (body) {
234
+ this.builder.edge(header, body.entry, 'cond-true');
235
+ this.builder.connect(body.exits, header, 'loop-back');
236
+ }
237
+ else {
238
+ this.builder.edge(header, header, 'loop-back'); // empty body re-tests
239
+ }
240
+ this.builder.edge(header, loopExit, 'cond-false');
241
+ return { entry: header, exits: [loopExit] };
242
+ }
243
+ visitDoWhile(stmt) {
244
+ const label = this.takeLabel();
245
+ const cond = stmt.childForFieldName('condition') ?? stmt;
246
+ const condBlock = this.builder.newBlock(startLineOf(cond), endLineOf(cond), cond.text);
247
+ const loopExit = this.builder.newBlock(endLineOf(stmt), endLineOf(stmt), '');
248
+ this.cfc.pushLoop(condBlock, loopExit, label);
249
+ const body = this.visitBody(this.bodyBlockOf(stmt));
250
+ this.cfc.pop();
251
+ const backTarget = body ? body.entry : condBlock;
252
+ if (body)
253
+ this.builder.connect(body.exits, condBlock, 'seq');
254
+ this.builder.edge(condBlock, backTarget, 'loop-back'); // cond true → run body again
255
+ this.builder.edge(condBlock, loopExit, 'cond-false');
256
+ return { entry: backTarget, exits: [loopExit] };
257
+ }
258
+ visitFor(stmt) {
259
+ const label = this.takeLabel();
260
+ const init = stmt.childForFieldName('initializer');
261
+ const cond = stmt.childForFieldName('condition');
262
+ const incr = stmt.childForFieldName('increment');
263
+ const header = this.builder.newBlock(startLineOf(stmt), cond ? endLineOf(cond) : startLineOf(stmt), cond ? cond.text : 'for(;;)');
264
+ const loopExit = this.builder.newBlock(endLineOf(stmt), endLineOf(stmt), '');
265
+ let incrBlock = header;
266
+ if (incr) {
267
+ incrBlock = this.builder.newBlock(startLineOf(incr), endLineOf(incr), incr.text);
268
+ this.builder.edge(incrBlock, header, 'loop-back');
269
+ }
270
+ this.cfc.pushLoop(incrBlock, loopExit, label);
271
+ const body = this.visitBody(this.bodyBlockOf(stmt));
272
+ this.cfc.pop();
273
+ if (body) {
274
+ this.builder.edge(header, body.entry, 'cond-true');
275
+ // With no increment clause the body's exits ARE the back-edge — carry
276
+ // the loop-back kind on them (mirroring visitWhile/visitForIn) instead
277
+ // of a phantom header→header self-loop that models a path which never
278
+ // executes the body. With an increment, the body falls through to the
279
+ // increment (`seq`) and the increment carries the loop-back (:338).
280
+ this.builder.connect(body.exits, incrBlock, incr ? 'seq' : 'loop-back');
281
+ }
282
+ else {
283
+ this.builder.edge(header, incrBlock, 'cond-true');
284
+ // Empty body with no increment: the header genuinely re-tests itself.
285
+ if (!incr)
286
+ this.builder.edge(header, header, 'loop-back');
287
+ }
288
+ this.builder.edge(header, loopExit, 'cond-false');
289
+ let entry = header;
290
+ if (init) {
291
+ const initBlock = this.builder.newBlock(startLineOf(init), endLineOf(init), init.text);
292
+ this.builder.edge(initBlock, header, 'seq');
293
+ entry = initBlock;
294
+ }
295
+ return { entry, exits: [loopExit] };
296
+ }
297
+ visitForIn(stmt) {
298
+ const label = this.takeLabel();
299
+ const header = this.builder.newBlock(startLineOf(stmt), startLineOf(stmt), this.forInHeaderText(stmt));
300
+ const loopExit = this.builder.newBlock(endLineOf(stmt), endLineOf(stmt), '');
301
+ this.cfc.pushLoop(header, loopExit, label);
302
+ const body = this.visitBody(this.bodyBlockOf(stmt));
303
+ this.cfc.pop();
304
+ if (body) {
305
+ this.builder.edge(header, body.entry, 'cond-true');
306
+ this.builder.connect(body.exits, header, 'loop-back');
307
+ }
308
+ else {
309
+ this.builder.edge(header, header, 'loop-back');
310
+ }
311
+ this.builder.edge(header, loopExit, 'cond-false');
312
+ return { entry: header, exits: [loopExit] };
313
+ }
314
+ forInHeaderText(stmt) {
315
+ const left = stmt.childForFieldName('left')?.text ?? '';
316
+ const right = stmt.childForFieldName('right')?.text ?? '';
317
+ return left || right ? `for(${left} … ${right})` : 'for(… in/of …)';
318
+ }
319
+ visitSwitch(stmt) {
320
+ const label = this.takeLabel();
321
+ const value = stmt.childForFieldName('value') ?? stmt;
322
+ const dispatch = this.builder.newBlock(startLineOf(stmt), endLineOf(value), value.text);
323
+ const switchExit = this.builder.newBlock(endLineOf(stmt), endLineOf(stmt), '');
324
+ this.cfc.pushSwitch(switchExit, label);
325
+ const body = stmt.childForFieldName('body');
326
+ const cases = body
327
+ ? body.namedChildren.filter((c) => c.type === 'switch_case' || c.type === 'switch_default')
328
+ : [];
329
+ const caseResults = cases.map((c) => this.visitSeq(this.caseStatements(c)));
330
+ const hasDefault = cases.some((c) => c.type === 'switch_default');
331
+ // entryOf[i] = block a dispatch/fallthrough INTO case i lands on (empty
332
+ // cases are transparent — they resolve to the next case, or the exit).
333
+ const entryOf = new Array(cases.length);
334
+ let after = switchExit;
335
+ for (let i = cases.length - 1; i >= 0; i--) {
336
+ entryOf[i] = caseResults[i]?.entry ?? after;
337
+ after = entryOf[i];
338
+ }
339
+ for (let i = 0; i < cases.length; i++) {
340
+ this.builder.edge(dispatch, entryOf[i], 'switch-case');
341
+ }
342
+ if (!hasDefault)
343
+ this.builder.edge(dispatch, switchExit, 'switch-case'); // no-match path
344
+ for (let i = 0; i < cases.length; i++) {
345
+ const res = caseResults[i];
346
+ if (!res)
347
+ continue;
348
+ const fallTarget = i + 1 < cases.length ? entryOf[i + 1] : switchExit;
349
+ this.builder.connect(res.exits, fallTarget, 'fallthrough');
350
+ }
351
+ this.cfc.pop();
352
+ return { entry: dispatch, exits: [switchExit] };
353
+ }
354
+ caseStatements(caseNode) {
355
+ const value = caseNode.childForFieldName('value');
356
+ return caseNode.namedChildren.filter((c) => c.id !== value?.id && c.type !== 'comment');
357
+ }
358
+ visitTry(stmt) {
359
+ const bodyNode = stmt.childForFieldName('body');
360
+ // Single pass over named children — tree-sitter's `namedChildren` getter
361
+ // allocates a fresh array on every access, so avoid the double `.find`.
362
+ let catchClause;
363
+ let finallyClause;
364
+ for (let i = 0; i < stmt.namedChildCount; i++) {
365
+ const c = stmt.namedChild(i);
366
+ if (c?.type === 'catch_clause')
367
+ catchClause = c;
368
+ else if (c?.type === 'finally_clause')
369
+ finallyClause = c;
370
+ }
371
+ // Build finally first so its entry is known as both a normal join and a
372
+ // handler target. The finally body runs in the OUTER handler context.
373
+ const finallyRes = finallyClause
374
+ ? this.visitSeq(this.statementsOf(this.bodyBlockOf(finallyClause)))
375
+ : null;
376
+ // A throw inside catch propagates to finally (if any), else the outer handler.
377
+ let catchRes = null;
378
+ if (catchClause) {
379
+ if (finallyRes)
380
+ this.handlers.push(finallyRes.entry);
381
+ catchRes = this.visitSeq(this.statementsOf(this.bodyBlockOf(catchClause)));
382
+ if (finallyRes)
383
+ this.handlers.pop();
384
+ if (catchRes === null) {
385
+ // Empty (or comment-only) catch body — `catch {}`. The clause still
386
+ // CATCHES: handler semantics key off the syntactic clause, not the
387
+ // traversal result. Treating it as "no catch" sent the swallowed
388
+ // exception to the outer handler/EXIT and left post-try code
389
+ // unreachable when the body always throws — a hard false-negative
390
+ // for downstream taint. Synthesize one empty block spanning the
391
+ // clause (entry == sole exit) so exception flow lands in it and
392
+ // rejoins the normal continuation. Created BEFORE the protected
393
+ // region is walked, so it never receives a spurious throw edge.
394
+ const idx = this.builder.newBlock(startLineOf(catchClause), endLineOf(catchClause), '');
395
+ catchRes = { entry: idx, exits: [idx] };
396
+ }
397
+ }
398
+ // Handler for the try body: catch if present, else finally, else outer.
399
+ const tryHandler = catchRes?.entry ?? finallyRes?.entry ?? this.currentHandler();
400
+ const protectedStart = this.builder.blockCount;
401
+ this.handlers.push(tryHandler);
402
+ const bodyRes = bodyNode ? this.visitSeq(this.statementsOf(bodyNode)) : null;
403
+ this.handlers.pop();
404
+ // Conservative exceptional edges: ANY block in the protected region may raise
405
+ // to the handler — not just an explicit `throw`, and not just the body ENTRY.
406
+ // Edging every block created during the try-body walk keeps exception flow
407
+ // sound when the body BRANCHES: an `if` / nested-try / post-branch block whose
408
+ // interior blocks would otherwise have no path to the handler — i.e. a taint
409
+ // false-negative into `catch` for the downstream PDG analysis. The
410
+ // per-function edge cap bounds the count; explicit `throw`s add their own
411
+ // (idempotent) edge to the same handler.
412
+ if (catchClause || finallyClause) {
413
+ for (let b = protectedStart; b < this.builder.blockCount; b++) {
414
+ this.builder.edge(b, tryHandler, 'throw');
415
+ }
416
+ }
417
+ const exits = [];
418
+ if (finallyRes) {
419
+ // Normal completion of try AND catch both flow through finally.
420
+ if (bodyRes)
421
+ this.builder.connect(bodyRes.exits, finallyRes.entry, 'seq');
422
+ if (catchRes)
423
+ this.builder.connect(catchRes.exits, finallyRes.entry, 'seq');
424
+ exits.push(...finallyRes.exits);
425
+ // No catch → an exception re-propagates out after finally runs.
426
+ if (!catchRes)
427
+ this.builder.connect(finallyRes.exits, this.currentHandler(), 'throw');
428
+ }
429
+ else {
430
+ if (bodyRes)
431
+ exits.push(...bodyRes.exits);
432
+ if (catchRes)
433
+ exits.push(...catchRes.exits);
434
+ }
435
+ const entry = bodyRes?.entry ?? finallyRes?.entry ?? catchRes?.entry;
436
+ if (entry === undefined)
437
+ return null;
438
+ return { entry, exits: [...new Set(exits)] };
439
+ }
440
+ /** Nearest enclosing exception handler, or the function EXIT. */
441
+ currentHandler() {
442
+ return this.handlers.length ? this.handlers[this.handlers.length - 1] : this.builder.exitIndex;
443
+ }
444
+ /** Consume the label awaiting the loop/switch this call is building. */
445
+ takeLabel() {
446
+ const label = this.pendingLabel;
447
+ this.pendingLabel = undefined;
448
+ return label;
449
+ }
450
+ labelOf(stmt) {
451
+ const id = stmt.childForFieldName('label') ??
452
+ stmt.namedChildren.find((c) => c.type === 'statement_identifier');
453
+ return id?.text;
454
+ }
455
+ }
456
+ /** Build the CFG for one TS/JS function node (or `undefined` if not a function). */
457
+ function buildFunctionCfg(fnNode, filePath) {
458
+ if (!TS_FUNCTION_TYPES.has(fnNode.type))
459
+ return undefined;
460
+ const startLine = startLineOf(fnNode);
461
+ const endLine = endLineOf(fnNode);
462
+ const startColumn = fnNode.startPosition.column;
463
+ const builder = new CfgBuilder(filePath, startLine, endLine, startColumn);
464
+ const body = fnNode.childForFieldName('body');
465
+ if (!body)
466
+ return undefined; // overload signature / abstract method — no body
467
+ if (body.type !== 'statement_block') {
468
+ // Expression-bodied arrow: `() => expr` — one block whose value is returned.
469
+ const blk = builder.newBlock(startLineOf(body), endLineOf(body), body.text);
470
+ builder.edge(builder.entryIndex, blk, 'seq');
471
+ builder.edge(blk, builder.exitIndex, 'return');
472
+ return builder.finish();
473
+ }
474
+ const walk = new TsCfgWalk(builder);
475
+ const res = walk.visitSeq(body.namedChildren.filter((c) => c.type !== 'comment'));
476
+ if (!res) {
477
+ builder.edge(builder.entryIndex, builder.exitIndex, 'seq'); // empty body
478
+ return builder.finish();
479
+ }
480
+ builder.edge(builder.entryIndex, res.entry, 'seq');
481
+ builder.connect(res.exits, builder.exitIndex, 'seq'); // normal fall-off → EXIT
482
+ return builder.finish();
483
+ }
484
+ /** Whether a node is a TS/JS function this visitor builds a CFG for. */
485
+ function isFunction(node) {
486
+ return TS_FUNCTION_TYPES.has(node.type);
487
+ }
488
+ /** The TS/JS CFG visitor (shared by TypeScript and JavaScript). */
489
+ export function createTypeScriptCfgVisitor() {
490
+ return { buildFunctionCfg, isFunction };
491
+ }
492
+ export { TS_FUNCTION_TYPES };
@@ -19,6 +19,7 @@ import type { MethodExtractor } from './method-types.js';
19
19
  import type { VariableExtractor } from './variable-types.js';
20
20
  import type { ImportResolverFn } from './import-resolvers/types.js';
21
21
  import type { SyntaxNode } from './utils/ast-helpers.js';
22
+ import type { CfgVisitor } from './cfg/types.js';
22
23
  import type { NodeLabel } from '../../_shared/index.js';
23
24
  import type Parser from 'tree-sitter';
24
25
  import type { ExtractedDecoratorRoute } from './workers/parse-worker.js';
@@ -284,6 +285,16 @@ interface LanguageProviderConfig {
284
285
  * Default: undefined (provider has no capture-time module-level side effects).
285
286
  */
286
287
  readonly collectCaptureSideChannel?: (filePath: string) => unknown;
288
+ /**
289
+ * Per-language control-flow-graph builder (#2081 M1, PDG/taint substrate).
290
+ * Invoked IN THE PARSE WORKER (where the AST lives) for each function node,
291
+ * gated on the `--pdg` opt-in; the resulting per-function CFGs are serialized
292
+ * onto `ParsedFile.cfgSideChannel` and emitted as BasicBlock nodes + CFG
293
+ * edges during scope-resolution. `TNode` is `SyntaxNode` for the tree-sitter
294
+ * languages. Default: undefined (language has no CFG support yet — TS/JS are
295
+ * the M1 set).
296
+ */
297
+ readonly cfgVisitor?: CfgVisitor<SyntaxNode>;
287
298
  /**
288
299
  * Interpret a raw `@import.statement` capture group into a `ParsedImport`.
289
300
  * The central finalize algorithm resolves `ParsedImport.targetRaw` to a
@@ -193,8 +193,12 @@ function findFuncDeclarator(node) {
193
193
  }
194
194
  return null;
195
195
  }
196
- // Unwrap pointer_declarator / reference_declarator
197
- while (decl.type === 'pointer_declarator' || decl.type === 'reference_declarator') {
196
+ // Unwrap declarator wrappers. Deleted free functions are represented as
197
+ // `init_declarator(function_declarator, delete_expression)` by
198
+ // tree-sitter-cpp 0.23.
199
+ while (decl.type === 'pointer_declarator' ||
200
+ decl.type === 'reference_declarator' ||
201
+ decl.type === 'init_declarator') {
198
202
  const next = decl.childForFieldName('declarator');
199
203
  if (next === null) {
200
204
  // reference_declarator may not use field name
@@ -113,6 +113,9 @@ export function emitCppScopeCaptures(sourceText, filePath, cachedTree) {
113
113
  if (hasExplicitSpecifier(fnNode)) {
114
114
  grouped['@declaration.is-explicit'] = syntheticCapture('@declaration.is-explicit', fnNode, 'true');
115
115
  }
116
+ if (hasDeletedMethodClause(fnNode, grouped['@declaration.name']?.text)) {
117
+ grouped['@declaration.is-deleted'] = syntheticCapture('@declaration.is-deleted', fnNode, 'true');
118
+ }
116
119
  // Detect static storage class (file-local linkage)
117
120
  if (hasStaticStorageClass(fnNode)) {
118
121
  const nameText = grouped['@declaration.name']?.text;
@@ -1562,8 +1565,11 @@ function extractDeclaratorLeafName(node) {
1562
1565
  let cur = node;
1563
1566
  let safety = 16;
1564
1567
  while (safety-- > 0) {
1565
- if (cur.type === 'identifier' || cur.type === 'type_identifier')
1568
+ if (cur.type === 'identifier' ||
1569
+ cur.type === 'type_identifier' ||
1570
+ cur.type === 'operator_name') {
1566
1571
  return cur.text;
1572
+ }
1567
1573
  // Common wrapper nodes — follow the 'declarator' field when present.
1568
1574
  const next = cur.childForFieldName('declarator') ??
1569
1575
  // parenthesized_declarator: single named child
@@ -1590,6 +1596,23 @@ function hasExplicitSpecifier(node) {
1590
1596
  }
1591
1597
  return /\bexplicit\b/.test(node.text.slice(0, 128));
1592
1598
  }
1599
+ function hasDeletedMethodClause(node, callableName) {
1600
+ for (let i = 0; i < node.namedChildCount; i++) {
1601
+ const child = node.namedChild(i);
1602
+ if (child?.type === 'delete_method_clause')
1603
+ return true;
1604
+ // tree-sitter-cpp 0.23 parses a deleted free-function declaration as
1605
+ // `declaration > init_declarator > delete_expression`, while class
1606
+ // members use the dedicated `delete_method_clause`.
1607
+ if (child?.type === 'init_declarator' &&
1608
+ child.childForFieldName('value')?.type === 'delete_expression' &&
1609
+ callableName !== undefined &&
1610
+ extractDeclaratorLeafName(child.childForFieldName('declarator') ?? child) === callableName) {
1611
+ return true;
1612
+ }
1613
+ }
1614
+ return false;
1615
+ }
1593
1616
  /**
1594
1617
  * Check if a C++ function_definition or declaration has `static` storage class.
1595
1618
  */
@@ -193,6 +193,29 @@ const CPP_SCOPE_QUERY = `
193
193
  declarator: (function_declarator
194
194
  declarator: (identifier) @declaration.name)) @declaration.function
195
195
 
196
+ ;; tree-sitter-cpp 0.23 represents a deleted free function as an
197
+ ;; init_declarator whose value is a delete_expression.
198
+ (declaration
199
+ declarator: (init_declarator
200
+ declarator: (function_declarator
201
+ declarator: (identifier) @declaration.name)
202
+ value: (delete_expression))) @declaration.function
203
+
204
+ ;; Deleted free operator declaration.
205
+ (declaration
206
+ declarator: (init_declarator
207
+ declarator: (function_declarator
208
+ declarator: (operator_name) @declaration.name)
209
+ value: (delete_expression))) @declaration.function
210
+
211
+ ;; Deleted free function with a pointer return type.
212
+ (declaration
213
+ declarator: (init_declarator
214
+ declarator: (pointer_declarator
215
+ declarator: (function_declarator
216
+ declarator: (identifier) @declaration.name))
217
+ value: (delete_expression))) @declaration.function
218
+
196
219
  ;; Free operator prototype: std::ostream& operator<<(std::ostream&, T)
197
220
  (declaration
198
221
  declarator: (function_declarator