@holmes-lab/holmes-kit 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +65 -0
- package/README.md +15 -9
- package/dist/.build-id +1 -1
- package/dist/holmes/cpg/foundation/cfg.js +487 -4
- package/dist/holmes/cpg/foundation/ddg.js +251 -5
- package/dist/holmes/cpg/foundation/language-matrix.d.ts +17 -0
- package/dist/holmes/cpg/foundation/language-matrix.js +52 -4
- package/dist/holmes/cpg/language-capability.js +15 -3
- package/dist/holmes/cpg/language-parser-walk.js +63 -5
- package/dist/holmes/mcp/handlers.js +40 -28
- package/dist/holmes/rtm/flow-sensitive-taint.js +18 -2
- package/dist/holmes/rtm/rtm-builder.d.ts +6 -14
- package/dist/holmes/rtm/rtm-builder.js +117 -0
- package/dist/holmes/rtm/taint-vocabulary.d.ts +1 -1
- package/dist/holmes/rtm/taint-vocabulary.js +46 -0
- package/package.json +1 -1
|
@@ -12,6 +12,14 @@ exports.FUNCTION_TYPES = new Set([
|
|
|
12
12
|
'function_definition', 'lambda',
|
|
13
13
|
// @implements A-SPEC-519.1 — Go: methods and function literals (closures/goroutine bodies).
|
|
14
14
|
'method_declaration', 'func_literal',
|
|
15
|
+
// @implements A-SPEC-520.1 — Java: constructors, lambdas and initializer blocks.
|
|
16
|
+
'constructor_declaration', 'lambda_expression', 'static_initializer',
|
|
17
|
+
'instance_initializer', 'compact_constructor_declaration',
|
|
18
|
+
// @implements A-SPEC-521.1 — C#: local functions, anonymous methods, operators, accessors.
|
|
19
|
+
'local_function_statement', 'anonymous_method_expression', 'destructor_declaration',
|
|
20
|
+
'operator_declaration', 'conversion_operator_declaration', 'accessor_declaration',
|
|
21
|
+
// @implements A-SPEC-522.1 — Rust: named functions and closures.
|
|
22
|
+
'function_item', 'closure_expression',
|
|
15
23
|
]);
|
|
16
24
|
const TS_SIMPLE = new Set([
|
|
17
25
|
'expression_statement', 'lexical_declaration', 'variable_declaration', 'debugger_statement',
|
|
@@ -69,8 +77,102 @@ const GO_RULES = {
|
|
|
69
77
|
]),
|
|
70
78
|
block: 'block',
|
|
71
79
|
};
|
|
80
|
+
// @implements A-SPEC-520.1 — Java, classified from the census over gson+junit4 (8,376
|
|
81
|
+
// functions, 20 statement types). Java rides the TS chassis: if/while/for/do/labeled/break/
|
|
82
|
+
// continue/return/throw and the try-clause names are the SAME probed shapes. What is Java's own:
|
|
83
|
+
// one switch node carrying two styles, a try body spelled `block`, try-with-resources, and
|
|
84
|
+
// synchronized as a body wrapper.
|
|
85
|
+
const JAVA_SIMPLE = new Set([
|
|
86
|
+
'expression_statement', 'local_variable_declaration', 'class_declaration',
|
|
87
|
+
'record_declaration', 'assert_statement', 'empty_statement',
|
|
88
|
+
]);
|
|
89
|
+
const JAVA_RULES = {
|
|
90
|
+
simple: JAVA_SIMPLE,
|
|
91
|
+
handled: new Set([
|
|
92
|
+
...JAVA_SIMPLE,
|
|
93
|
+
'block', 'if_statement', 'while_statement', 'do_statement', 'for_statement',
|
|
94
|
+
'enhanced_for_statement', 'switch_expression', 'try_statement',
|
|
95
|
+
'try_with_resources_statement', 'synchronized_statement', 'labeled_statement',
|
|
96
|
+
'break_statement', 'continue_statement', 'return_statement', 'throw_statement',
|
|
97
|
+
'yield_statement',
|
|
98
|
+
]),
|
|
99
|
+
block: 'block',
|
|
100
|
+
};
|
|
101
|
+
// @implements A-SPEC-521.1 — C#, classified from the census over newtonsoft+restsharp (13,642
|
|
102
|
+
// functions, 27 statement types). Same face as Java, opposite semantics in three places: switch
|
|
103
|
+
// fallthrough is ILLEGAL (sections isolate), `yield` is the iterator protocol (return passes
|
|
104
|
+
// through, break exits), and the preprocessor puts ALTERNATIVES in statement position. `goto`
|
|
105
|
+
// refuses (one corpus occurrence). Files where conditional compilation crosses syntax refuse at
|
|
106
|
+
// L1 (51 of 1,149 — the honest limit, recorded).
|
|
107
|
+
const CSHARP_SIMPLE = new Set([
|
|
108
|
+
'expression_statement', 'local_declaration_statement', 'local_function_statement',
|
|
109
|
+
'empty_statement', 'preproc_region', 'preproc_endregion', 'preproc_pragma',
|
|
110
|
+
]);
|
|
111
|
+
const CSHARP_RULES = {
|
|
112
|
+
simple: CSHARP_SIMPLE,
|
|
113
|
+
handled: new Set([
|
|
114
|
+
...CSHARP_SIMPLE,
|
|
115
|
+
'block', 'if_statement', 'while_statement', 'do_statement', 'for_statement',
|
|
116
|
+
'foreach_statement', 'switch_statement', 'try_statement', 'using_statement',
|
|
117
|
+
'lock_statement', 'checked_statement', 'preproc_if',
|
|
118
|
+
'break_statement', 'continue_statement', 'return_statement', 'throw_statement',
|
|
119
|
+
'yield_statement',
|
|
120
|
+
]),
|
|
121
|
+
block: 'block',
|
|
122
|
+
};
|
|
123
|
+
// @implements A-SPEC-522.1 — Rust, where control flow is an EXPRESSION. The simple set
|
|
124
|
+
// enumerates the value expressions and declarations the corpus census actually put in statement
|
|
125
|
+
// or trailing position (36 kinds over ripgrep+serde); anything new refuses, keeping fail-closed
|
|
126
|
+
// meaningful. Control expressions are lowered as control wherever they stand as a statement or a
|
|
127
|
+
// trailing value — flattening them would produce the zero-violation HOLLOW graph this slice's
|
|
128
|
+
// corpus gate (branch edges > 0) exists to catch.
|
|
129
|
+
const RUST_SIMPLE = new Set([
|
|
130
|
+
'let_declaration', 'call_expression', 'macro_invocation', 'identifier', 'self',
|
|
131
|
+
'scoped_identifier', 'field_expression', 'reference_expression', 'binary_expression',
|
|
132
|
+
'unary_expression', 'assignment_expression', 'compound_assignment_expr', 'struct_expression',
|
|
133
|
+
'tuple_expression', 'array_expression', 'index_expression', 'await_expression',
|
|
134
|
+
'type_cast_expression', 'range_expression', 'closure_expression', 'parenthesized_expression',
|
|
135
|
+
'integer_literal', 'float_literal', 'string_literal', 'raw_string_literal', 'char_literal',
|
|
136
|
+
'boolean_literal', 'unit_expression', 'try_expression', 'method_call_expression',
|
|
137
|
+
'use_declaration', 'struct_item', 'enum_item', 'impl_item', 'function_item', 'const_item',
|
|
138
|
+
'static_item', 'type_item', 'mod_item', 'trait_item', 'macro_definition', 'attribute_item',
|
|
139
|
+
'inner_attribute_item', 'empty_statement', 'foreign_mod_item', 'extern_crate_declaration',
|
|
140
|
+
// expression_statement lands here when its inner is NOT a control expression (the unwrap
|
|
141
|
+
// check runs first), so a plain `g();` is one point.
|
|
142
|
+
'expression_statement',
|
|
143
|
+
]);
|
|
144
|
+
const RUST_RULES = {
|
|
145
|
+
simple: RUST_SIMPLE,
|
|
146
|
+
handled: new Set([
|
|
147
|
+
...RUST_SIMPLE,
|
|
148
|
+
'block', 'expression_statement', 'if_expression', 'match_expression', 'for_expression',
|
|
149
|
+
'while_expression', 'loop_expression', 'return_expression', 'break_expression',
|
|
150
|
+
'continue_expression',
|
|
151
|
+
]),
|
|
152
|
+
block: 'block',
|
|
153
|
+
};
|
|
154
|
+
// @implements A-SPEC-523.1 — C++, classified from three corpora. Its first fact is the PARSER:
|
|
155
|
+
// 25-34% of template/macro-heavy real files refuse at L1 (measured on nlohmann/fmt/leveldb) —
|
|
156
|
+
// the matrix's ast basis discloses this. What parses rides the TS chassis (else_clause wrapper,
|
|
157
|
+
// C fallthrough switch, same try-clause names). Labels and goto refuse (zero corpus occurrences).
|
|
158
|
+
const CPP_SIMPLE = new Set([
|
|
159
|
+
'expression_statement', 'declaration', 'alias_declaration', 'static_assert_declaration',
|
|
160
|
+
'enum_specifier', 'using_declaration', 'type_definition', 'struct_specifier',
|
|
161
|
+
'preproc_call', 'preproc_def', 'preproc_function_def', 'preproc_include',
|
|
162
|
+
]);
|
|
163
|
+
const CPP_RULES = {
|
|
164
|
+
simple: CPP_SIMPLE,
|
|
165
|
+
handled: new Set([
|
|
166
|
+
...CPP_SIMPLE,
|
|
167
|
+
'compound_statement', 'if_statement', 'while_statement', 'do_statement', 'for_statement',
|
|
168
|
+
'for_range_loop', 'switch_statement', 'try_statement', 'throw_statement',
|
|
169
|
+
'break_statement', 'continue_statement', 'return_statement', 'preproc_if', 'preproc_ifdef',
|
|
170
|
+
]),
|
|
171
|
+
block: 'compound_statement',
|
|
172
|
+
};
|
|
72
173
|
const LANG_RULES = {
|
|
73
|
-
typescript: TS_RULES, tsx: TS_RULES, python: PY_RULES, go: GO_RULES,
|
|
174
|
+
typescript: TS_RULES, tsx: TS_RULES, python: PY_RULES, go: GO_RULES, java: JAVA_RULES,
|
|
175
|
+
csharp: CSHARP_RULES, rust: RUST_RULES, cpp: CPP_RULES,
|
|
74
176
|
};
|
|
75
177
|
// @implements A-SPEC-510.6
|
|
76
178
|
/**
|
|
@@ -115,6 +217,7 @@ function cfgOf(ast, fn, source) {
|
|
|
115
217
|
const rules = LANG_RULES[ast.lang];
|
|
116
218
|
const isPython = ast.lang === 'python';
|
|
117
219
|
const isGo = ast.lang === 'go';
|
|
220
|
+
const isRust = ast.lang === 'rust';
|
|
118
221
|
const kids = childrenIndex(ast);
|
|
119
222
|
// Points: one per lowered statement (block ids after coalescing). 0=ENTRY, 1=EXIT.
|
|
120
223
|
const stmtsOf = [[], []];
|
|
@@ -155,7 +258,9 @@ function cfgOf(ast, fn, source) {
|
|
|
155
258
|
}
|
|
156
259
|
return { entry, exits };
|
|
157
260
|
};
|
|
158
|
-
const stmtChildren = (i) =>
|
|
261
|
+
const stmtChildren = (i) =>
|
|
262
|
+
// `comment` (TS/PY/Go) and Java's line_comment/block_comment are all named nodes.
|
|
263
|
+
kids.of(i).filter((k) => ast.nodes[k].named && !ast.nodes[k].type.includes('comment'));
|
|
159
264
|
const wireException = (first, targetProvider) => {
|
|
160
265
|
// Conservative: every point created inside [first, now) can throw into the handler.
|
|
161
266
|
const target = targetProvider();
|
|
@@ -168,14 +273,54 @@ function cfgOf(ast, fn, source) {
|
|
|
168
273
|
throw new Unsupported(t);
|
|
169
274
|
const label = ctx.pendingLabel;
|
|
170
275
|
ctx.pendingLabel = null;
|
|
276
|
+
// @implements A-SPEC-522.1 — Rust control lives in expressions. A statement whose sole
|
|
277
|
+
// named child is a control expression lowers as that control (flattening it makes the
|
|
278
|
+
// hollow graph); a `?` anywhere in a point's subtree adds the implicit early-return edge;
|
|
279
|
+
// a let-else is a branch whose false arm is the (compiler-enforced) diverging else block.
|
|
280
|
+
const RUST_CONTROL = new Set(['if_expression', 'match_expression', 'for_expression', 'while_expression', 'loop_expression', 'return_expression', 'break_expression', 'continue_expression']);
|
|
281
|
+
if (isRust && t === 'expression_statement') {
|
|
282
|
+
const inner = stmtChildren(i);
|
|
283
|
+
if (inner.length === 1 && RUST_CONTROL.has(ast.nodes[inner[0]].type))
|
|
284
|
+
return lowerStmt(inner[0], ctx);
|
|
285
|
+
}
|
|
286
|
+
const rustQuestion = (p, root) => {
|
|
287
|
+
if (!isRust)
|
|
288
|
+
return;
|
|
289
|
+
const stack = [root];
|
|
290
|
+
while (stack.length) {
|
|
291
|
+
const n = stack.pop();
|
|
292
|
+
if (n !== root && exports.FUNCTION_TYPES.has(ast.nodes[n].type))
|
|
293
|
+
continue; // closures own their `?`
|
|
294
|
+
if (ast.nodes[n].type === 'try_expression') {
|
|
295
|
+
link(p, ctx.catchT.at(-1) ?? EXIT, 'throw');
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
for (const c of kids.of(n))
|
|
299
|
+
stack.push(c);
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
if (isRust && t === 'let_declaration') {
|
|
303
|
+
const elseBlock = kids.of(i).some((k) => !ast.nodes[k].named && source !== undefined
|
|
304
|
+
&& source.slice(ast.nodes[k].start, ast.nodes[k].end) === 'else')
|
|
305
|
+
? kids.of(i).filter((k) => ast.nodes[k].type === 'block').at(-1) : undefined;
|
|
306
|
+
if (elseBlock !== undefined) {
|
|
307
|
+
const p = newPoint(i);
|
|
308
|
+
rustQuestion(p, i);
|
|
309
|
+
const ef = lowerStmt(elseBlock, ctx);
|
|
310
|
+
link(p, ef.entry, 'false');
|
|
311
|
+
return { entry: p, exits: [{ from: p, kind: 'true' }, ...ef.exits] };
|
|
312
|
+
}
|
|
313
|
+
}
|
|
171
314
|
if (rules.simple.has(t)) {
|
|
172
315
|
const p = newPoint(i);
|
|
316
|
+
rustQuestion(p, i);
|
|
173
317
|
return { entry: p, exits: [{ from: p, kind: 'seq' }] };
|
|
174
318
|
}
|
|
175
319
|
switch (t) {
|
|
176
320
|
case 'statement_block':
|
|
177
321
|
case 'block':
|
|
178
322
|
case 'statement_list':
|
|
323
|
+
case 'compound_statement':
|
|
179
324
|
// Go's block carries one extra statement_list layer (probed shape) — both are containers.
|
|
180
325
|
return lowerSeq(stmtChildren(i), ctx);
|
|
181
326
|
case 'labeled_statement': {
|
|
@@ -209,9 +354,29 @@ function cfgOf(ast, fn, source) {
|
|
|
209
354
|
: stmtChildren(clause).find((k) => ast.nodes[k].type === 'block');
|
|
210
355
|
}
|
|
211
356
|
}
|
|
357
|
+
else if (ast.lang === 'java' || ast.lang === 'csharp') {
|
|
358
|
+
// @implements A-SPEC-520.1 / A-SPEC-521.1 — Java and C# have NO else_clause
|
|
359
|
+
// wrapper: named[2] IS the
|
|
360
|
+
// alternative (a block, or the next if_statement of an else-if chain). Unwrapping it
|
|
361
|
+
// like TS lowered an else-if's CONDITION as a statement — the corpus caught it.
|
|
362
|
+
alt = named[2];
|
|
363
|
+
}
|
|
212
364
|
else {
|
|
213
365
|
alt = named[2] !== undefined ? stmtChildren(named[2])[0] : undefined;
|
|
214
366
|
}
|
|
367
|
+
if (cons === undefined || !rules.handled.has(ast.nodes[cons].type)) {
|
|
368
|
+
// `if (c()) ;` — an empty then-arm: both edges leave the header directly.
|
|
369
|
+
const exits0 = [{ from: p, kind: 'true' }];
|
|
370
|
+
if (alt !== undefined && rules.handled.has(ast.nodes[alt].type)) {
|
|
371
|
+
const af0 = lowerStmt(alt, ctx);
|
|
372
|
+
link(p, af0.entry, 'false');
|
|
373
|
+
exits0.push(...af0.exits);
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
exits0.push({ from: p, kind: 'false' });
|
|
377
|
+
}
|
|
378
|
+
return { entry: p, exits: exits0 };
|
|
379
|
+
}
|
|
215
380
|
const cf = lowerStmt(cons, ctx);
|
|
216
381
|
link(p, cf.entry, 'true');
|
|
217
382
|
const exits = [...cf.exits];
|
|
@@ -240,6 +405,17 @@ function cfgOf(ast, fn, source) {
|
|
|
240
405
|
const bodyIdx = (isPython || isGo)
|
|
241
406
|
? kids.of(i).filter((k) => ast.nodes[k].type === 'block').at(0)
|
|
242
407
|
: stmtChildren(i).at(-1);
|
|
408
|
+
// Found by the Java corpus, latent for TS too: `for (; ; ) ;` has NO named children —
|
|
409
|
+
// empty clauses and an empty-statement body. The loop is a bare spin: header with a
|
|
410
|
+
// self loop-back and only the (over-approximate) exhaustion exit.
|
|
411
|
+
if (bodyIdx === undefined || !rules.handled.has(ast.nodes[bodyIdx].type)) {
|
|
412
|
+
// Latent for TS, found by the Java corpus: an EMPTY body (`while (c()) ;`,
|
|
413
|
+
// `for (; ; ) ;`) leaves no statement child, so `.at(-1)` lands on the condition
|
|
414
|
+
// itself. The loop is a bare spin: header with a self loop-back.
|
|
415
|
+
const header = newPoint(i);
|
|
416
|
+
link(header, header, 'loop-back');
|
|
417
|
+
return { entry: header, exits: [{ from: header, kind: 'false' }] };
|
|
418
|
+
}
|
|
243
419
|
// @implements A-SPEC-519.1 — a bare Go `for {}` (no named non-block child) is INFINITE:
|
|
244
420
|
// no exhaustion edge. `for { select {…} }` is the daemon idiom, and a phantom false edge
|
|
245
421
|
// would hang spurious control dependence on everything after the loop. A for_clause with
|
|
@@ -273,6 +449,10 @@ function cfgOf(ast, fn, source) {
|
|
|
273
449
|
case 'do_statement': {
|
|
274
450
|
const header = newPoint(i); // condition point (the `do` node)
|
|
275
451
|
const bodyIdx = stmtChildren(i)[0];
|
|
452
|
+
if (bodyIdx === undefined || !rules.handled.has(ast.nodes[bodyIdx].type)) {
|
|
453
|
+
link(header, header, 'loop-back');
|
|
454
|
+
return { entry: header, exits: [{ from: header, kind: 'false' }] };
|
|
455
|
+
}
|
|
276
456
|
const myBreak = { label, target: -1 };
|
|
277
457
|
ctx.breakT.push({ label, owner: myBreak }, { label: null, owner: myBreak });
|
|
278
458
|
ctx.contT.push({ label, target: header }, { label: null, target: header });
|
|
@@ -368,6 +548,238 @@ function cfgOf(ast, fn, source) {
|
|
|
368
548
|
exits.push({ from: b.point, kind: 'break' });
|
|
369
549
|
return { entry: p, exits };
|
|
370
550
|
}
|
|
551
|
+
// @implements A-SPEC-520.1 — Java's enhanced for: the header owns the binding; body
|
|
552
|
+
// loops back; exhaustion exits false — same skeleton as Go's range.
|
|
553
|
+
case 'enhanced_for_statement':
|
|
554
|
+
case 'foreach_statement':
|
|
555
|
+
case 'for_range_loop': {
|
|
556
|
+
const header = newPoint(i);
|
|
557
|
+
const bodyIdx = kids.of(i).find((k) => ast.nodes[k].type === rules.block)
|
|
558
|
+
?? stmtChildren(i).at(-1);
|
|
559
|
+
const myBreak = { label, target: -1 };
|
|
560
|
+
ctx.breakT.push({ label, owner: myBreak }, { label: null, owner: myBreak });
|
|
561
|
+
ctx.contT.push({ label, target: header }, { label: null, target: header });
|
|
562
|
+
const bf = lowerStmt(bodyIdx, ctx);
|
|
563
|
+
ctx.breakT.splice(-2);
|
|
564
|
+
ctx.contT.splice(-2);
|
|
565
|
+
link(header, bf.entry, 'true');
|
|
566
|
+
for (const e of bf.exits)
|
|
567
|
+
link(e.from, header, 'loop-back');
|
|
568
|
+
const exits = [{ from: header, kind: 'false' }];
|
|
569
|
+
for (const b of claimBreaks(myBreak))
|
|
570
|
+
exits.push({ from: b.point, kind: 'break' });
|
|
571
|
+
return { entry: header, exits };
|
|
572
|
+
}
|
|
573
|
+
// @implements A-SPEC-520.1 — synchronized: a body wrapper for control flow (the lock is
|
|
574
|
+
// a use the DDG reads off the header point); lock semantics are declared out of scope.
|
|
575
|
+
case 'synchronized_statement':
|
|
576
|
+
case 'lock_statement':
|
|
577
|
+
case 'checked_statement': {
|
|
578
|
+
const p = newPoint(i);
|
|
579
|
+
const body = kids.of(i).find((k) => ast.nodes[k].type === 'block');
|
|
580
|
+
if (body === undefined)
|
|
581
|
+
throw new Unsupported(t + ' without a block');
|
|
582
|
+
const bf = lowerStmt(body, ctx);
|
|
583
|
+
link(p, bf.entry, 'seq');
|
|
584
|
+
return { entry: p, exits: bf.exits };
|
|
585
|
+
}
|
|
586
|
+
// @implements A-SPEC-521.1 — the preprocessor puts ALTERNATIVES in statement position.
|
|
587
|
+
// Sequencing the arms would fabricate flow no build configuration ever runs; each arm is
|
|
588
|
+
// a branch and both rejoin after #endif (a may-analysis over every configuration).
|
|
589
|
+
case 'preproc_if':
|
|
590
|
+
case 'preproc_ifdef': {
|
|
591
|
+
const p = newPoint(i);
|
|
592
|
+
const named = stmtChildren(i);
|
|
593
|
+
const elseArm = named.find((k) => ['preproc_else', 'preproc_elif'].includes(ast.nodes[k].type));
|
|
594
|
+
const ifStmts = named.filter((k) => rules.handled.has(ast.nodes[k].type));
|
|
595
|
+
const exits = [];
|
|
596
|
+
const f1 = lowerSeq(ifStmts, ctx);
|
|
597
|
+
link(p, f1.entry, 'true');
|
|
598
|
+
exits.push(...f1.exits);
|
|
599
|
+
if (elseArm !== undefined) {
|
|
600
|
+
if (ast.nodes[elseArm].type === 'preproc_elif')
|
|
601
|
+
throw new Unsupported('preproc_elif');
|
|
602
|
+
const elseStmts = stmtChildren(elseArm).filter((k) => rules.handled.has(ast.nodes[k].type));
|
|
603
|
+
const f2 = lowerSeq(elseStmts, ctx);
|
|
604
|
+
link(p, f2.entry, 'false');
|
|
605
|
+
exits.push(...f2.exits);
|
|
606
|
+
}
|
|
607
|
+
else {
|
|
608
|
+
exits.push({ from: p, kind: 'false' });
|
|
609
|
+
}
|
|
610
|
+
return { entry: p, exits };
|
|
611
|
+
}
|
|
612
|
+
// @implements A-SPEC-521.1 — using: a wrapper whose header owns the resource defs
|
|
613
|
+
// (variable_declarator does the DDG work); the implicit Dispose is covered by the
|
|
614
|
+
// enclosing try wiring, same posture as Java's try-with-resources.
|
|
615
|
+
case 'using_statement': {
|
|
616
|
+
const p = newPoint(i);
|
|
617
|
+
const body = kids.of(i).find((k) => ast.nodes[k].type === 'block')
|
|
618
|
+
?? stmtChildren(i).filter((k) => rules.handled.has(ast.nodes[k].type)).at(-1);
|
|
619
|
+
if (body === undefined)
|
|
620
|
+
return { entry: p, exits: [{ from: p, kind: 'seq' }] };
|
|
621
|
+
const bf = lowerStmt(body, ctx);
|
|
622
|
+
link(p, bf.entry, 'seq');
|
|
623
|
+
return { entry: p, exits: bf.exits };
|
|
624
|
+
}
|
|
625
|
+
// @implements A-SPEC-520.1 — ONE node, TWO styles. Old-style groups fall through into
|
|
626
|
+
// each other (TS switch semantics); arrow rules never touch (Go case semantics). A
|
|
627
|
+
// `yield` leaves the switch like a break-with-value. Statement position only — an
|
|
628
|
+
// expression-position switch stays inside its statement's point, like a ternary.
|
|
629
|
+
case 'switch_expression': {
|
|
630
|
+
const p = newPoint(i);
|
|
631
|
+
const myBreak = { label, target: -1 };
|
|
632
|
+
ctx.breakT.push({ label, owner: myBreak }, { label: null, owner: myBreak });
|
|
633
|
+
ctx.yieldT.push(myBreak);
|
|
634
|
+
const swBlock = kids.of(i).find((k) => ast.nodes[k].type === 'switch_block');
|
|
635
|
+
const members = swBlock === undefined ? [] : kids.of(swBlock).filter((k) => ['switch_block_statement_group', 'switch_rule'].includes(ast.nodes[k].type));
|
|
636
|
+
let sawDefault = false;
|
|
637
|
+
let prevFall = [];
|
|
638
|
+
const exits = [];
|
|
639
|
+
for (const m of members) {
|
|
640
|
+
const labelNode = kids.of(m).find((k) => ast.nodes[k].type === 'switch_label');
|
|
641
|
+
if (labelNode !== undefined && source !== undefined
|
|
642
|
+
&& source.slice(ast.nodes[labelNode].start, ast.nodes[labelNode].end).trim().startsWith('default'))
|
|
643
|
+
sawDefault = true;
|
|
644
|
+
const stmts = stmtChildren(m).filter((k) => ast.nodes[k].type !== 'switch_label');
|
|
645
|
+
const cf = lowerSeq(stmts, ctx);
|
|
646
|
+
link(p, cf.entry, 'case');
|
|
647
|
+
if (ast.nodes[m].type === 'switch_block_statement_group') {
|
|
648
|
+
for (const e of prevFall)
|
|
649
|
+
link(e.from, cf.entry, 'seq'); // implicit fallthrough
|
|
650
|
+
prevFall = cf.exits;
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
exits.push(...cf.exits); // arrow rule: isolated
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
exits.push(...prevFall);
|
|
657
|
+
if (!sawDefault)
|
|
658
|
+
exits.push({ from: p, kind: 'default' });
|
|
659
|
+
ctx.breakT.splice(-2);
|
|
660
|
+
ctx.yieldT.pop();
|
|
661
|
+
for (const b of claimBreaks(myBreak))
|
|
662
|
+
exits.push({ from: b.point, kind: 'break' });
|
|
663
|
+
return { entry: p, exits };
|
|
664
|
+
}
|
|
665
|
+
case 'yield_statement': {
|
|
666
|
+
const p = newPoint(i);
|
|
667
|
+
// @implements A-SPEC-521.1 — the SAME node name, two protocols. Java: a value-carrying
|
|
668
|
+
// switch escape. C#: the iterator protocol — `yield return v` (has a named child)
|
|
669
|
+
// CONTINUES executing; `yield break` (bare) leaves the function.
|
|
670
|
+
if (ast.lang === 'csharp') {
|
|
671
|
+
if (stmtChildren(i).length > 0)
|
|
672
|
+
return { entry: p, exits: [{ from: p, kind: 'seq' }] };
|
|
673
|
+
link(p, EXIT, 'return');
|
|
674
|
+
return { entry: p, exits: [] };
|
|
675
|
+
}
|
|
676
|
+
const owner = ctx.yieldT.at(-1);
|
|
677
|
+
if (owner === undefined)
|
|
678
|
+
throw new Unsupported('yield outside switch');
|
|
679
|
+
pendingBreaks.push({ point: p, owner });
|
|
680
|
+
return { entry: p, exits: [] };
|
|
681
|
+
}
|
|
682
|
+
// @implements A-SPEC-522.1 — Rust's if EXPRESSION: [cond, block, else_clause?[block|if]].
|
|
683
|
+
case 'if_expression': {
|
|
684
|
+
const p = newPoint(i);
|
|
685
|
+
rustQuestion(p, i === undefined ? i : kids.of(i)[0] ?? i);
|
|
686
|
+
const consR = kids.of(i).find((k) => ast.nodes[k].type === 'block');
|
|
687
|
+
const elseClause = kids.of(i).find((k) => ast.nodes[k].type === 'else_clause');
|
|
688
|
+
const altR = elseClause === undefined ? undefined
|
|
689
|
+
: kids.of(elseClause).find((k) => ['block', 'if_expression'].includes(ast.nodes[k].type));
|
|
690
|
+
const cf = lowerStmt(consR, ctx);
|
|
691
|
+
link(p, cf.entry, 'true');
|
|
692
|
+
const exits = [...cf.exits];
|
|
693
|
+
if (altR !== undefined) {
|
|
694
|
+
const af = lowerStmt(altR, ctx);
|
|
695
|
+
link(p, af.entry, 'false');
|
|
696
|
+
exits.push(...af.exits);
|
|
697
|
+
}
|
|
698
|
+
else {
|
|
699
|
+
exits.push({ from: p, kind: 'false' });
|
|
700
|
+
}
|
|
701
|
+
return { entry: p, exits };
|
|
702
|
+
}
|
|
703
|
+
// @implements A-SPEC-522.1 — match: arms isolated, EXHAUSTIVE (rustc enforces it, so a
|
|
704
|
+
// "no arm matched" escape would be flow no execution takes — the opposite of Python's
|
|
705
|
+
// match, deliberately). Arm values may be blocks or bare expressions.
|
|
706
|
+
case 'match_expression': {
|
|
707
|
+
const p = newPoint(i);
|
|
708
|
+
const mb = kids.of(i).find((k) => ast.nodes[k].type === 'match_block');
|
|
709
|
+
const exits = [];
|
|
710
|
+
for (const arm of mb === undefined ? [] : kids.of(mb)) {
|
|
711
|
+
if (ast.nodes[arm].type !== 'match_arm')
|
|
712
|
+
continue;
|
|
713
|
+
const val = kids.of(arm).filter((k) => ast.nodes[k].named && ast.nodes[k].type !== 'match_pattern').at(-1);
|
|
714
|
+
if (val === undefined)
|
|
715
|
+
continue;
|
|
716
|
+
const vf = rules.handled.has(ast.nodes[val].type)
|
|
717
|
+
? lowerStmt(val, ctx)
|
|
718
|
+
: (() => { const vp = newPoint(val); rustQuestion(vp, val); return { entry: vp, exits: [{ from: vp, kind: 'seq' }] }; })();
|
|
719
|
+
link(p, vf.entry, 'case');
|
|
720
|
+
exits.push(...vf.exits);
|
|
721
|
+
}
|
|
722
|
+
return { entry: p, exits };
|
|
723
|
+
}
|
|
724
|
+
// @implements A-SPEC-522.1 — for/while/loop expressions ride the loop skeleton; a bare
|
|
725
|
+
// `loop` is INFINITE (no exhaustion edge — Go's bare-for call, same reasoning).
|
|
726
|
+
case 'for_expression':
|
|
727
|
+
case 'while_expression':
|
|
728
|
+
case 'loop_expression': {
|
|
729
|
+
const header = newPoint(i);
|
|
730
|
+
rustQuestion(header, i);
|
|
731
|
+
const bodyR = kids.of(i).find((k) => ast.nodes[k].type === 'block');
|
|
732
|
+
const myBreak = { label, target: -1 };
|
|
733
|
+
const loopLabel = (() => {
|
|
734
|
+
const ln = kids.of(i).find((k) => ast.nodes[k].type === 'label');
|
|
735
|
+
return ln !== undefined && source !== undefined
|
|
736
|
+
? source.slice(ast.nodes[ln].start, ast.nodes[ln].end) : label;
|
|
737
|
+
})();
|
|
738
|
+
const owner = { label: loopLabel, target: -1 };
|
|
739
|
+
ctx.breakT.push({ label: loopLabel, owner }, { label: null, owner });
|
|
740
|
+
ctx.contT.push({ label: loopLabel, target: header }, { label: null, target: header });
|
|
741
|
+
const bf = lowerStmt(bodyR, ctx);
|
|
742
|
+
ctx.breakT.splice(-2);
|
|
743
|
+
ctx.contT.splice(-2);
|
|
744
|
+
link(header, bf.entry, 'true');
|
|
745
|
+
for (const e of bf.exits)
|
|
746
|
+
link(e.from, header, 'loop-back');
|
|
747
|
+
const exits = [];
|
|
748
|
+
if (t !== 'loop_expression')
|
|
749
|
+
exits.push({ from: header, kind: 'false' });
|
|
750
|
+
for (const b of claimBreaks(owner))
|
|
751
|
+
exits.push({ from: b.point, kind: 'break' });
|
|
752
|
+
void myBreak;
|
|
753
|
+
return { entry: header, exits };
|
|
754
|
+
}
|
|
755
|
+
case 'return_expression': {
|
|
756
|
+
const p = newPoint(i);
|
|
757
|
+
rustQuestion(p, i);
|
|
758
|
+
link(p, EXIT, 'return');
|
|
759
|
+
return { entry: p, exits: [] };
|
|
760
|
+
}
|
|
761
|
+
case 'break_expression': {
|
|
762
|
+
const p = newPoint(i);
|
|
763
|
+
const ln = kids.of(i).find((k) => ast.nodes[k].type === 'label');
|
|
764
|
+
const lbl = ln !== undefined && source !== undefined
|
|
765
|
+
? source.slice(ast.nodes[ln].start, ast.nodes[ln].end) : null;
|
|
766
|
+
const bentry = nearestBreak(ctx.breakT, lbl);
|
|
767
|
+
if (!bentry)
|
|
768
|
+
throw new Unsupported('break outside breakable');
|
|
769
|
+
pendingBreaks.push({ point: p, owner: bentry.owner });
|
|
770
|
+
return { entry: p, exits: [] };
|
|
771
|
+
}
|
|
772
|
+
case 'continue_expression': {
|
|
773
|
+
const p = newPoint(i);
|
|
774
|
+
const ln = kids.of(i).find((k) => ast.nodes[k].type === 'label');
|
|
775
|
+
const lbl = ln !== undefined && source !== undefined
|
|
776
|
+
? source.slice(ast.nodes[ln].start, ast.nodes[ln].end) : null;
|
|
777
|
+
const centry = nearestCont(ctx.contT, lbl);
|
|
778
|
+
if (!centry || centry.target < 0)
|
|
779
|
+
throw new Unsupported('continue outside loop');
|
|
780
|
+
link(p, centry.target, 'continue');
|
|
781
|
+
return { entry: p, exits: [] };
|
|
782
|
+
}
|
|
371
783
|
// @implements A-SPEC-519.1 — select: each communication case is a branch off the header;
|
|
372
784
|
// WITHOUT a default the statement BLOCKS until some case fires, so there is no "no arm
|
|
373
785
|
// matched" escape (the opposite of Python's match).
|
|
@@ -420,6 +832,67 @@ function cfgOf(ast, fn, source) {
|
|
|
420
832
|
return { entry: p, exits: [] };
|
|
421
833
|
}
|
|
422
834
|
case 'switch_statement': {
|
|
835
|
+
// @implements A-SPEC-521.1 — C# shares the node NAME with TS and inverts the
|
|
836
|
+
// semantics: implicit fallthrough is a compile error, so sections are ISOLATED.
|
|
837
|
+
// (The suite caught the first attempt — a duplicate case label silently shadowed the
|
|
838
|
+
// TS lowering and broke its fallthrough pin.)
|
|
839
|
+
if (ast.lang === 'csharp') {
|
|
840
|
+
const p = newPoint(i);
|
|
841
|
+
const myBreak = { label, target: -1 };
|
|
842
|
+
ctx.breakT.push({ label, owner: myBreak }, { label: null, owner: myBreak });
|
|
843
|
+
const swBody = kids.of(i).find((k) => ast.nodes[k].type === 'switch_body');
|
|
844
|
+
let sawDefault = false;
|
|
845
|
+
const exits = [];
|
|
846
|
+
for (const sec of swBody === undefined ? [] : kids.of(swBody)) {
|
|
847
|
+
if (ast.nodes[sec].type !== 'switch_section')
|
|
848
|
+
continue;
|
|
849
|
+
const children = stmtChildren(sec);
|
|
850
|
+
const stmts = children.filter((k) => rules.handled.has(ast.nodes[k].type));
|
|
851
|
+
const hasPattern = children.some((k) => ast.nodes[k].type.includes('pattern'));
|
|
852
|
+
if (!hasPattern)
|
|
853
|
+
sawDefault = true; // a default section carries no pattern
|
|
854
|
+
const cf = lowerSeq(stmts, ctx);
|
|
855
|
+
link(p, cf.entry, hasPattern ? 'case' : 'default');
|
|
856
|
+
exits.push(...cf.exits); // isolated: nothing flows to a sibling
|
|
857
|
+
}
|
|
858
|
+
if (!sawDefault)
|
|
859
|
+
exits.push({ from: p, kind: 'default' });
|
|
860
|
+
ctx.breakT.splice(-2);
|
|
861
|
+
for (const b of claimBreaks(myBreak))
|
|
862
|
+
exits.push({ from: b.point, kind: 'break' });
|
|
863
|
+
return { entry: p, exits };
|
|
864
|
+
}
|
|
865
|
+
// @implements A-SPEC-523.1 — C++: flat case_statement children inside a
|
|
866
|
+
// compound_statement, with C's implicit fallthrough (a value-less case is default).
|
|
867
|
+
if (ast.lang === 'cpp') {
|
|
868
|
+
const p = newPoint(i);
|
|
869
|
+
const myBreak = { label, target: -1 };
|
|
870
|
+
ctx.breakT.push({ label, owner: myBreak }, { label: null, owner: myBreak });
|
|
871
|
+
const swBody = kids.of(i).find((k) => ast.nodes[k].type === 'compound_statement');
|
|
872
|
+
let sawDefault = false;
|
|
873
|
+
let prevFall = [];
|
|
874
|
+
const exits = [];
|
|
875
|
+
for (const c of swBody === undefined ? [] : kids.of(swBody)) {
|
|
876
|
+
if (ast.nodes[c].type !== 'case_statement')
|
|
877
|
+
continue;
|
|
878
|
+
const children = stmtChildren(c);
|
|
879
|
+
const stmts = children.filter((k) => rules.handled.has(ast.nodes[k].type));
|
|
880
|
+
if (stmts.length === children.length)
|
|
881
|
+
sawDefault = true; // no label value → default
|
|
882
|
+
const cf = lowerSeq(stmts, ctx);
|
|
883
|
+
link(p, cf.entry, stmts.length === children.length ? 'default' : 'case');
|
|
884
|
+
for (const e of prevFall)
|
|
885
|
+
link(e.from, cf.entry, 'seq'); // C fallthrough
|
|
886
|
+
prevFall = cf.exits;
|
|
887
|
+
}
|
|
888
|
+
exits.push(...prevFall);
|
|
889
|
+
if (!sawDefault)
|
|
890
|
+
exits.push({ from: p, kind: 'default' });
|
|
891
|
+
ctx.breakT.splice(-2);
|
|
892
|
+
for (const b of claimBreaks(myBreak))
|
|
893
|
+
exits.push({ from: b.point, kind: 'break' });
|
|
894
|
+
return { entry: p, exits };
|
|
895
|
+
}
|
|
423
896
|
const p = newPoint(i);
|
|
424
897
|
const body = stmtChildren(i).at(-1); // switch_body
|
|
425
898
|
const myBreak = { label, target: -1 };
|
|
@@ -478,13 +951,17 @@ function cfgOf(ast, fn, source) {
|
|
|
478
951
|
link(p, ctx.catchT.at(-1) ?? EXIT, 'throw');
|
|
479
952
|
return { entry: p, exits: [] };
|
|
480
953
|
}
|
|
954
|
+
// @implements A-SPEC-520.1 — try-with-resources joins the try case: same clause wiring,
|
|
955
|
+
// plus a point for the resource_specification so its defs (`var r = open()`) have a home.
|
|
956
|
+
case 'try_with_resources_statement':
|
|
481
957
|
case 'try_statement': {
|
|
482
958
|
const named = stmtChildren(i);
|
|
483
959
|
// @implements A-SPEC-510.7 — Python spells the same shape with `block` / `except_clause`
|
|
484
960
|
// / `finally_clause`, PLUS an `else_clause` that runs ONLY when no exception was raised.
|
|
961
|
+
// @implements A-SPEC-520.1 — Java spells the try body `block`; TS `statement_block`.
|
|
485
962
|
const body = isPython
|
|
486
963
|
? kids.of(i).find((k) => ast.nodes[k].type === 'block')
|
|
487
|
-
: named.find((k) => ast.nodes[k].type
|
|
964
|
+
: named.find((k) => ['statement_block', 'block', 'compound_statement'].includes(ast.nodes[k].type));
|
|
488
965
|
const handler = isPython
|
|
489
966
|
? kids.of(i).find((k) => ast.nodes[k].type === 'except_clause')
|
|
490
967
|
: named.find((k) => ast.nodes[k].type === 'catch_clause');
|
|
@@ -538,6 +1015,12 @@ function cfgOf(ast, fn, source) {
|
|
|
538
1015
|
else {
|
|
539
1016
|
exits.push(...after);
|
|
540
1017
|
}
|
|
1018
|
+
const resSpec = kids.of(i).find((k) => ast.nodes[k].type === 'resource_specification');
|
|
1019
|
+
if (resSpec !== undefined) {
|
|
1020
|
+
const rp = newPoint(resSpec);
|
|
1021
|
+
link(rp, bf.entry, 'seq');
|
|
1022
|
+
return { entry: rp, exits };
|
|
1023
|
+
}
|
|
541
1024
|
return { entry: bf.entry, exits };
|
|
542
1025
|
}
|
|
543
1026
|
default:
|
|
@@ -569,7 +1052,7 @@ function cfgOf(ast, fn, source) {
|
|
|
569
1052
|
}
|
|
570
1053
|
return undefined;
|
|
571
1054
|
};
|
|
572
|
-
const ctx = { breakT: [], contT: [], catchT: [], pendingLabel: null };
|
|
1055
|
+
const ctx = { breakT: [], contT: [], catchT: [], yieldT: [], pendingLabel: null };
|
|
573
1056
|
const bodyNode = ast.nodes[fn.bodyIndex];
|
|
574
1057
|
const flow = bodyNode.type === rules.block
|
|
575
1058
|
? lowerSeq(stmtChildren(fn.bodyIndex), ctx)
|