@holmes-lab/holmes-kit 0.5.0 → 0.6.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 +35 -0
- package/dist/.build-id +1 -1
- package/dist/holmes/cpg/foundation/cfg.js +434 -4
- package/dist/holmes/cpg/foundation/ddg.js +179 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,41 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
<!-- @implements A-SPEC-209 -->
|
|
8
|
+
## [0.6.0] - 2026-09-02
|
|
9
|
+
|
|
10
|
+
Three more languages earn corpus-validated control- and data-flow: Java, C#, and Rust join
|
|
11
|
+
TypeScript, Python and Go. Six of seven official languages now build CFG/DDG/CDG with zero
|
|
12
|
+
invariant violations over real open-source corpora. Every grade in the matrix is a measurement.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **Java CFG/DDG/CDG** (REQ-520): rides the TS chassis (same probed shapes for if/while/for/do
|
|
17
|
+
and the try clauses) plus the four seams where Java's semantics live: ONE switch node carrying
|
|
18
|
+
two styles (old groups fall through, arrow rules never touch), try-with-resources with resource
|
|
19
|
+
defs, catch parameters inside catch_formal_parameter, and field/method names excluded from
|
|
20
|
+
uses (`this.x = x` reads x once). **Measured over gson+junit4: 8,376/8,376 functions, zero
|
|
21
|
+
refusals, zero violations, 19,818 REACHING_DEF + 7,622 CDG edges.** The corpus caught three
|
|
22
|
+
would-have-shipped defects — named comment nodes, empty bodies (`while (c()) ;` — latent for
|
|
23
|
+
TS too), and Java's wrapperless else that lowered an else-if CONDITION as a statement.
|
|
24
|
+
- **C# CFG/DDG/CDG** (REQ-521): same face as Java, opposite semantics where it hurts — switch
|
|
25
|
+
fallthrough is a compile error (sections isolate), `yield` is the iterator protocol
|
|
26
|
+
(yield return continues, yield break exits), and `#if/#else` arms are ALTERNATIVES lowered as
|
|
27
|
+
branches. **Measured over Newtonsoft.Json+RestSharp: 9,184 functions, zero violations; the
|
|
28
|
+
only refusals are designed (preproc_elif 3, goto 1); 51 files (4.4%) refuse honestly at L1
|
|
29
|
+
where conditional compilation crosses syntax.** One sealed design was corrected mid-slice and
|
|
30
|
+
re-sealed: a declaration pattern binds at the switch HEADER, because a def on the point that
|
|
31
|
+
uses it can never reach.
|
|
32
|
+
- **Rust CFG/DDG/CDG** (REQ-522): control flow is an EXPRESSION in Rust (measured: if 1,170 /
|
|
33
|
+
match 468 / for 307 all inside expression_statement), which creates a new failure mode — a
|
|
34
|
+
zero-violation but HOLLOW graph. The corpus gate therefore demands branch edges exist, not
|
|
35
|
+
just that invariants hold. Match is exhaustive (no phantom escape — rustc enforces it), `?` is
|
|
36
|
+
an implicit early return (both paths leave the point), bare `loop` is infinite, let-else's
|
|
37
|
+
else arm diverges, and pattern bindings follow the lowercase-initial convention (documented
|
|
38
|
+
heuristic). **Measured over ripgrep+serde: 5,247/5,247 functions, zero refusals, zero
|
|
39
|
+
violations, 4,786 branch edges (avg 5.6 blocks/function), 13,647 REACHING_DEF + 6,808 CDG
|
|
40
|
+
edges.**
|
|
41
|
+
|
|
7
42
|
<!-- @implements A-SPEC-209 -->
|
|
8
43
|
## [0.5.0] - 2026-09-02
|
|
9
44
|
|
package/dist/.build-id
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
814d021-mtjewubg
|
|
@@ -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,83 @@ 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
|
+
};
|
|
72
154
|
const LANG_RULES = {
|
|
73
|
-
typescript: TS_RULES, tsx: TS_RULES, python: PY_RULES, go: GO_RULES,
|
|
155
|
+
typescript: TS_RULES, tsx: TS_RULES, python: PY_RULES, go: GO_RULES, java: JAVA_RULES,
|
|
156
|
+
csharp: CSHARP_RULES, rust: RUST_RULES,
|
|
74
157
|
};
|
|
75
158
|
// @implements A-SPEC-510.6
|
|
76
159
|
/**
|
|
@@ -115,6 +198,7 @@ function cfgOf(ast, fn, source) {
|
|
|
115
198
|
const rules = LANG_RULES[ast.lang];
|
|
116
199
|
const isPython = ast.lang === 'python';
|
|
117
200
|
const isGo = ast.lang === 'go';
|
|
201
|
+
const isRust = ast.lang === 'rust';
|
|
118
202
|
const kids = childrenIndex(ast);
|
|
119
203
|
// Points: one per lowered statement (block ids after coalescing). 0=ENTRY, 1=EXIT.
|
|
120
204
|
const stmtsOf = [[], []];
|
|
@@ -155,7 +239,9 @@ function cfgOf(ast, fn, source) {
|
|
|
155
239
|
}
|
|
156
240
|
return { entry, exits };
|
|
157
241
|
};
|
|
158
|
-
const stmtChildren = (i) =>
|
|
242
|
+
const stmtChildren = (i) =>
|
|
243
|
+
// `comment` (TS/PY/Go) and Java's line_comment/block_comment are all named nodes.
|
|
244
|
+
kids.of(i).filter((k) => ast.nodes[k].named && !ast.nodes[k].type.includes('comment'));
|
|
159
245
|
const wireException = (first, targetProvider) => {
|
|
160
246
|
// Conservative: every point created inside [first, now) can throw into the handler.
|
|
161
247
|
const target = targetProvider();
|
|
@@ -168,8 +254,47 @@ function cfgOf(ast, fn, source) {
|
|
|
168
254
|
throw new Unsupported(t);
|
|
169
255
|
const label = ctx.pendingLabel;
|
|
170
256
|
ctx.pendingLabel = null;
|
|
257
|
+
// @implements A-SPEC-522.1 — Rust control lives in expressions. A statement whose sole
|
|
258
|
+
// named child is a control expression lowers as that control (flattening it makes the
|
|
259
|
+
// hollow graph); a `?` anywhere in a point's subtree adds the implicit early-return edge;
|
|
260
|
+
// a let-else is a branch whose false arm is the (compiler-enforced) diverging else block.
|
|
261
|
+
const RUST_CONTROL = new Set(['if_expression', 'match_expression', 'for_expression', 'while_expression', 'loop_expression', 'return_expression', 'break_expression', 'continue_expression']);
|
|
262
|
+
if (isRust && t === 'expression_statement') {
|
|
263
|
+
const inner = stmtChildren(i);
|
|
264
|
+
if (inner.length === 1 && RUST_CONTROL.has(ast.nodes[inner[0]].type))
|
|
265
|
+
return lowerStmt(inner[0], ctx);
|
|
266
|
+
}
|
|
267
|
+
const rustQuestion = (p, root) => {
|
|
268
|
+
if (!isRust)
|
|
269
|
+
return;
|
|
270
|
+
const stack = [root];
|
|
271
|
+
while (stack.length) {
|
|
272
|
+
const n = stack.pop();
|
|
273
|
+
if (n !== root && exports.FUNCTION_TYPES.has(ast.nodes[n].type))
|
|
274
|
+
continue; // closures own their `?`
|
|
275
|
+
if (ast.nodes[n].type === 'try_expression') {
|
|
276
|
+
link(p, ctx.catchT.at(-1) ?? EXIT, 'throw');
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
for (const c of kids.of(n))
|
|
280
|
+
stack.push(c);
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
if (isRust && t === 'let_declaration') {
|
|
284
|
+
const elseBlock = kids.of(i).some((k) => !ast.nodes[k].named && source !== undefined
|
|
285
|
+
&& source.slice(ast.nodes[k].start, ast.nodes[k].end) === 'else')
|
|
286
|
+
? kids.of(i).filter((k) => ast.nodes[k].type === 'block').at(-1) : undefined;
|
|
287
|
+
if (elseBlock !== undefined) {
|
|
288
|
+
const p = newPoint(i);
|
|
289
|
+
rustQuestion(p, i);
|
|
290
|
+
const ef = lowerStmt(elseBlock, ctx);
|
|
291
|
+
link(p, ef.entry, 'false');
|
|
292
|
+
return { entry: p, exits: [{ from: p, kind: 'true' }, ...ef.exits] };
|
|
293
|
+
}
|
|
294
|
+
}
|
|
171
295
|
if (rules.simple.has(t)) {
|
|
172
296
|
const p = newPoint(i);
|
|
297
|
+
rustQuestion(p, i);
|
|
173
298
|
return { entry: p, exits: [{ from: p, kind: 'seq' }] };
|
|
174
299
|
}
|
|
175
300
|
switch (t) {
|
|
@@ -209,9 +334,29 @@ function cfgOf(ast, fn, source) {
|
|
|
209
334
|
: stmtChildren(clause).find((k) => ast.nodes[k].type === 'block');
|
|
210
335
|
}
|
|
211
336
|
}
|
|
337
|
+
else if (ast.lang === 'java' || ast.lang === 'csharp') {
|
|
338
|
+
// @implements A-SPEC-520.1 / A-SPEC-521.1 — Java and C# have NO else_clause
|
|
339
|
+
// wrapper: named[2] IS the
|
|
340
|
+
// alternative (a block, or the next if_statement of an else-if chain). Unwrapping it
|
|
341
|
+
// like TS lowered an else-if's CONDITION as a statement — the corpus caught it.
|
|
342
|
+
alt = named[2];
|
|
343
|
+
}
|
|
212
344
|
else {
|
|
213
345
|
alt = named[2] !== undefined ? stmtChildren(named[2])[0] : undefined;
|
|
214
346
|
}
|
|
347
|
+
if (cons === undefined || !rules.handled.has(ast.nodes[cons].type)) {
|
|
348
|
+
// `if (c()) ;` — an empty then-arm: both edges leave the header directly.
|
|
349
|
+
const exits0 = [{ from: p, kind: 'true' }];
|
|
350
|
+
if (alt !== undefined && rules.handled.has(ast.nodes[alt].type)) {
|
|
351
|
+
const af0 = lowerStmt(alt, ctx);
|
|
352
|
+
link(p, af0.entry, 'false');
|
|
353
|
+
exits0.push(...af0.exits);
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
exits0.push({ from: p, kind: 'false' });
|
|
357
|
+
}
|
|
358
|
+
return { entry: p, exits: exits0 };
|
|
359
|
+
}
|
|
215
360
|
const cf = lowerStmt(cons, ctx);
|
|
216
361
|
link(p, cf.entry, 'true');
|
|
217
362
|
const exits = [...cf.exits];
|
|
@@ -240,6 +385,17 @@ function cfgOf(ast, fn, source) {
|
|
|
240
385
|
const bodyIdx = (isPython || isGo)
|
|
241
386
|
? kids.of(i).filter((k) => ast.nodes[k].type === 'block').at(0)
|
|
242
387
|
: stmtChildren(i).at(-1);
|
|
388
|
+
// Found by the Java corpus, latent for TS too: `for (; ; ) ;` has NO named children —
|
|
389
|
+
// empty clauses and an empty-statement body. The loop is a bare spin: header with a
|
|
390
|
+
// self loop-back and only the (over-approximate) exhaustion exit.
|
|
391
|
+
if (bodyIdx === undefined || !rules.handled.has(ast.nodes[bodyIdx].type)) {
|
|
392
|
+
// Latent for TS, found by the Java corpus: an EMPTY body (`while (c()) ;`,
|
|
393
|
+
// `for (; ; ) ;`) leaves no statement child, so `.at(-1)` lands on the condition
|
|
394
|
+
// itself. The loop is a bare spin: header with a self loop-back.
|
|
395
|
+
const header = newPoint(i);
|
|
396
|
+
link(header, header, 'loop-back');
|
|
397
|
+
return { entry: header, exits: [{ from: header, kind: 'false' }] };
|
|
398
|
+
}
|
|
243
399
|
// @implements A-SPEC-519.1 — a bare Go `for {}` (no named non-block child) is INFINITE:
|
|
244
400
|
// no exhaustion edge. `for { select {…} }` is the daemon idiom, and a phantom false edge
|
|
245
401
|
// would hang spurious control dependence on everything after the loop. A for_clause with
|
|
@@ -273,6 +429,10 @@ function cfgOf(ast, fn, source) {
|
|
|
273
429
|
case 'do_statement': {
|
|
274
430
|
const header = newPoint(i); // condition point (the `do` node)
|
|
275
431
|
const bodyIdx = stmtChildren(i)[0];
|
|
432
|
+
if (bodyIdx === undefined || !rules.handled.has(ast.nodes[bodyIdx].type)) {
|
|
433
|
+
link(header, header, 'loop-back');
|
|
434
|
+
return { entry: header, exits: [{ from: header, kind: 'false' }] };
|
|
435
|
+
}
|
|
276
436
|
const myBreak = { label, target: -1 };
|
|
277
437
|
ctx.breakT.push({ label, owner: myBreak }, { label: null, owner: myBreak });
|
|
278
438
|
ctx.contT.push({ label, target: header }, { label: null, target: header });
|
|
@@ -368,6 +528,236 @@ function cfgOf(ast, fn, source) {
|
|
|
368
528
|
exits.push({ from: b.point, kind: 'break' });
|
|
369
529
|
return { entry: p, exits };
|
|
370
530
|
}
|
|
531
|
+
// @implements A-SPEC-520.1 — Java's enhanced for: the header owns the binding; body
|
|
532
|
+
// loops back; exhaustion exits false — same skeleton as Go's range.
|
|
533
|
+
case 'enhanced_for_statement':
|
|
534
|
+
case 'foreach_statement': {
|
|
535
|
+
const header = newPoint(i);
|
|
536
|
+
const bodyIdx = kids.of(i).find((k) => ast.nodes[k].type === 'block')
|
|
537
|
+
?? stmtChildren(i).at(-1);
|
|
538
|
+
const myBreak = { label, target: -1 };
|
|
539
|
+
ctx.breakT.push({ label, owner: myBreak }, { label: null, owner: myBreak });
|
|
540
|
+
ctx.contT.push({ label, target: header }, { label: null, target: header });
|
|
541
|
+
const bf = lowerStmt(bodyIdx, ctx);
|
|
542
|
+
ctx.breakT.splice(-2);
|
|
543
|
+
ctx.contT.splice(-2);
|
|
544
|
+
link(header, bf.entry, 'true');
|
|
545
|
+
for (const e of bf.exits)
|
|
546
|
+
link(e.from, header, 'loop-back');
|
|
547
|
+
const exits = [{ from: header, kind: 'false' }];
|
|
548
|
+
for (const b of claimBreaks(myBreak))
|
|
549
|
+
exits.push({ from: b.point, kind: 'break' });
|
|
550
|
+
return { entry: header, exits };
|
|
551
|
+
}
|
|
552
|
+
// @implements A-SPEC-520.1 — synchronized: a body wrapper for control flow (the lock is
|
|
553
|
+
// a use the DDG reads off the header point); lock semantics are declared out of scope.
|
|
554
|
+
case 'synchronized_statement':
|
|
555
|
+
case 'lock_statement':
|
|
556
|
+
case 'checked_statement': {
|
|
557
|
+
const p = newPoint(i);
|
|
558
|
+
const body = kids.of(i).find((k) => ast.nodes[k].type === 'block');
|
|
559
|
+
if (body === undefined)
|
|
560
|
+
throw new Unsupported(t + ' without a block');
|
|
561
|
+
const bf = lowerStmt(body, ctx);
|
|
562
|
+
link(p, bf.entry, 'seq');
|
|
563
|
+
return { entry: p, exits: bf.exits };
|
|
564
|
+
}
|
|
565
|
+
// @implements A-SPEC-521.1 — the preprocessor puts ALTERNATIVES in statement position.
|
|
566
|
+
// Sequencing the arms would fabricate flow no build configuration ever runs; each arm is
|
|
567
|
+
// a branch and both rejoin after #endif (a may-analysis over every configuration).
|
|
568
|
+
case 'preproc_if': {
|
|
569
|
+
const p = newPoint(i);
|
|
570
|
+
const named = stmtChildren(i);
|
|
571
|
+
const elseArm = named.find((k) => ['preproc_else', 'preproc_elif'].includes(ast.nodes[k].type));
|
|
572
|
+
const ifStmts = named.filter((k) => rules.handled.has(ast.nodes[k].type));
|
|
573
|
+
const exits = [];
|
|
574
|
+
const f1 = lowerSeq(ifStmts, ctx);
|
|
575
|
+
link(p, f1.entry, 'true');
|
|
576
|
+
exits.push(...f1.exits);
|
|
577
|
+
if (elseArm !== undefined) {
|
|
578
|
+
if (ast.nodes[elseArm].type === 'preproc_elif')
|
|
579
|
+
throw new Unsupported('preproc_elif');
|
|
580
|
+
const elseStmts = stmtChildren(elseArm).filter((k) => rules.handled.has(ast.nodes[k].type));
|
|
581
|
+
const f2 = lowerSeq(elseStmts, ctx);
|
|
582
|
+
link(p, f2.entry, 'false');
|
|
583
|
+
exits.push(...f2.exits);
|
|
584
|
+
}
|
|
585
|
+
else {
|
|
586
|
+
exits.push({ from: p, kind: 'false' });
|
|
587
|
+
}
|
|
588
|
+
return { entry: p, exits };
|
|
589
|
+
}
|
|
590
|
+
// @implements A-SPEC-521.1 — using: a wrapper whose header owns the resource defs
|
|
591
|
+
// (variable_declarator does the DDG work); the implicit Dispose is covered by the
|
|
592
|
+
// enclosing try wiring, same posture as Java's try-with-resources.
|
|
593
|
+
case 'using_statement': {
|
|
594
|
+
const p = newPoint(i);
|
|
595
|
+
const body = kids.of(i).find((k) => ast.nodes[k].type === 'block')
|
|
596
|
+
?? stmtChildren(i).filter((k) => rules.handled.has(ast.nodes[k].type)).at(-1);
|
|
597
|
+
if (body === undefined)
|
|
598
|
+
return { entry: p, exits: [{ from: p, kind: 'seq' }] };
|
|
599
|
+
const bf = lowerStmt(body, ctx);
|
|
600
|
+
link(p, bf.entry, 'seq');
|
|
601
|
+
return { entry: p, exits: bf.exits };
|
|
602
|
+
}
|
|
603
|
+
// @implements A-SPEC-520.1 — ONE node, TWO styles. Old-style groups fall through into
|
|
604
|
+
// each other (TS switch semantics); arrow rules never touch (Go case semantics). A
|
|
605
|
+
// `yield` leaves the switch like a break-with-value. Statement position only — an
|
|
606
|
+
// expression-position switch stays inside its statement's point, like a ternary.
|
|
607
|
+
case 'switch_expression': {
|
|
608
|
+
const p = newPoint(i);
|
|
609
|
+
const myBreak = { label, target: -1 };
|
|
610
|
+
ctx.breakT.push({ label, owner: myBreak }, { label: null, owner: myBreak });
|
|
611
|
+
ctx.yieldT.push(myBreak);
|
|
612
|
+
const swBlock = kids.of(i).find((k) => ast.nodes[k].type === 'switch_block');
|
|
613
|
+
const members = swBlock === undefined ? [] : kids.of(swBlock).filter((k) => ['switch_block_statement_group', 'switch_rule'].includes(ast.nodes[k].type));
|
|
614
|
+
let sawDefault = false;
|
|
615
|
+
let prevFall = [];
|
|
616
|
+
const exits = [];
|
|
617
|
+
for (const m of members) {
|
|
618
|
+
const labelNode = kids.of(m).find((k) => ast.nodes[k].type === 'switch_label');
|
|
619
|
+
if (labelNode !== undefined && source !== undefined
|
|
620
|
+
&& source.slice(ast.nodes[labelNode].start, ast.nodes[labelNode].end).trim().startsWith('default'))
|
|
621
|
+
sawDefault = true;
|
|
622
|
+
const stmts = stmtChildren(m).filter((k) => ast.nodes[k].type !== 'switch_label');
|
|
623
|
+
const cf = lowerSeq(stmts, ctx);
|
|
624
|
+
link(p, cf.entry, 'case');
|
|
625
|
+
if (ast.nodes[m].type === 'switch_block_statement_group') {
|
|
626
|
+
for (const e of prevFall)
|
|
627
|
+
link(e.from, cf.entry, 'seq'); // implicit fallthrough
|
|
628
|
+
prevFall = cf.exits;
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
exits.push(...cf.exits); // arrow rule: isolated
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
exits.push(...prevFall);
|
|
635
|
+
if (!sawDefault)
|
|
636
|
+
exits.push({ from: p, kind: 'default' });
|
|
637
|
+
ctx.breakT.splice(-2);
|
|
638
|
+
ctx.yieldT.pop();
|
|
639
|
+
for (const b of claimBreaks(myBreak))
|
|
640
|
+
exits.push({ from: b.point, kind: 'break' });
|
|
641
|
+
return { entry: p, exits };
|
|
642
|
+
}
|
|
643
|
+
case 'yield_statement': {
|
|
644
|
+
const p = newPoint(i);
|
|
645
|
+
// @implements A-SPEC-521.1 — the SAME node name, two protocols. Java: a value-carrying
|
|
646
|
+
// switch escape. C#: the iterator protocol — `yield return v` (has a named child)
|
|
647
|
+
// CONTINUES executing; `yield break` (bare) leaves the function.
|
|
648
|
+
if (ast.lang === 'csharp') {
|
|
649
|
+
if (stmtChildren(i).length > 0)
|
|
650
|
+
return { entry: p, exits: [{ from: p, kind: 'seq' }] };
|
|
651
|
+
link(p, EXIT, 'return');
|
|
652
|
+
return { entry: p, exits: [] };
|
|
653
|
+
}
|
|
654
|
+
const owner = ctx.yieldT.at(-1);
|
|
655
|
+
if (owner === undefined)
|
|
656
|
+
throw new Unsupported('yield outside switch');
|
|
657
|
+
pendingBreaks.push({ point: p, owner });
|
|
658
|
+
return { entry: p, exits: [] };
|
|
659
|
+
}
|
|
660
|
+
// @implements A-SPEC-522.1 — Rust's if EXPRESSION: [cond, block, else_clause?[block|if]].
|
|
661
|
+
case 'if_expression': {
|
|
662
|
+
const p = newPoint(i);
|
|
663
|
+
rustQuestion(p, i === undefined ? i : kids.of(i)[0] ?? i);
|
|
664
|
+
const consR = kids.of(i).find((k) => ast.nodes[k].type === 'block');
|
|
665
|
+
const elseClause = kids.of(i).find((k) => ast.nodes[k].type === 'else_clause');
|
|
666
|
+
const altR = elseClause === undefined ? undefined
|
|
667
|
+
: kids.of(elseClause).find((k) => ['block', 'if_expression'].includes(ast.nodes[k].type));
|
|
668
|
+
const cf = lowerStmt(consR, ctx);
|
|
669
|
+
link(p, cf.entry, 'true');
|
|
670
|
+
const exits = [...cf.exits];
|
|
671
|
+
if (altR !== undefined) {
|
|
672
|
+
const af = lowerStmt(altR, ctx);
|
|
673
|
+
link(p, af.entry, 'false');
|
|
674
|
+
exits.push(...af.exits);
|
|
675
|
+
}
|
|
676
|
+
else {
|
|
677
|
+
exits.push({ from: p, kind: 'false' });
|
|
678
|
+
}
|
|
679
|
+
return { entry: p, exits };
|
|
680
|
+
}
|
|
681
|
+
// @implements A-SPEC-522.1 — match: arms isolated, EXHAUSTIVE (rustc enforces it, so a
|
|
682
|
+
// "no arm matched" escape would be flow no execution takes — the opposite of Python's
|
|
683
|
+
// match, deliberately). Arm values may be blocks or bare expressions.
|
|
684
|
+
case 'match_expression': {
|
|
685
|
+
const p = newPoint(i);
|
|
686
|
+
const mb = kids.of(i).find((k) => ast.nodes[k].type === 'match_block');
|
|
687
|
+
const exits = [];
|
|
688
|
+
for (const arm of mb === undefined ? [] : kids.of(mb)) {
|
|
689
|
+
if (ast.nodes[arm].type !== 'match_arm')
|
|
690
|
+
continue;
|
|
691
|
+
const val = kids.of(arm).filter((k) => ast.nodes[k].named && ast.nodes[k].type !== 'match_pattern').at(-1);
|
|
692
|
+
if (val === undefined)
|
|
693
|
+
continue;
|
|
694
|
+
const vf = rules.handled.has(ast.nodes[val].type)
|
|
695
|
+
? lowerStmt(val, ctx)
|
|
696
|
+
: (() => { const vp = newPoint(val); rustQuestion(vp, val); return { entry: vp, exits: [{ from: vp, kind: 'seq' }] }; })();
|
|
697
|
+
link(p, vf.entry, 'case');
|
|
698
|
+
exits.push(...vf.exits);
|
|
699
|
+
}
|
|
700
|
+
return { entry: p, exits };
|
|
701
|
+
}
|
|
702
|
+
// @implements A-SPEC-522.1 — for/while/loop expressions ride the loop skeleton; a bare
|
|
703
|
+
// `loop` is INFINITE (no exhaustion edge — Go's bare-for call, same reasoning).
|
|
704
|
+
case 'for_expression':
|
|
705
|
+
case 'while_expression':
|
|
706
|
+
case 'loop_expression': {
|
|
707
|
+
const header = newPoint(i);
|
|
708
|
+
rustQuestion(header, i);
|
|
709
|
+
const bodyR = kids.of(i).find((k) => ast.nodes[k].type === 'block');
|
|
710
|
+
const myBreak = { label, target: -1 };
|
|
711
|
+
const loopLabel = (() => {
|
|
712
|
+
const ln = kids.of(i).find((k) => ast.nodes[k].type === 'label');
|
|
713
|
+
return ln !== undefined && source !== undefined
|
|
714
|
+
? source.slice(ast.nodes[ln].start, ast.nodes[ln].end) : label;
|
|
715
|
+
})();
|
|
716
|
+
const owner = { label: loopLabel, target: -1 };
|
|
717
|
+
ctx.breakT.push({ label: loopLabel, owner }, { label: null, owner });
|
|
718
|
+
ctx.contT.push({ label: loopLabel, target: header }, { label: null, target: header });
|
|
719
|
+
const bf = lowerStmt(bodyR, ctx);
|
|
720
|
+
ctx.breakT.splice(-2);
|
|
721
|
+
ctx.contT.splice(-2);
|
|
722
|
+
link(header, bf.entry, 'true');
|
|
723
|
+
for (const e of bf.exits)
|
|
724
|
+
link(e.from, header, 'loop-back');
|
|
725
|
+
const exits = [];
|
|
726
|
+
if (t !== 'loop_expression')
|
|
727
|
+
exits.push({ from: header, kind: 'false' });
|
|
728
|
+
for (const b of claimBreaks(owner))
|
|
729
|
+
exits.push({ from: b.point, kind: 'break' });
|
|
730
|
+
void myBreak;
|
|
731
|
+
return { entry: header, exits };
|
|
732
|
+
}
|
|
733
|
+
case 'return_expression': {
|
|
734
|
+
const p = newPoint(i);
|
|
735
|
+
rustQuestion(p, i);
|
|
736
|
+
link(p, EXIT, 'return');
|
|
737
|
+
return { entry: p, exits: [] };
|
|
738
|
+
}
|
|
739
|
+
case 'break_expression': {
|
|
740
|
+
const p = newPoint(i);
|
|
741
|
+
const ln = kids.of(i).find((k) => ast.nodes[k].type === 'label');
|
|
742
|
+
const lbl = ln !== undefined && source !== undefined
|
|
743
|
+
? source.slice(ast.nodes[ln].start, ast.nodes[ln].end) : null;
|
|
744
|
+
const bentry = nearestBreak(ctx.breakT, lbl);
|
|
745
|
+
if (!bentry)
|
|
746
|
+
throw new Unsupported('break outside breakable');
|
|
747
|
+
pendingBreaks.push({ point: p, owner: bentry.owner });
|
|
748
|
+
return { entry: p, exits: [] };
|
|
749
|
+
}
|
|
750
|
+
case 'continue_expression': {
|
|
751
|
+
const p = newPoint(i);
|
|
752
|
+
const ln = kids.of(i).find((k) => ast.nodes[k].type === 'label');
|
|
753
|
+
const lbl = ln !== undefined && source !== undefined
|
|
754
|
+
? source.slice(ast.nodes[ln].start, ast.nodes[ln].end) : null;
|
|
755
|
+
const centry = nearestCont(ctx.contT, lbl);
|
|
756
|
+
if (!centry || centry.target < 0)
|
|
757
|
+
throw new Unsupported('continue outside loop');
|
|
758
|
+
link(p, centry.target, 'continue');
|
|
759
|
+
return { entry: p, exits: [] };
|
|
760
|
+
}
|
|
371
761
|
// @implements A-SPEC-519.1 — select: each communication case is a branch off the header;
|
|
372
762
|
// WITHOUT a default the statement BLOCKS until some case fires, so there is no "no arm
|
|
373
763
|
// matched" escape (the opposite of Python's match).
|
|
@@ -420,6 +810,36 @@ function cfgOf(ast, fn, source) {
|
|
|
420
810
|
return { entry: p, exits: [] };
|
|
421
811
|
}
|
|
422
812
|
case 'switch_statement': {
|
|
813
|
+
// @implements A-SPEC-521.1 — C# shares the node NAME with TS and inverts the
|
|
814
|
+
// semantics: implicit fallthrough is a compile error, so sections are ISOLATED.
|
|
815
|
+
// (The suite caught the first attempt — a duplicate case label silently shadowed the
|
|
816
|
+
// TS lowering and broke its fallthrough pin.)
|
|
817
|
+
if (ast.lang === 'csharp') {
|
|
818
|
+
const p = newPoint(i);
|
|
819
|
+
const myBreak = { label, target: -1 };
|
|
820
|
+
ctx.breakT.push({ label, owner: myBreak }, { label: null, owner: myBreak });
|
|
821
|
+
const swBody = kids.of(i).find((k) => ast.nodes[k].type === 'switch_body');
|
|
822
|
+
let sawDefault = false;
|
|
823
|
+
const exits = [];
|
|
824
|
+
for (const sec of swBody === undefined ? [] : kids.of(swBody)) {
|
|
825
|
+
if (ast.nodes[sec].type !== 'switch_section')
|
|
826
|
+
continue;
|
|
827
|
+
const children = stmtChildren(sec);
|
|
828
|
+
const stmts = children.filter((k) => rules.handled.has(ast.nodes[k].type));
|
|
829
|
+
const hasPattern = children.some((k) => ast.nodes[k].type.includes('pattern'));
|
|
830
|
+
if (!hasPattern)
|
|
831
|
+
sawDefault = true; // a default section carries no pattern
|
|
832
|
+
const cf = lowerSeq(stmts, ctx);
|
|
833
|
+
link(p, cf.entry, hasPattern ? 'case' : 'default');
|
|
834
|
+
exits.push(...cf.exits); // isolated: nothing flows to a sibling
|
|
835
|
+
}
|
|
836
|
+
if (!sawDefault)
|
|
837
|
+
exits.push({ from: p, kind: 'default' });
|
|
838
|
+
ctx.breakT.splice(-2);
|
|
839
|
+
for (const b of claimBreaks(myBreak))
|
|
840
|
+
exits.push({ from: b.point, kind: 'break' });
|
|
841
|
+
return { entry: p, exits };
|
|
842
|
+
}
|
|
423
843
|
const p = newPoint(i);
|
|
424
844
|
const body = stmtChildren(i).at(-1); // switch_body
|
|
425
845
|
const myBreak = { label, target: -1 };
|
|
@@ -478,13 +898,17 @@ function cfgOf(ast, fn, source) {
|
|
|
478
898
|
link(p, ctx.catchT.at(-1) ?? EXIT, 'throw');
|
|
479
899
|
return { entry: p, exits: [] };
|
|
480
900
|
}
|
|
901
|
+
// @implements A-SPEC-520.1 — try-with-resources joins the try case: same clause wiring,
|
|
902
|
+
// plus a point for the resource_specification so its defs (`var r = open()`) have a home.
|
|
903
|
+
case 'try_with_resources_statement':
|
|
481
904
|
case 'try_statement': {
|
|
482
905
|
const named = stmtChildren(i);
|
|
483
906
|
// @implements A-SPEC-510.7 — Python spells the same shape with `block` / `except_clause`
|
|
484
907
|
// / `finally_clause`, PLUS an `else_clause` that runs ONLY when no exception was raised.
|
|
908
|
+
// @implements A-SPEC-520.1 — Java spells the try body `block`; TS `statement_block`.
|
|
485
909
|
const body = isPython
|
|
486
910
|
? kids.of(i).find((k) => ast.nodes[k].type === 'block')
|
|
487
|
-
: named.find((k) => ast.nodes[k].type
|
|
911
|
+
: named.find((k) => ['statement_block', 'block'].includes(ast.nodes[k].type));
|
|
488
912
|
const handler = isPython
|
|
489
913
|
? kids.of(i).find((k) => ast.nodes[k].type === 'except_clause')
|
|
490
914
|
: named.find((k) => ast.nodes[k].type === 'catch_clause');
|
|
@@ -538,6 +962,12 @@ function cfgOf(ast, fn, source) {
|
|
|
538
962
|
else {
|
|
539
963
|
exits.push(...after);
|
|
540
964
|
}
|
|
965
|
+
const resSpec = kids.of(i).find((k) => ast.nodes[k].type === 'resource_specification');
|
|
966
|
+
if (resSpec !== undefined) {
|
|
967
|
+
const rp = newPoint(resSpec);
|
|
968
|
+
link(rp, bf.entry, 'seq');
|
|
969
|
+
return { entry: rp, exits };
|
|
970
|
+
}
|
|
541
971
|
return { entry: bf.entry, exits };
|
|
542
972
|
}
|
|
543
973
|
default:
|
|
@@ -569,7 +999,7 @@ function cfgOf(ast, fn, source) {
|
|
|
569
999
|
}
|
|
570
1000
|
return undefined;
|
|
571
1001
|
};
|
|
572
|
-
const ctx = { breakT: [], contT: [], catchT: [], pendingLabel: null };
|
|
1002
|
+
const ctx = { breakT: [], contT: [], catchT: [], yieldT: [], pendingLabel: null };
|
|
573
1003
|
const bodyNode = ast.nodes[fn.bodyIndex];
|
|
574
1004
|
const flow = bodyNode.type === rules.block
|
|
575
1005
|
? lowerSeq(stmtChildren(fn.bodyIndex), ctx)
|
|
@@ -17,7 +17,9 @@ function defUseOf(ast, cfg, fn, source) {
|
|
|
17
17
|
const isPython = ast.lang === 'python';
|
|
18
18
|
const isGo = ast.lang === 'go';
|
|
19
19
|
const params = new Map();
|
|
20
|
-
const bareArrow = ast.nodes[fn.nodeIndex].type === 'arrow_function'
|
|
20
|
+
const bareArrow = (ast.nodes[fn.nodeIndex].type === 'arrow_function'
|
|
21
|
+
// @implements A-SPEC-520.1 — Java's single-parameter lambda (`z -> …`) has the same shape.
|
|
22
|
+
|| ast.nodes[fn.nodeIndex].type === 'lambda_expression')
|
|
21
23
|
&& !kids.of(fn.nodeIndex).some((k) => ast.nodes[k].type === 'formal_parameters');
|
|
22
24
|
for (const k of kids.of(fn.nodeIndex)) {
|
|
23
25
|
// Only formal_parameters — a declaration's NAME identifier is not a parameter (measured: the
|
|
@@ -27,7 +29,9 @@ function defUseOf(ast, cfg, fn, source) {
|
|
|
27
29
|
// times on one declaration: receiver, parameters, NAMED RESULTS. All of them bind names at
|
|
28
30
|
// ENTRY, so Go must not break after the first (the probe pinned the second-list shape).
|
|
29
31
|
const isParamList = ast.nodes[k].type === 'formal_parameters' || ast.nodes[k].type === 'parameters'
|
|
30
|
-
|| ast.nodes[k].type === 'parameter_list'
|
|
32
|
+
|| ast.nodes[k].type === 'parameter_list'
|
|
33
|
+
// @implements A-SPEC-522.1 — Rust: fn parameters and closure |x| parameters.
|
|
34
|
+
|| ast.nodes[k].type === 'parameters' || ast.nodes[k].type === 'closure_parameters';
|
|
31
35
|
if (!isParamList && !(bareArrow && ast.nodes[k].type === 'identifier'))
|
|
32
36
|
continue;
|
|
33
37
|
const collect = (n) => {
|
|
@@ -55,6 +59,21 @@ function defUseOf(ast, cfg, fn, source) {
|
|
|
55
59
|
for (const b of cfg.blocks)
|
|
56
60
|
for (const s of b.stmts)
|
|
57
61
|
stmtSet.add(s);
|
|
62
|
+
// @implements A-SPEC-522.1 — walk a Rust PATTERN: lowercase-initial identifiers bind (defs),
|
|
63
|
+
// capitalized ones are constructors/types (skipped). Everything else recurses.
|
|
64
|
+
const walkRustPattern = (n, stmt) => {
|
|
65
|
+
const ty = ast.nodes[n].type;
|
|
66
|
+
if (ty === 'identifier') {
|
|
67
|
+
const t = text(n);
|
|
68
|
+
if (/^[a-z_]/.test(t) && t !== '_')
|
|
69
|
+
add(defs, stmt, t);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (ty.includes('literal') || ty === 'type_identifier')
|
|
73
|
+
return;
|
|
74
|
+
for (const c of kids.of(n))
|
|
75
|
+
walkRustPattern(c, stmt);
|
|
76
|
+
};
|
|
58
77
|
const classify = (stmt) => {
|
|
59
78
|
const walk = (n, defCtx) => {
|
|
60
79
|
const ty = ast.nodes[n].type;
|
|
@@ -62,7 +81,7 @@ function defUseOf(ast, cfg, fn, source) {
|
|
|
62
81
|
return; // nested function boundary
|
|
63
82
|
if (n !== stmt && stmtSet.has(n))
|
|
64
83
|
return; // nested statements are their own rows
|
|
65
|
-
if (ty === 'type_annotation' || ty === 'type_arguments' || ty
|
|
84
|
+
if (ty === 'type_annotation' || ty === 'type_arguments' || ty.includes('comment'))
|
|
66
85
|
return;
|
|
67
86
|
if (ty === 'identifier' || ty === 'shorthand_property_identifier_pattern') {
|
|
68
87
|
if (defCtx)
|
|
@@ -78,6 +97,105 @@ function defUseOf(ast, cfg, fn, source) {
|
|
|
78
97
|
if (ty === 'field_identifier' || ty === 'package_identifier' || ty === 'label_name')
|
|
79
98
|
return;
|
|
80
99
|
switch (ty) {
|
|
100
|
+
// @implements A-SPEC-522.1 — Rust's binding shapes. Pattern identifiers bind by the
|
|
101
|
+
// LOWERCASE-INITIAL convention (rustc warns on violations): `Some(x)` binds x, skips the
|
|
102
|
+
// constructor Some — a documented heuristic, not a guess about arbitrary code.
|
|
103
|
+
case 'let_declaration': {
|
|
104
|
+
const named = kids.of(n).filter((c) => ast.nodes[c].named);
|
|
105
|
+
if (named[0] !== undefined)
|
|
106
|
+
walkRustPattern(named[0], stmt);
|
|
107
|
+
for (const r of named.slice(1))
|
|
108
|
+
walk(r, false);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
case 'compound_assignment_expr': {
|
|
112
|
+
const [lhs, ...rest] = kids.of(n).filter((c) => ast.nodes[c].named);
|
|
113
|
+
if (lhs !== undefined && ast.nodes[lhs].type === 'identifier') {
|
|
114
|
+
add(defs, stmt, text(lhs));
|
|
115
|
+
add(uses, stmt, text(lhs));
|
|
116
|
+
}
|
|
117
|
+
else if (lhs !== undefined)
|
|
118
|
+
walk(lhs, false);
|
|
119
|
+
for (const r of rest)
|
|
120
|
+
walk(r, false);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
case 'for_expression': {
|
|
124
|
+
// `for PAT in ITER { .. }` — the pattern binds fresh; iterable is a use.
|
|
125
|
+
const named = kids.of(n).filter((c) => ast.nodes[c].named);
|
|
126
|
+
if (named[0] !== undefined)
|
|
127
|
+
walkRustPattern(named[0], stmt);
|
|
128
|
+
for (const r of named.slice(1))
|
|
129
|
+
walk(r, false);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
case 'let_condition': {
|
|
133
|
+
// `while let Some(x) = e` / `if let ...` — pattern binds at the header point.
|
|
134
|
+
const named = kids.of(n).filter((c) => ast.nodes[c].named);
|
|
135
|
+
if (named[0] !== undefined)
|
|
136
|
+
walkRustPattern(named[0], stmt);
|
|
137
|
+
for (const r of named.slice(1))
|
|
138
|
+
walk(r, false);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
case 'scoped_identifier': {
|
|
142
|
+
// a::b::c — path segments are module/type names, not local reads.
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// @implements A-SPEC-520.1 — Java's shapes. A field's name and a method's name are plain
|
|
146
|
+
// `identifier` nodes in this grammar, so an unguarded walk would read `this.x = x` as TWO
|
|
147
|
+
// uses of x and every call as a use of its method name — with a same-named local, that is
|
|
148
|
+
// a fabricated reaching edge, not just noise.
|
|
149
|
+
// @implements A-SPEC-521.1 — C#: the LAST named child of a member access is the member's
|
|
150
|
+
// NAME (`this` is unnamed, so `this.x` has exactly one — the name). Walking it would read
|
|
151
|
+
// `this.x = x` as two uses of x. Delegates held in FIELDS lose their use through this
|
|
152
|
+
// (declared); a bare local delegate call (`a()`) keeps its use — the callee is walked.
|
|
153
|
+
case 'member_access_expression': {
|
|
154
|
+
const named = kids.of(n).filter((c) => ast.nodes[c].named);
|
|
155
|
+
for (let ix = 0; ix < named.length - 1; ix++)
|
|
156
|
+
walk(named[ix], false);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
case 'field_access': {
|
|
160
|
+
const named = kids.of(n).filter((c) => ast.nodes[c].named);
|
|
161
|
+
if (named.length > 0)
|
|
162
|
+
walk(named[0], false); // the object; the field name is skipped
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
case 'method_invocation': {
|
|
166
|
+
const named = kids.of(n).filter((c) => ast.nodes[c].named);
|
|
167
|
+
const argsAt = named.findIndex((c) => ast.nodes[c].type === 'argument_list');
|
|
168
|
+
named.forEach((c, ix) => {
|
|
169
|
+
if (ix === (argsAt > 0 ? argsAt - 1 : -1) && ast.nodes[c].type === 'identifier')
|
|
170
|
+
return; // the name
|
|
171
|
+
if (ast.nodes[c].type === 'type_arguments')
|
|
172
|
+
return;
|
|
173
|
+
walk(c, false);
|
|
174
|
+
});
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
case 'enhanced_for_statement':
|
|
178
|
+
case 'foreach_statement': {
|
|
179
|
+
// `for (T s : xs)` — the binding is a def; the iterable and body-side reads are uses.
|
|
180
|
+
const named = kids.of(n).filter((c) => ast.nodes[c].named);
|
|
181
|
+
for (const c of named) {
|
|
182
|
+
if (ast.nodes[c].type === 'identifier')
|
|
183
|
+
add(defs, stmt, text(c));
|
|
184
|
+
else
|
|
185
|
+
walk(c, false);
|
|
186
|
+
}
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
case 'resource': {
|
|
190
|
+
// try-with-resources: `var r = open()` — r defs at the resource point, the init is a use.
|
|
191
|
+
for (const c of kids.of(n)) {
|
|
192
|
+
if (ast.nodes[c].type === 'identifier')
|
|
193
|
+
add(defs, stmt, text(c));
|
|
194
|
+
else
|
|
195
|
+
walk(c, false);
|
|
196
|
+
}
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
81
199
|
// @implements A-SPEC-519.1 — Go's binding shapes, written from the probed grammar.
|
|
82
200
|
case 'short_var_declaration': {
|
|
83
201
|
// `a, b := e1, e2` — every LHS identifier is a def; the RHS list is uses.
|
|
@@ -285,13 +403,69 @@ function defUseOf(ast, cfg, fn, source) {
|
|
|
285
403
|
};
|
|
286
404
|
for (const s of stmtSet)
|
|
287
405
|
classify(s);
|
|
406
|
+
// @implements A-SPEC-522.1 — Rust match patterns bind at the MATCH HEADER point (the same
|
|
407
|
+
// posture as C#'s declaration pattern, for the same measured reason: a def on the point that
|
|
408
|
+
// uses it can never reach). Guards (`n if g(n)`) read at the header too.
|
|
409
|
+
if (ast.lang === 'rust') {
|
|
410
|
+
ast.nodes.forEach((n, i) => {
|
|
411
|
+
if (n.type !== 'match_expression' || !stmtSet.has(i))
|
|
412
|
+
return;
|
|
413
|
+
const mb = kids.of(i).find((k) => ast.nodes[k].type === 'match_block');
|
|
414
|
+
for (const arm of mb === undefined ? [] : kids.of(mb)) {
|
|
415
|
+
if (ast.nodes[arm].type !== 'match_arm')
|
|
416
|
+
continue;
|
|
417
|
+
const pat = kids.of(arm).find((k) => ast.nodes[k].type === 'match_pattern');
|
|
418
|
+
if (pat !== undefined) {
|
|
419
|
+
walkRustPattern(pat, i);
|
|
420
|
+
// a guard inside the pattern (`n if g(n)`) reads its names at dispatch time
|
|
421
|
+
for (const g of kids.of(pat)) {
|
|
422
|
+
if (ast.nodes[g].type !== 'match_pattern' && ast.nodes[g].named) {
|
|
423
|
+
const collect = (m) => {
|
|
424
|
+
if (ast.nodes[m].type === 'identifier' && /^[a-z_]/.test(text(m)))
|
|
425
|
+
add(uses, i, text(m));
|
|
426
|
+
for (const c of kids.of(m))
|
|
427
|
+
collect(c);
|
|
428
|
+
};
|
|
429
|
+
collect(g);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
// @implements A-SPEC-521.1 — a C# declaration pattern (`case string f:`) binds at the SWITCH
|
|
437
|
+
// HEADER point, the same posture as Go's type-switch binding. Attaching it to the section's
|
|
438
|
+
// first statement was the first design and measurably wrong: reaching definitions read a use
|
|
439
|
+
// against the statement's IN set, so a def on the same point can never reach `case string f:
|
|
440
|
+
// Use(f);` — and that first statement usually IS the use. (Design corrected mid-slice, re-sealed.)
|
|
441
|
+
ast.nodes.forEach((n, i) => {
|
|
442
|
+
if (n.type !== 'switch_section')
|
|
443
|
+
return;
|
|
444
|
+
const pat = kids.of(i).find((k) => ast.nodes[k].type === 'declaration_pattern');
|
|
445
|
+
if (pat === undefined)
|
|
446
|
+
return;
|
|
447
|
+
const bind = kids.of(pat).filter((k) => ast.nodes[k].named).at(-1);
|
|
448
|
+
if (bind === undefined || ast.nodes[bind].type !== 'identifier')
|
|
449
|
+
return;
|
|
450
|
+
let sw = ast.nodes[i].parent; // switch_body
|
|
451
|
+
while (sw >= 0 && ast.nodes[sw].type !== 'switch_statement')
|
|
452
|
+
sw = ast.nodes[sw].parent;
|
|
453
|
+
if (sw >= 0 && stmtSet.has(sw))
|
|
454
|
+
add(defs, sw, text(bind));
|
|
455
|
+
});
|
|
288
456
|
// catch parameters: a def at the head of the handler body's first statement.
|
|
289
457
|
ast.nodes.forEach((n, i) => {
|
|
290
458
|
if (n.type !== 'catch_clause')
|
|
291
459
|
return;
|
|
292
460
|
const named = kids.of(i).filter((k) => ast.nodes[k].named);
|
|
293
|
-
|
|
294
|
-
|
|
461
|
+
// @implements A-SPEC-520.1 — Java wraps the parameter in catch_formal_parameter; TS puts the
|
|
462
|
+
// identifier directly under the clause. One lookup serves both.
|
|
463
|
+
const formal = named.find((k) => ast.nodes[k].type === 'catch_formal_parameter');
|
|
464
|
+
const param = named.find((k) => ast.nodes[k].type === 'identifier')
|
|
465
|
+
?? (formal !== undefined
|
|
466
|
+
? kids.of(formal).find((k) => ast.nodes[k].type === 'identifier')
|
|
467
|
+
: undefined);
|
|
468
|
+
const block = named.find((k) => ['statement_block', 'block'].includes(ast.nodes[k].type));
|
|
295
469
|
if (param === undefined || block === undefined)
|
|
296
470
|
return;
|
|
297
471
|
const first = kids.of(block).find((k) => stmtSet.has(k));
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"//": "@implements A-SPEC-209",
|
|
3
3
|
"name": "@holmes-lab/holmes-kit",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.6.0",
|
|
5
5
|
"description": "Holmes-Kit — deterministic Agentic Software Engineering (ASE) harness with causal traceability (spec chain + D-CPG + RTM + phase guardrail)",
|
|
6
6
|
"main": "dist/holmes/mcp/server.js",
|
|
7
7
|
"types": "dist/holmes/mcp/server.d.ts",
|