@holmes-lab/holmes-kit 0.4.1 → 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 +70 -0
- package/dist/.build-id +1 -1
- package/dist/holmes/cpg/foundation/ast-store.d.ts +1 -1
- package/dist/holmes/cpg/foundation/ast-store.js +8 -0
- package/dist/holmes/cpg/foundation/cfg.js +570 -8
- package/dist/holmes/cpg/foundation/ddg.js +279 -7
- package/dist/holmes/cpg/foundation/language-matrix.js +9 -2
- package/grammars/manifest.json +30 -0
- package/grammars/tree-sitter-c_sharp.wasm +0 -0
- package/grammars/tree-sitter-cpp.wasm +0 -0
- package/grammars/tree-sitter-go.wasm +0 -0
- package/grammars/tree-sitter-java.wasm +0 -0
- package/grammars/tree-sitter-rust.wasm +0 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,76 @@ 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
|
+
|
|
42
|
+
<!-- @implements A-SPEC-209 -->
|
|
43
|
+
## [0.5.0] - 2026-09-02
|
|
44
|
+
|
|
45
|
+
The seven-language goal (GOAL-7langs) starts shipping: the L1 substrate now parses every
|
|
46
|
+
officially supported language, and Go becomes the third language with a full, corpus-validated
|
|
47
|
+
control- and data-flow story. Grades in the language matrix are still measurements, not goals —
|
|
48
|
+
a cell moves only when a real corpus proves it.
|
|
49
|
+
|
|
50
|
+
### Added
|
|
51
|
+
|
|
52
|
+
- **L1 AST for all seven languages** (REQ-518): five more grammars join the vendored wasm
|
|
53
|
+
substrate — go 0.25.0, rust 0.24.0, java 0.23.5, c-sharp 0.23.5, cpp 0.23.4 — each ABI-probed
|
|
54
|
+
against web-tree-sitter 0.27.0 before pinning (5/5 load+parse). The expansion is five pins and
|
|
55
|
+
five extension mappings; no code changed, which was the design claim being tested. All 30
|
|
56
|
+
language fixtures parse with zero span violations. `.h` maps to the C++ grammar (near-superset;
|
|
57
|
+
a mis-parse surfaces as errorCount>0 and every layer above refuses on it).
|
|
58
|
+
- **Go CFG/DDG/CDG** (REQ-519): Go joins the shared lowering with its semantics pinned by intent,
|
|
59
|
+
written from a full statement census over two corpora (2,637 functions, 22 statement types)
|
|
60
|
+
rather than from the grammar docs. No implicit switch fallthrough (an explicit `fallthrough`
|
|
61
|
+
links to the next case); a bare `for {}` is INFINITE — no phantom exhaustion edge behind the
|
|
62
|
+
`for { select {…} }` daemon idiom; a `select` without `default` blocks; `defer` is a
|
|
63
|
+
registration-point statement (its arguments evaluate there, per the Go spec); `goto` refuses
|
|
64
|
+
the whole function, fail-closed. Receivers and NAMED RESULTS are parameters.
|
|
65
|
+
**Measured over gin+cobra: 2,637/2,637 functions lowered, zero refusals, zero dominator/DDG/CDG
|
|
66
|
+
invariant violations, 19,975 REACHING_DEF and 4,633 CDG edges (parse 337ms, build 1.5s).**
|
|
67
|
+
|
|
68
|
+
### Fixed
|
|
69
|
+
|
|
70
|
+
- **The language matrix could contradict itself** (REQ-517): the missing-edge list in a
|
|
71
|
+
relations cell was a hardcoded literal next to a derived present-list, so after REQ-511 the
|
|
72
|
+
generated doc read "calls, inherits (no imports/inherits)" — inherits present and absent in one
|
|
73
|
+
breath. Both the missing list and the full/partial judgement now derive from
|
|
74
|
+
EXTRACTABLE_RELATIONS by set difference, and a property test bans the contradiction SHAPE
|
|
75
|
+
itself (no edge may appear on both sides, any language, any future widening).
|
|
76
|
+
|
|
7
77
|
<!-- @implements A-SPEC-209 -->
|
|
8
78
|
## [0.4.1] - 2026-09-02
|
|
9
79
|
|
package/dist/.build-id
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
814d021-mtjewubg
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** The honest language envelope: exactly what the vendored grammars cover — nothing more. */
|
|
2
2
|
export declare const AST_LANGUAGES: ReadonlyArray<{
|
|
3
3
|
ext: RegExp;
|
|
4
|
-
lang: 'typescript' | 'tsx' | 'python';
|
|
4
|
+
lang: 'typescript' | 'tsx' | 'python' | 'go' | 'rust' | 'java' | 'csharp' | 'cpp';
|
|
5
5
|
wasm: string;
|
|
6
6
|
}>;
|
|
7
7
|
export interface AstNodeRow {
|
|
@@ -67,6 +67,14 @@ exports.AST_LANGUAGES = [
|
|
|
67
67
|
{ ext: /\.(ts|mts|cts)$/, lang: 'typescript', wasm: 'tree-sitter-typescript.wasm' },
|
|
68
68
|
{ ext: /\.(js|mjs|cjs|jsx)$/, lang: 'tsx', wasm: 'tree-sitter-tsx.wasm' }, // TS grammar family parses JS (decision 3: no separate JS grammar)
|
|
69
69
|
{ ext: /\.py$/, lang: 'python', wasm: 'tree-sitter-python.wasm' },
|
|
70
|
+
// @implements A-SPEC-518.1 — the five languages the goal opens. `.h` goes to the C++ grammar on
|
|
71
|
+
// purpose (H-SPEC-518): C++ is a near-superset of C and a mis-parse surfaces as errorCount>0,
|
|
72
|
+
// which every layer above refuses on — revisited against the P2-C++ corpus.
|
|
73
|
+
{ ext: /\.go$/, lang: 'go', wasm: 'tree-sitter-go.wasm' },
|
|
74
|
+
{ ext: /\.rs$/, lang: 'rust', wasm: 'tree-sitter-rust.wasm' },
|
|
75
|
+
{ ext: /\.java$/, lang: 'java', wasm: 'tree-sitter-java.wasm' },
|
|
76
|
+
{ ext: /\.cs$/, lang: 'csharp', wasm: 'tree-sitter-c_sharp.wasm' },
|
|
77
|
+
{ ext: /\.(cpp|cc|cxx|hpp|h)$/, lang: 'cpp', wasm: 'tree-sitter-cpp.wasm' },
|
|
70
78
|
];
|
|
71
79
|
const GRAMMARS_DIR = (() => {
|
|
72
80
|
// src/holmes/cpg/foundation → three up is the package root; identical depth from dist.
|
|
@@ -10,6 +10,16 @@ exports.FUNCTION_TYPES = new Set([
|
|
|
10
10
|
'generator_function', 'arrow_function', 'method_definition', 'function',
|
|
11
11
|
// @implements A-SPEC-510.7 — Python's function node joins the same census.
|
|
12
12
|
'function_definition', 'lambda',
|
|
13
|
+
// @implements A-SPEC-519.1 — Go: methods and function literals (closures/goroutine bodies).
|
|
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',
|
|
13
23
|
]);
|
|
14
24
|
const TS_SIMPLE = new Set([
|
|
15
25
|
'expression_statement', 'lexical_declaration', 'variable_declaration', 'debugger_statement',
|
|
@@ -46,8 +56,104 @@ const PY_RULES = {
|
|
|
46
56
|
]),
|
|
47
57
|
block: 'block',
|
|
48
58
|
};
|
|
59
|
+
// @implements A-SPEC-519.1 — Go, classified from the FULL statement census over two corpora
|
|
60
|
+
// (2,637 functions, 22 types — evidence REQ-519), not from reading the grammar's docs.
|
|
61
|
+
// `defer`/`go` are pass-through statements (defer's args evaluate at registration per the Go
|
|
62
|
+
// spec; goroutine bodies are func_literals that functionsIn owns separately). `goto` is absent
|
|
63
|
+
// on purpose: zero corpus occurrences, and fail-closed refusal is the honest treatment.
|
|
64
|
+
const GO_SIMPLE = new Set([
|
|
65
|
+
'expression_statement', 'short_var_declaration', 'assignment_statement', 'var_declaration',
|
|
66
|
+
'const_declaration', 'type_declaration', 'inc_statement', 'dec_statement', 'send_statement',
|
|
67
|
+
'defer_statement', 'go_statement', 'empty_statement',
|
|
68
|
+
]);
|
|
69
|
+
const GO_RULES = {
|
|
70
|
+
simple: GO_SIMPLE,
|
|
71
|
+
handled: new Set([
|
|
72
|
+
...GO_SIMPLE,
|
|
73
|
+
'block', 'statement_list', 'if_statement', 'for_statement',
|
|
74
|
+
'expression_switch_statement', 'type_switch_statement', 'select_statement',
|
|
75
|
+
'labeled_statement', 'break_statement', 'continue_statement', 'return_statement',
|
|
76
|
+
'fallthrough_statement',
|
|
77
|
+
]),
|
|
78
|
+
block: 'block',
|
|
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
|
+
};
|
|
49
154
|
const LANG_RULES = {
|
|
50
|
-
typescript: TS_RULES, tsx: TS_RULES, python: PY_RULES,
|
|
155
|
+
typescript: TS_RULES, tsx: TS_RULES, python: PY_RULES, go: GO_RULES, java: JAVA_RULES,
|
|
156
|
+
csharp: CSHARP_RULES, rust: RUST_RULES,
|
|
51
157
|
};
|
|
52
158
|
// @implements A-SPEC-510.6
|
|
53
159
|
/**
|
|
@@ -91,6 +197,8 @@ function cfgOf(ast, fn, source) {
|
|
|
91
197
|
}
|
|
92
198
|
const rules = LANG_RULES[ast.lang];
|
|
93
199
|
const isPython = ast.lang === 'python';
|
|
200
|
+
const isGo = ast.lang === 'go';
|
|
201
|
+
const isRust = ast.lang === 'rust';
|
|
94
202
|
const kids = childrenIndex(ast);
|
|
95
203
|
// Points: one per lowered statement (block ids after coalescing). 0=ENTRY, 1=EXIT.
|
|
96
204
|
const stmtsOf = [[], []];
|
|
@@ -131,7 +239,9 @@ function cfgOf(ast, fn, source) {
|
|
|
131
239
|
}
|
|
132
240
|
return { entry, exits };
|
|
133
241
|
};
|
|
134
|
-
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'));
|
|
135
245
|
const wireException = (first, targetProvider) => {
|
|
136
246
|
// Conservative: every point created inside [first, now) can throw into the handler.
|
|
137
247
|
const target = targetProvider();
|
|
@@ -144,13 +254,54 @@ function cfgOf(ast, fn, source) {
|
|
|
144
254
|
throw new Unsupported(t);
|
|
145
255
|
const label = ctx.pendingLabel;
|
|
146
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
|
+
}
|
|
147
295
|
if (rules.simple.has(t)) {
|
|
148
296
|
const p = newPoint(i);
|
|
297
|
+
rustQuestion(p, i);
|
|
149
298
|
return { entry: p, exits: [{ from: p, kind: 'seq' }] };
|
|
150
299
|
}
|
|
151
300
|
switch (t) {
|
|
152
301
|
case 'statement_block':
|
|
153
302
|
case 'block':
|
|
303
|
+
case 'statement_list':
|
|
304
|
+
// Go's block carries one extra statement_list layer (probed shape) — both are containers.
|
|
154
305
|
return lowerSeq(stmtChildren(i), ctx);
|
|
155
306
|
case 'labeled_statement': {
|
|
156
307
|
const [labelNode, inner] = [kids.of(i)[0], stmtChildren(i).at(-1)];
|
|
@@ -164,9 +315,18 @@ function cfgOf(ast, fn, source) {
|
|
|
164
315
|
// @implements A-SPEC-510.7 — Python spells the arms as fields (`consequence`,
|
|
165
316
|
// `alternative` = elif_clause | else_clause), TS as positional children. Reading the
|
|
166
317
|
// fields where they exist keeps ONE lowering for both grammars.
|
|
167
|
-
|
|
318
|
+
// @implements A-SPEC-519.1 — Go: consequence is the FIRST block child; the alternative
|
|
319
|
+
// (an else block or an else-if chain's if_statement) is the last named child when it is
|
|
320
|
+
// distinct from the consequence. The header point owns the init (:=) and condition.
|
|
321
|
+
const goCons = isGo ? kids.of(i).find((k) => ast.nodes[k].type === 'block') : undefined;
|
|
322
|
+
const cons = isGo ? goCons : isPython ? (kids.of(i).find((k) => ast.nodes[k].type === 'block') ?? named[1]) : named[1];
|
|
168
323
|
let alt;
|
|
169
|
-
if (
|
|
324
|
+
if (isGo) {
|
|
325
|
+
const last = named.at(-1);
|
|
326
|
+
if (last !== goCons && ['block', 'if_statement'].includes(ast.nodes[last].type))
|
|
327
|
+
alt = last;
|
|
328
|
+
}
|
|
329
|
+
else if (isPython) {
|
|
170
330
|
const clause = kids.of(i).find((k) => ['elif_clause', 'else_clause'].includes(ast.nodes[k].type));
|
|
171
331
|
if (clause !== undefined) {
|
|
172
332
|
alt = ast.nodes[clause].type === 'elif_clause'
|
|
@@ -174,9 +334,29 @@ function cfgOf(ast, fn, source) {
|
|
|
174
334
|
: stmtChildren(clause).find((k) => ast.nodes[k].type === 'block');
|
|
175
335
|
}
|
|
176
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
|
+
}
|
|
177
344
|
else {
|
|
178
345
|
alt = named[2] !== undefined ? stmtChildren(named[2])[0] : undefined;
|
|
179
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
|
+
}
|
|
180
360
|
const cf = lowerStmt(cons, ctx);
|
|
181
361
|
link(p, cf.entry, 'true');
|
|
182
362
|
const exits = [...cf.exits];
|
|
@@ -202,9 +382,25 @@ function cfgOf(ast, fn, source) {
|
|
|
202
382
|
? kids.of(i).filter((k) => ast.nodes[k].type === 'else_clause')
|
|
203
383
|
.flatMap((k) => stmtChildren(k)).find((k) => ast.nodes[k].type === 'block')
|
|
204
384
|
: undefined;
|
|
205
|
-
const bodyIdx = isPython
|
|
385
|
+
const bodyIdx = (isPython || isGo)
|
|
206
386
|
? kids.of(i).filter((k) => ast.nodes[k].type === 'block').at(0)
|
|
207
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
|
+
}
|
|
399
|
+
// @implements A-SPEC-519.1 — a bare Go `for {}` (no named non-block child) is INFINITE:
|
|
400
|
+
// no exhaustion edge. `for { select {…} }` is the daemon idiom, and a phantom false edge
|
|
401
|
+
// would hang spurious control dependence on everything after the loop. A for_clause with
|
|
402
|
+
// an empty condition keeps the edge (documented over-approximation).
|
|
403
|
+
const goInfinite = isGo && !kids.of(i).some((k) => ast.nodes[k].named && ast.nodes[k].type !== 'block' && ast.nodes[k].type !== 'comment');
|
|
208
404
|
const myBreak = { label, target: -1 }; // ONE owner: labeled and bare breaks share it
|
|
209
405
|
ctx.breakT.push({ label, owner: myBreak }, { label: null, owner: myBreak });
|
|
210
406
|
ctx.contT.push({ label, target: header }, { label: null, target: header });
|
|
@@ -220,7 +416,7 @@ function cfgOf(ast, fn, source) {
|
|
|
220
416
|
link(header, ef.entry, 'false'); // exhaustion path enters `else`
|
|
221
417
|
exits.push(...ef.exits);
|
|
222
418
|
}
|
|
223
|
-
else {
|
|
419
|
+
else if (!goInfinite) {
|
|
224
420
|
exits.push({ from: header, kind: 'false' });
|
|
225
421
|
}
|
|
226
422
|
// break targets resolved lazily: edges recorded with target -1 fixed by caller — instead
|
|
@@ -233,6 +429,10 @@ function cfgOf(ast, fn, source) {
|
|
|
233
429
|
case 'do_statement': {
|
|
234
430
|
const header = newPoint(i); // condition point (the `do` node)
|
|
235
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
|
+
}
|
|
236
436
|
const myBreak = { label, target: -1 };
|
|
237
437
|
ctx.breakT.push({ label, owner: myBreak }, { label: null, owner: myBreak });
|
|
238
438
|
ctx.contT.push({ label, target: header }, { label: null, target: header });
|
|
@@ -282,12 +482,364 @@ function cfgOf(ast, fn, source) {
|
|
|
282
482
|
throw new Unsupported('with_statement without a block');
|
|
283
483
|
return lowerStmt(body, ctx);
|
|
284
484
|
}
|
|
485
|
+
// @implements A-SPEC-519.1 — Go's two switch forms. NO implicit fallthrough (each case
|
|
486
|
+
// leaves the switch), an explicit fallthrough_statement links to the NEXT case's entry,
|
|
487
|
+
// and a missing default leaves a header escape (no case matched). The binding of a type
|
|
488
|
+
// switch (`switch v := x.(type)`) lives on the header point, where the DDG collects it.
|
|
489
|
+
case 'expression_switch_statement':
|
|
490
|
+
case 'type_switch_statement': {
|
|
491
|
+
const p = newPoint(i);
|
|
492
|
+
const myBreak = { label, target: -1 };
|
|
493
|
+
ctx.breakT.push({ label, owner: myBreak }, { label: null, owner: myBreak });
|
|
494
|
+
const caseNodes = kids.of(i).filter((k) => ['expression_case', 'type_case', 'default_case'].includes(ast.nodes[k].type));
|
|
495
|
+
let sawDefault = false;
|
|
496
|
+
const exits = [];
|
|
497
|
+
let pendingFalls = [];
|
|
498
|
+
caseNodes.forEach((c, ci) => {
|
|
499
|
+
if (ast.nodes[c].type === 'default_case')
|
|
500
|
+
sawDefault = true;
|
|
501
|
+
const bodyList = kids.of(c).find((k) => ast.nodes[k].type === 'statement_list');
|
|
502
|
+
const stmts = bodyList === undefined ? [] : stmtChildren(bodyList);
|
|
503
|
+
const fallIdx = stmts.filter((k) => ast.nodes[k].type === 'fallthrough_statement');
|
|
504
|
+
const rest = stmts.filter((k) => ast.nodes[k].type !== 'fallthrough_statement');
|
|
505
|
+
const cf = lowerSeq(rest, ctx);
|
|
506
|
+
link(p, cf.entry, ast.nodes[c].type === 'default_case' ? 'default' : 'case');
|
|
507
|
+
for (const f of pendingFalls)
|
|
508
|
+
link(f, cf.entry, 'seq');
|
|
509
|
+
pendingFalls = [];
|
|
510
|
+
if (fallIdx.length > 0) {
|
|
511
|
+
// The census shows fallthrough only as a case's LAST statement; a next case must
|
|
512
|
+
// exist (its absence is a Go compile error) — refuse honestly if it does not.
|
|
513
|
+
if (ci === caseNodes.length - 1)
|
|
514
|
+
throw new Unsupported('fallthrough in last case');
|
|
515
|
+
const fp = newPoint(fallIdx[0]);
|
|
516
|
+
for (const e of cf.exits)
|
|
517
|
+
link(e.from, fp, e.kind);
|
|
518
|
+
pendingFalls.push(fp);
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
exits.push(...cf.exits);
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
if (!sawDefault)
|
|
525
|
+
exits.push({ from: p, kind: 'default' });
|
|
526
|
+
ctx.breakT.splice(-2);
|
|
527
|
+
for (const b of claimBreaks(myBreak))
|
|
528
|
+
exits.push({ from: b.point, kind: 'break' });
|
|
529
|
+
return { entry: p, exits };
|
|
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
|
+
}
|
|
761
|
+
// @implements A-SPEC-519.1 — select: each communication case is a branch off the header;
|
|
762
|
+
// WITHOUT a default the statement BLOCKS until some case fires, so there is no "no arm
|
|
763
|
+
// matched" escape (the opposite of Python's match).
|
|
764
|
+
case 'select_statement': {
|
|
765
|
+
const p = newPoint(i);
|
|
766
|
+
const myBreak = { label, target: -1 };
|
|
767
|
+
ctx.breakT.push({ label, owner: myBreak }, { label: null, owner: myBreak });
|
|
768
|
+
let sawDefault = false;
|
|
769
|
+
const exits = [];
|
|
770
|
+
for (const c of kids.of(i)) {
|
|
771
|
+
const cty = ast.nodes[c].type;
|
|
772
|
+
if (cty !== 'communication_case' && cty !== 'default_case')
|
|
773
|
+
continue;
|
|
774
|
+
if (cty === 'default_case')
|
|
775
|
+
sawDefault = true;
|
|
776
|
+
const comm = kids.of(c).find((k) => ['receive_statement', 'send_statement', 'expression_statement'].includes(ast.nodes[k].type));
|
|
777
|
+
const bodyList = kids.of(c).find((k) => ast.nodes[k].type === 'statement_list');
|
|
778
|
+
const stmts = bodyList === undefined ? [] : stmtChildren(bodyList);
|
|
779
|
+
let entry;
|
|
780
|
+
let head;
|
|
781
|
+
if (comm !== undefined) {
|
|
782
|
+
const cp = newPoint(comm);
|
|
783
|
+
entry = cp;
|
|
784
|
+
head = { entry: cp, exits: [{ from: cp, kind: 'seq' }] };
|
|
785
|
+
}
|
|
786
|
+
else {
|
|
787
|
+
head = undefined;
|
|
788
|
+
entry = -1;
|
|
789
|
+
}
|
|
790
|
+
const bf = lowerSeq(stmts, ctx);
|
|
791
|
+
if (head !== undefined) {
|
|
792
|
+
for (const e of head.exits)
|
|
793
|
+
link(e.from, bf.entry, e.kind);
|
|
794
|
+
}
|
|
795
|
+
else {
|
|
796
|
+
entry = bf.entry;
|
|
797
|
+
}
|
|
798
|
+
link(p, entry, cty === 'default_case' ? 'default' : 'case');
|
|
799
|
+
exits.push(...bf.exits);
|
|
800
|
+
}
|
|
801
|
+
if (sawDefault) { /* a ready default gives an immediate path — covered by its case edge */ }
|
|
802
|
+
ctx.breakT.splice(-2);
|
|
803
|
+
for (const b of claimBreaks(myBreak))
|
|
804
|
+
exits.push({ from: b.point, kind: 'break' });
|
|
805
|
+
return { entry: p, exits };
|
|
806
|
+
}
|
|
285
807
|
case 'raise_statement': {
|
|
286
808
|
const p = newPoint(i);
|
|
287
809
|
link(p, ctx.catchT.at(-1) ?? EXIT, 'throw');
|
|
288
810
|
return { entry: p, exits: [] };
|
|
289
811
|
}
|
|
290
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
|
+
}
|
|
291
843
|
const p = newPoint(i);
|
|
292
844
|
const body = stmtChildren(i).at(-1); // switch_body
|
|
293
845
|
const myBreak = { label, target: -1 };
|
|
@@ -346,13 +898,17 @@ function cfgOf(ast, fn, source) {
|
|
|
346
898
|
link(p, ctx.catchT.at(-1) ?? EXIT, 'throw');
|
|
347
899
|
return { entry: p, exits: [] };
|
|
348
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':
|
|
349
904
|
case 'try_statement': {
|
|
350
905
|
const named = stmtChildren(i);
|
|
351
906
|
// @implements A-SPEC-510.7 — Python spells the same shape with `block` / `except_clause`
|
|
352
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`.
|
|
353
909
|
const body = isPython
|
|
354
910
|
? kids.of(i).find((k) => ast.nodes[k].type === 'block')
|
|
355
|
-
: named.find((k) => ast.nodes[k].type
|
|
911
|
+
: named.find((k) => ['statement_block', 'block'].includes(ast.nodes[k].type));
|
|
356
912
|
const handler = isPython
|
|
357
913
|
? kids.of(i).find((k) => ast.nodes[k].type === 'except_clause')
|
|
358
914
|
: named.find((k) => ast.nodes[k].type === 'catch_clause');
|
|
@@ -406,6 +962,12 @@ function cfgOf(ast, fn, source) {
|
|
|
406
962
|
else {
|
|
407
963
|
exits.push(...after);
|
|
408
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
|
+
}
|
|
409
971
|
return { entry: bf.entry, exits };
|
|
410
972
|
}
|
|
411
973
|
default:
|
|
@@ -437,7 +999,7 @@ function cfgOf(ast, fn, source) {
|
|
|
437
999
|
}
|
|
438
1000
|
return undefined;
|
|
439
1001
|
};
|
|
440
|
-
const ctx = { breakT: [], contT: [], catchT: [], pendingLabel: null };
|
|
1002
|
+
const ctx = { breakT: [], contT: [], catchT: [], yieldT: [], pendingLabel: null };
|
|
441
1003
|
const bodyNode = ast.nodes[fn.bodyIndex];
|
|
442
1004
|
const flow = bodyNode.type === rules.block
|
|
443
1005
|
? lowerSeq(stmtChildren(fn.bodyIndex), ctx)
|
|
@@ -6,20 +6,32 @@ exports.ddgViolations = ddgViolations;
|
|
|
6
6
|
const cfg_1 = require("./cfg");
|
|
7
7
|
/** Identifier positions that are DEF sites, keyed by the parent node type. */
|
|
8
8
|
const PATTERN_TYPES = new Set(['object_pattern', 'array_pattern', 'shorthand_property_identifier_pattern',
|
|
9
|
-
'pair_pattern', 'rest_pattern', 'assignment_pattern'
|
|
9
|
+
'pair_pattern', 'rest_pattern', 'assignment_pattern',
|
|
10
|
+
// @implements A-SPEC-519.1 — Go: an expression_list on a binding LHS carries the def context
|
|
11
|
+
// through to each identifier (`a, b := …`, `for i, v := range …`).
|
|
12
|
+
'expression_list']);
|
|
10
13
|
function defUseOf(ast, cfg, fn, source) {
|
|
11
14
|
const kids = (0, cfg_1.childrenIndex)(ast);
|
|
12
15
|
const text = (i) => source.slice(ast.nodes[i].start, ast.nodes[i].end);
|
|
13
16
|
// ── parameters: ENTRY defs (and part of the local namespace).
|
|
14
17
|
const isPython = ast.lang === 'python';
|
|
18
|
+
const isGo = ast.lang === 'go';
|
|
15
19
|
const params = new Map();
|
|
16
|
-
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')
|
|
17
23
|
&& !kids.of(fn.nodeIndex).some((k) => ast.nodes[k].type === 'formal_parameters');
|
|
18
24
|
for (const k of kids.of(fn.nodeIndex)) {
|
|
19
25
|
// Only formal_parameters — a declaration's NAME identifier is not a parameter (measured: the
|
|
20
26
|
// early break on the name swallowed every real parameter of named functions).
|
|
21
27
|
// @implements A-SPEC-510.7 — Python's parameter list node is `parameters`.
|
|
22
|
-
|
|
28
|
+
// @implements A-SPEC-519.1 — Go's list node is `parameter_list`, and it appears up to THREE
|
|
29
|
+
// times on one declaration: receiver, parameters, NAMED RESULTS. All of them bind names at
|
|
30
|
+
// ENTRY, so Go must not break after the first (the probe pinned the second-list shape).
|
|
31
|
+
const isParamList = ast.nodes[k].type === 'formal_parameters' || ast.nodes[k].type === 'parameters'
|
|
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';
|
|
23
35
|
if (!isParamList && !(bareArrow && ast.nodes[k].type === 'identifier'))
|
|
24
36
|
continue;
|
|
25
37
|
const collect = (n) => {
|
|
@@ -32,7 +44,7 @@ function defUseOf(ast, cfg, fn, source) {
|
|
|
32
44
|
collect(c);
|
|
33
45
|
};
|
|
34
46
|
collect(k);
|
|
35
|
-
if (isParamList)
|
|
47
|
+
if (isParamList && !isGo)
|
|
36
48
|
break;
|
|
37
49
|
}
|
|
38
50
|
const defs = new Map();
|
|
@@ -47,6 +59,21 @@ function defUseOf(ast, cfg, fn, source) {
|
|
|
47
59
|
for (const b of cfg.blocks)
|
|
48
60
|
for (const s of b.stmts)
|
|
49
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
|
+
};
|
|
50
77
|
const classify = (stmt) => {
|
|
51
78
|
const walk = (n, defCtx) => {
|
|
52
79
|
const ty = ast.nodes[n].type;
|
|
@@ -54,7 +81,7 @@ function defUseOf(ast, cfg, fn, source) {
|
|
|
54
81
|
return; // nested function boundary
|
|
55
82
|
if (n !== stmt && stmtSet.has(n))
|
|
56
83
|
return; // nested statements are their own rows
|
|
57
|
-
if (ty === 'type_annotation' || ty === 'type_arguments' || ty
|
|
84
|
+
if (ty === 'type_annotation' || ty === 'type_arguments' || ty.includes('comment'))
|
|
58
85
|
return;
|
|
59
86
|
if (ty === 'identifier' || ty === 'shorthand_property_identifier_pattern') {
|
|
60
87
|
if (defCtx)
|
|
@@ -65,7 +92,196 @@ function defUseOf(ast, cfg, fn, source) {
|
|
|
65
92
|
}
|
|
66
93
|
if (ty === 'property_identifier' || ty === 'statement_identifier' || ty === 'type_identifier')
|
|
67
94
|
return;
|
|
95
|
+
// @implements A-SPEC-519.1 — Go: a selector's field, a package qualifier and a label are
|
|
96
|
+
// names in other namespaces, never local value reads.
|
|
97
|
+
if (ty === 'field_identifier' || ty === 'package_identifier' || ty === 'label_name')
|
|
98
|
+
return;
|
|
68
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
|
+
}
|
|
199
|
+
// @implements A-SPEC-519.1 — Go's binding shapes, written from the probed grammar.
|
|
200
|
+
case 'short_var_declaration': {
|
|
201
|
+
// `a, b := e1, e2` — every LHS identifier is a def; the RHS list is uses.
|
|
202
|
+
const named = kids.of(n).filter((c) => ast.nodes[c].named);
|
|
203
|
+
if (named[0] !== undefined)
|
|
204
|
+
walk(named[0], true);
|
|
205
|
+
for (const r of named.slice(1))
|
|
206
|
+
walk(r, false);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
case 'assignment_statement': {
|
|
210
|
+
// `x = e` defs x; `x += e` defs AND uses it. No fields survive persistence, so the
|
|
211
|
+
// operator is read from the unnamed token between the two expression_lists.
|
|
212
|
+
const all = kids.of(n);
|
|
213
|
+
const named = all.filter((c) => ast.nodes[c].named);
|
|
214
|
+
const op = all.find((c) => !ast.nodes[c].named && text(c).endsWith('='));
|
|
215
|
+
const compound = op !== undefined && text(op) !== '=';
|
|
216
|
+
const lhs = named[0];
|
|
217
|
+
if (lhs !== undefined) {
|
|
218
|
+
for (const el of ast.nodes[lhs].type === 'expression_list' ? kids.of(lhs).filter((c) => ast.nodes[c].named) : [lhs]) {
|
|
219
|
+
if (ast.nodes[el].type === 'identifier') {
|
|
220
|
+
add(defs, stmt, text(el));
|
|
221
|
+
if (compound)
|
|
222
|
+
add(uses, stmt, text(el));
|
|
223
|
+
}
|
|
224
|
+
else
|
|
225
|
+
walk(el, false); // a.b / a[i] mutation: uses only
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
for (const r of named.slice(1))
|
|
229
|
+
walk(r, false);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
case 'var_spec':
|
|
233
|
+
case 'const_spec': {
|
|
234
|
+
// `var x, y T = e1, e2` — identifier children are defs, the value list is uses.
|
|
235
|
+
for (const c of kids.of(n)) {
|
|
236
|
+
if (ast.nodes[c].type === 'identifier')
|
|
237
|
+
add(defs, stmt, text(c));
|
|
238
|
+
else
|
|
239
|
+
walk(c, false);
|
|
240
|
+
}
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
case 'inc_statement':
|
|
244
|
+
case 'dec_statement': {
|
|
245
|
+
const arg = kids.of(n).find((c) => ast.nodes[c].named);
|
|
246
|
+
if (arg !== undefined && ast.nodes[arg].type === 'identifier') {
|
|
247
|
+
add(defs, stmt, text(arg));
|
|
248
|
+
add(uses, stmt, text(arg));
|
|
249
|
+
}
|
|
250
|
+
else if (arg !== undefined)
|
|
251
|
+
walk(arg, false);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
case 'range_clause':
|
|
255
|
+
case 'receive_statement': {
|
|
256
|
+
// `for i, v := range xs` / `case v := <-ch` — LHS binds, RHS is read. With `=` instead
|
|
257
|
+
// of `:=` the LHS identifiers are still (re)defined; the same treatment holds.
|
|
258
|
+
const named = kids.of(n).filter((c) => ast.nodes[c].named);
|
|
259
|
+
if (named.length >= 2) {
|
|
260
|
+
walk(named[0], true);
|
|
261
|
+
for (const r of named.slice(1))
|
|
262
|
+
walk(r, false);
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
for (const r of named)
|
|
266
|
+
walk(r, false); // bare `for range xs` / `<-ch`
|
|
267
|
+
}
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
case 'type_switch_statement': {
|
|
271
|
+
// `switch v := x.(type)` — v defs at the header; everything else is uses.
|
|
272
|
+
const named = kids.of(n).filter((c) => ast.nodes[c].named);
|
|
273
|
+
const hasBind = kids.of(n).some((c) => !ast.nodes[c].named && text(c) === ':=');
|
|
274
|
+
if (hasBind && named[0] !== undefined && ast.nodes[named[0]].type === 'expression_list') {
|
|
275
|
+
walk(named[0], true);
|
|
276
|
+
for (const r of named.slice(1))
|
|
277
|
+
walk(r, false);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
for (const r of named)
|
|
281
|
+
walk(r, false);
|
|
282
|
+
}
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
69
285
|
// @implements A-SPEC-510.7 — Python's assignment shapes.
|
|
70
286
|
case 'assignment': {
|
|
71
287
|
const named = kids.of(n).filter((c) => ast.nodes[c].named);
|
|
@@ -187,13 +403,69 @@ function defUseOf(ast, cfg, fn, source) {
|
|
|
187
403
|
};
|
|
188
404
|
for (const s of stmtSet)
|
|
189
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
|
+
});
|
|
190
456
|
// catch parameters: a def at the head of the handler body's first statement.
|
|
191
457
|
ast.nodes.forEach((n, i) => {
|
|
192
458
|
if (n.type !== 'catch_clause')
|
|
193
459
|
return;
|
|
194
460
|
const named = kids.of(i).filter((k) => ast.nodes[k].named);
|
|
195
|
-
|
|
196
|
-
|
|
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));
|
|
197
469
|
if (param === undefined || block === undefined)
|
|
198
470
|
return;
|
|
199
471
|
const first = kids.of(block).find((k) => stmtSet.has(k));
|
|
@@ -43,9 +43,16 @@ function relationsCell(ext) {
|
|
|
43
43
|
if (resolved.length === 0)
|
|
44
44
|
return { support: 'none', basis: 'nothing reaches the graph' };
|
|
45
45
|
// A-SPEC-286: the claim is made in the graph-resolved column, never the scanner's raw output.
|
|
46
|
-
|
|
46
|
+
// @implements A-SPEC-517.1 — the MISSING list is derived from the same authority as the present
|
|
47
|
+
// list. It used to be the literal `(no imports/inherits)` next to a derived present-list, so when
|
|
48
|
+
// REQ-511 widened graphResolved the doc read "calls, inherits (no imports/inherits)" — inherits
|
|
49
|
+
// present and absent in one breath. The `>= 3` magic number was the same sin (a hardcoded "the
|
|
50
|
+
// full set has three kinds"); judging by set difference keeps both halves true if
|
|
51
|
+
// EXTRACTABLE_RELATIONS ever widens.
|
|
52
|
+
const missing = language_capability_1.EXTRACTABLE_RELATIONS.filter((r) => !resolved.includes(r));
|
|
53
|
+
return missing.length === 0
|
|
47
54
|
? { support: 'full', basis: `graph edges: ${resolved.join(', ')}` }
|
|
48
|
-
: { support: 'partial', basis: `graph edges: ${resolved.join(', ')} (no
|
|
55
|
+
: { support: 'partial', basis: `graph edges: ${resolved.join(', ')} (no ${missing.join('/')})` };
|
|
49
56
|
}
|
|
50
57
|
function astCell(astLang) {
|
|
51
58
|
// The TS grammar family covers tsx/js too — AST_LANGUAGES is the authority on which wasm applies.
|
package/grammars/manifest.json
CHANGED
|
@@ -18,6 +18,36 @@
|
|
|
18
18
|
"package": "tree-sitter-python",
|
|
19
19
|
"packageVersion": "0.25.0",
|
|
20
20
|
"sha256": "16108b50df4ee9a30168794252ab55e7c93bfc5765d7fa0aa3e335752c515f47"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"file": "tree-sitter-go.wasm",
|
|
24
|
+
"package": "tree-sitter-go",
|
|
25
|
+
"packageVersion": "0.25.0",
|
|
26
|
+
"sha256": "9504573f352b20be7f2f1911754d710622aedc15afff16d5ed8fb5645681aee7"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"file": "tree-sitter-rust.wasm",
|
|
30
|
+
"package": "tree-sitter-rust",
|
|
31
|
+
"packageVersion": "0.24.0",
|
|
32
|
+
"sha256": "f65f354215611fd94ad34134b3427eb3d58cbb745df7b6509ba722184db73d57"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"file": "tree-sitter-java.wasm",
|
|
36
|
+
"package": "tree-sitter-java",
|
|
37
|
+
"packageVersion": "0.23.5",
|
|
38
|
+
"sha256": "4fdeac4ca6ca089f06c6f7e562abcac1733cd465728cc7031ebb73c2019122c4"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"file": "tree-sitter-c_sharp.wasm",
|
|
42
|
+
"package": "tree-sitter-c-sharp",
|
|
43
|
+
"packageVersion": "0.23.5",
|
|
44
|
+
"sha256": "6f69e1cae44e1c32c1eccc170dc5a9778fb94ff716f71113fe1f8c4299aa2f40"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"file": "tree-sitter-cpp.wasm",
|
|
48
|
+
"package": "tree-sitter-cpp",
|
|
49
|
+
"packageVersion": "0.23.4",
|
|
50
|
+
"sha256": "174eb0deb75b2ec7881bcacda9f995648d8e683956e5c2267e69ab6dc503fcbf"
|
|
21
51
|
}
|
|
22
52
|
]
|
|
23
53
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
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",
|