@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 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
- 2b427b6-mtj2vv29
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) => kids.of(i).filter((k) => ast.nodes[k].named && ast.nodes[k].type !== 'comment');
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
- const cons = isPython ? (kids.of(i).find((k) => ast.nodes[k].type === 'block') ?? named[1]) : named[1];
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 (isPython) {
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 === 'statement_block');
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
- const isParamList = ast.nodes[k].type === 'formal_parameters' || ast.nodes[k].type === 'parameters';
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 === 'comment')
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
- const param = named.find((k) => ast.nodes[k].type === 'identifier');
196
- const block = named.find((k) => ast.nodes[k].type === 'statement_block');
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
- return resolved.length >= 3
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 imports/inherits)` };
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.
@@ -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
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.1",
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",