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.
- package/dist/_shared/scope-resolution/parsed-file.d.ts +21 -0
- package/dist/_shared/scope-resolution/parsed-file.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/symbol-definition.d.ts +4 -0
- package/dist/_shared/scope-resolution/symbol-definition.d.ts.map +1 -1
- package/dist/cli/analyze-config.js +1 -0
- package/dist/cli/analyze.d.ts +6 -0
- package/dist/cli/analyze.js +2 -0
- package/dist/cli/index.js +2 -0
- package/dist/core/ingestion/cfg/cfg-builder.d.ts +51 -0
- package/dist/core/ingestion/cfg/cfg-builder.js +100 -0
- package/dist/core/ingestion/cfg/collect.d.ts +30 -0
- package/dist/core/ingestion/cfg/collect.js +34 -0
- package/dist/core/ingestion/cfg/control-flow-context.d.ts +27 -0
- package/dist/core/ingestion/cfg/control-flow-context.js +49 -0
- package/dist/core/ingestion/cfg/emit.d.ts +64 -0
- package/dist/core/ingestion/cfg/emit.js +95 -0
- package/dist/core/ingestion/cfg/traversal-result.d.ts +20 -0
- package/dist/core/ingestion/cfg/traversal-result.js +2 -0
- package/dist/core/ingestion/cfg/types.d.ts +66 -0
- package/dist/core/ingestion/cfg/types.js +13 -0
- package/dist/core/ingestion/cfg/visitors/typescript.d.ts +49 -0
- package/dist/core/ingestion/cfg/visitors/typescript.js +492 -0
- package/dist/core/ingestion/language-provider.d.ts +11 -0
- package/dist/core/ingestion/languages/cpp/arity-metadata.js +6 -2
- package/dist/core/ingestion/languages/cpp/captures.js +24 -1
- package/dist/core/ingestion/languages/cpp/query.js +23 -0
- package/dist/core/ingestion/languages/typescript.js +5 -0
- package/dist/core/ingestion/method-extractors/configs/c-cpp.js +6 -13
- package/dist/core/ingestion/method-extractors/generic.js +1 -0
- package/dist/core/ingestion/method-types.d.ts +2 -0
- package/dist/core/ingestion/model/symbol-table.d.ts +1 -0
- package/dist/core/ingestion/model/symbol-table.js +1 -0
- package/dist/core/ingestion/parsing-processor.js +1 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +18 -1
- package/dist/core/ingestion/pipeline.d.ts +23 -0
- package/dist/core/ingestion/scope-extractor.js +3 -0
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +12 -0
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +145 -24
- package/dist/core/ingestion/scope-resolution/pipeline/phase.js +3 -0
- package/dist/core/ingestion/scope-resolution/pipeline/reconcile-ownership.js +43 -3
- package/dist/core/ingestion/scope-resolution/pipeline/run.d.ts +10 -0
- package/dist/core/ingestion/scope-resolution/pipeline/run.js +62 -0
- package/dist/core/ingestion/scope-resolution/resolution-outcome.d.ts +1 -1
- package/dist/core/ingestion/utils/method-props.js +1 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +1 -0
- package/dist/core/ingestion/workers/parse-worker.js +43 -1
- package/dist/core/ingestion/workers/worker-pool.d.ts +9 -0
- package/dist/core/ingestion/workers/worker-pool.js +5 -2
- package/dist/core/run-analyze.d.ts +32 -0
- package/dist/core/run-analyze.js +75 -1
- package/dist/storage/parse-cache.d.ts +22 -1
- package/dist/storage/parse-cache.js +20 -10
- package/dist/storage/repo-manager.d.ts +31 -7
- 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
|
|
197
|
-
|
|
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' ||
|
|
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
|