From 6932e7a9fdfa4fe56eba1001c3c3aa159df8d267 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20Magyar?= Date: Mon, 15 Jun 2026 12:31:04 +0100 Subject: [PATCH] feat(cfg): PDG/CFG visitors for all supported languages (#2195) (#2197) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test(cfg): validate cfg/visitors literals + drop 3 dead TS node types Extend the grammar-literal CI gate (test/helpers/literal-collectors.ts) to scan cfg/visitors/*.ts, mapping each visitor file to its grammar via the existing basename rule (c-cpp -> C/C++, csharp -> C#, java -> Java, go -> Go, typescript -> TS). Closes the gap where the gate never validated CFG visitor node-type literals -- the prerequisite for adding C-family visitors safely (#2195 U1). The newly-scanned TS visitor surfaced 3 dead literals absent from every grammar it serves (typescript/javascript/tsx all = 0): for_of_statement (for-of parses as for_in_statement), async_function_declaration and async_arrow_function (async functions are function_declaration / arrow_function + an async child). Removed them; behavior-preserving -- the cases never matched, bench --check fingerprints unchanged, TS visitor unit tests green. Co-Authored-By: Claude Opus 4.8 (1M context) * test(cfg): language-agnostic CFG unit-test harness (#2195 U1) Extract the grammar-agnostic engine from ts-cfg-harness into makeCfgHarness(grammar, visitor, filePath) at test/helpers/cfg-harness.ts. Function discovery delegates to visitor.isFunction, so the harness carries no language-specific node-type knowledge -- each C-family visitor's unit tests can drive the real worker-side builder against real source. ts-cfg-harness becomes a thin TS binding re-exporting the same parse/collectFunctions/cfgOf/cfgsOf (behavior-preserving: all 5 existing consumers -- taint propagate/model-match/summary-harvest/taint-emit + cfg harvest -- pass unchanged, 223 tests green). New harness.test.ts proves TS-faithfulness and isFunction-delegation via a stub visitor. The bench parameterization (measure.mjs) is sequenced into U7, where the first C-family scaling scenario makes the {grammar, visitorFactory} seam validatable against a real non-TS language. Co-Authored-By: Claude Opus 4.8 (1M context) * feat(cfg): C and C++ CFG visitor + def/use harvest (#2195 U2) Add createCCfgVisitor/createCppCfgVisitor over a shared CCfgWalk core. Grammar introspection confirmed tree-sitter-c and tree-sitter-cpp share every control-flow node type/field, so CppCfgWalk extends CCfgWalk with only the C++-only nodes (try/catch/throw/for_range_loop/lambda) via a visitExtra hook -- no language conditionals (AGENTS no-language-naming). Wire both into c-cpp.ts providers. Harvest (c-cpp-harvest.ts): two-phase binding table + per-statement defs/uses/mayDefs (no sites[] yet -- U6). Edge kinds match the TS contract; functionStartColumn populated; non-terminating loops (for(;;), while(1)) emit the structural exit-escape edge so EXIT stays reverse-reachable and CDG is not silently skipped -- verified against the production post-dominator + control-dependence solvers (for(;;) -> 3 CDG edges). buildFunctionCfg returns undefined rather than throwing. 23 real-parser regression tests; grammar-literal gate green (literals validated against both grammars). Documented gaps: C++ RAII destructors, setjmp/longjmp, computed goto (route to EXIT + warn). Co-Authored-By: Claude Opus 4.8 (1M context) * feat(cfg): C# CFG visitor + def/use harvest (#2195 U3) Add createCsharpCfgVisitor + csharp-harvest over the shared CfgBuilder / ControlFlowContext, modeling the C# statement taxonomy: if/else, for/foreach/while/do, switch_section (+ switch_expression arms), try/catch/catch_filter/finally, using + lock (deterministic finalizers -- dispose/release runs on normal AND exception exit, finally-* completion edges on crossing jumps), goto/labeled, yield (surface only), return/ throw/break/continue. Wire into csharpProvider. Every literal validated against tree-sitter-c-sharp via the introspection probe (record_declaration, no else_clause, switch_section, positional access where no field exists). Edge kinds match the contract; functionStartColumn populated; while(true) keeps EXIT reverse-reachable (production CDG probe: 3 edges). buildFunctionCfg returns undefined rather than throwing. 34 real-parser regression tests; grammar-literal gate green; no regression (cfg unit dir 256/256, tsc clean). Documented gaps: yield iterator state machine, goto case/default, async suspension points. Co-Authored-By: Claude Opus 4.8 (1M context) * feat(cfg): Java CFG visitor + def/use harvest (#2195 U4) Add createJavaCfgVisitor + java-harvest over the shared CfgBuilder / ControlFlowContext: if/else, classic for, enhanced-for, while, do-while, classic-vs-arrow switch (switch_block_statement_group fallthrough vs switch_rule no-fallthrough), try/catch/finally + try-with-resources (auto-close synthesized as a finalizer, closes on normal AND exception exit) + synchronized (monitor-release finalizer), labeled break/continue to the labeled frame, yield, return/throw/break/continue. Wire into javaProvider. Every literal validated against tree-sitter-java via the probe (switch_expression covers both switch forms, generic_type, line_comment, for init field). Edge kinds match the contract; functionStartColumn populated; while(true)/for(;;) keep EXIT reverse-reachable (production CDG probe: 3 edges; hazard fixture: 34 CDG edges). buildFunctionCfg returns undefined rather than throwing. 43 real-parser regression tests; grammar-literal gate green; no regression (cfg unit suite 304, tsc clean). Documented gaps: switch-as- expression-value inline, yield state machine, async/field-write defs. Co-Authored-By: Claude Opus 4.8 (1M context) * feat(cfg): Go CFG visitor + def/use harvest (#2195 U5) Add createGoCfgVisitor + go-harvest, the highest-divergence target: for_statement (all four shapes -- for_clause C-style, while-style, range_clause, bare for{}), expression/type switch (no implicit fallthrough) + explicit fallthrough_statement, select_statement, defer (LIFO finalizer legs at function exit), go (call is straight-line; the closure body is its own CFG via isFunction), labeled break/continue/goto, multiple-return assigns (a, b := f() defines each LHS). Wire into goProvider. CRITICAL (review A2): every non-terminating shape -- for{}, for cond{}, select{} with no default -- emits a structural exit-escape edge so EXIT stays reverse-reachable and the production CDG is not silently skipped. Verified: for{} -> CDG=3, select{} -> CDG=1, for-range -> CDG=2, all exitReachable=true. Every literal validated against tree-sitter-go via the probe. 32 real-parser regression tests; grammar-literal gate green; no regression (186 across all 5 visitors + gate, full cfg unit 331, tsc clean). Documented gaps: panic/recover unwind, goroutine happens-before. Co-Authored-By: Claude Opus 4.8 (1M context) * feat(cfg): call-site sites[] taint substrate for C-family (#2195 U6) Extend the C/C++/C#/Java/Go harvests with the call-site sites[] taint substrate (SiteRecord/SiteArgOccurrence), mirroring the TS shape so the shared taint matcher consumes all languages uniformly. Extract the grammar-agnostic site machinery into cfg/visitors/call-site-harvest.ts (CallSiteFactAccumulator -- names no language); each harvest adds only its per-grammar visitCall/walkChain over its call node (C/C++ call_expression, C# invocation_expression, Java method_invocation, Go call_expression). INERT BY DESIGN: no C-family taint model exists (registerBuiltinTaintModels is TS/JS only), so getSourceSinkConfig returns undefined for these languages and the harvested sites produce ZERO TAINTED edges -- the positive source->sink->TAINTED path is deferred with the model authoring. sites emitted only when non-empty; facts-only attachment, block/edge topology unchanged (pre-existing topology + def/use tests byte-identical). 23 new substrate tests; 574 green across the cfg/taint/emit suites; gate green; tsc clean. Co-Authored-By: Claude Opus 4.8 (1M context) * test(cfg): worker-mode PDG integration + bench parameterization (#2195 U7) Prove the five C-family visitors build PDG through the REAL worker pipeline. pipeline-pdg.test.ts: per-language (C/C++/C#/Java/Go) temp repo run with pdg:true asserts BasicBlock+CFG+REACHING_DEF+CDG all > 0 (CDG>0 proves EXIT stays reverse-reachable end-to-end through the worker, incl. each fixture's non-terminating loop/select); a paired run with pdg off asserts == 0, the two flag-off graphs byte-identical (R3), no PDG types leak, pinned by a golden snapshot. Counts e.g. Go 151 BB / 56 CDG. Parameterize bench/cfg/measure.mjs by a per-language LANGS registry resolved generically via getLanguageGrammar + getProvider(X).cfgVisitor (no static import table). Default TS byte-identical -- all 6 TS fingerprints unchanged under --check; taint-dense stays TS-only (TS_JS_TAINT_MODEL never runs against model-less C-family CFGs). Add a go:branchy scenario+baseline (namespaced) -- its fingerprint shape (32 blocks/46 edges) matches TS branchy, cross-validating the Go visitor. 15 pipeline tests + bench --check PASS (7 scenarios); 354 unit cfg green; dist rebuilt clean. Absorbs the bench parameterization deferred from U1. Co-Authored-By: Claude Opus 4.8 (1M context) * feat(cfg): Python CFG visitor + def/use harvest (#2195 U8) Add createPythonCfgVisitor + python-harvest -- the most structurally divergent target (indentation blocks, elif, for/while-else, with, try/ except/except-group/else/finally, match/case, comprehensions, walrus), confirming the shared CfgBuilder/ControlFlowContext core carries no brace-family assumptions. for/while else-clause sits on the normal- completion edge (not break); with modeled as try/finally dispose; match has no fallthrough. Wire into pythonProvider. Every literal validated against tree-sitter-python via the probe. while True: keeps EXIT reverse-reachable (production CDG probe: 3 edges; fixture: 42 CDG edges). 37 real-parser tests; gate green; no regression (cfg unit 391, tsc clean). Gaps: async/generator suspension, comprehension scope over-approximation. No sites[] (taint substrate, separate). Co-Authored-By: Claude Opus 4.8 (1M context) * feat(cfg): PHP CFG visitor + def/use harvest (#2195 U9) Add createPhpCfgVisitor + php-harvest: if/elseif/else (+ alt colon syntax), for/foreach/while/do-while, switch (fallthrough) + match (no fallthrough), try/catch/finally, break N/continue N (N-th enclosing loop), goto, return/throw. Wire into phpProvider. Every literal validated against tree-sitter-php (php_only) via the probe (for_statement initialize/condition/update; throw_expression not throw_statement; break/continue integer child). while(true) keeps EXIT reverse-reachable (production CDG probe: 3 edges; break 2 escapes the outer loop). 35 real-parser tests. Also repoint worker-roundtrip's "non-CFG language" gate test from Python (which now has a cfgVisitor) to COBOL (the permanent non-goal of the rollout) -- a stale assertion the Python commit invalidated. Full in-process sweep green (452 across 18 files). Gaps: match inline value, goto plain-block. Co-Authored-By: Claude Opus 4.8 (1M context) * feat(cfg): Ruby CFG visitor + def/use harvest (#2195 U10) Add createRubyCfgVisitor + ruby-harvest: if/unless/elsif/else + statement-modifier forms (x if c, x while c), while/until/for (until inverts the sense), case/when + case/in (pattern, no fallthrough), begin/rescue/else/ensure (ensure=finally, rescue=catch) + retry (loop-back into begin), return/break/next/redo, blocks/lambdas as their own closure CFGs. Wire into rubyProvider. Every literal validated against tree-sitter-ruby via the probe (case vs case_match, modifier nodes, typed rescue/ensure children). loop do / while true keep EXIT reverse-reachable (production CDG probe: 3 edges). 34 real-parser tests; comprehensive sweep green (486). Gaps: yield, expression-position if/case/begin inline, ivar/gvar non-local defs. Co-Authored-By: Claude Opus 4.8 (1M context) * feat(cfg): Rust CFG visitor + def/use harvest (#2195 U11) Add createRustCfgVisitor + rust-harvest for the expression-oriented Rust: if/else + if-let, loop (infinite -- structural escape edge), while/ while-let/for, match (no fallthrough) + guards, labeled break/continue ('outer), break-with-value, ? operator (try_expression) as an early-return throw edge to EXIT, let-else (diverging else). visitLet handles control-flow in value position (let x = loop/if/match). Wire into rustProvider. Every literal validated against tree-sitter-rust via the probe (label is a named child not a field; line_comment; _ pattern). loop {} keeps EXIT reverse-reachable (production CDG probe: 3 edges). 33 real-parser tests; comprehensive sweep green (519). Gaps: panic, async/.await, macro bodies. Co-Authored-By: Claude Opus 4.8 (1M context) * feat(cfg): Swift CFG visitor + def/use harvest (#2195 U12) Add createSwiftCfgVisitor + swift-harvest (vendored tree-sitter-swift via requireVendoredGrammar): if/else + optional binding (if let), guard...else (diverging early exit), for-in/while/repeat-while (bottom-test), switch (no implicit fallthrough; explicit fallthrough keyword; where guards), do/catch + try/try?/try!, defer (LIFO finalizer at scope exit), labeled break/continue, control_transfer_statement (one node for break/continue/ return/throw). Wire into swiftProvider. Every literal validated against the vendored grammar via the probe (no block node; if-let folds into condition+bound_identifier; defer parses as a call_expression with trailing closure). while true keeps EXIT reverse-reachable (production CDG probe: 3 edges). 24 real-parser tests; comprehensive sweep green (543). Gaps: computed properties, defer block-scope approx, fatalError traps. Co-Authored-By: Claude Opus 4.8 (1M context) * feat(cfg): Kotlin CFG visitor + def/use harvest (#2195 U13) Add createKotlinCfgVisitor + kotlin-harvest (vendored tree-sitter-kotlin): if/else, when (subject + subjectless, no fallthrough), for/while/do-while, try/catch/finally, jump_expression (return/return@/break/break@/continue/ continue@/throw), labeled loops, control_structure_body unwrapping, expression-body functions. The grammar is field-less for control flow, so the visitor navigates by child type+position. Wire into kotlinProvider. Every literal validated against the vendored grammar via the probe (line_comment/multiline_comment, not comment). while (true) keeps EXIT reverse-reachable (production CDG probe: 3 edges; worker-mode fixture: BB=82, CDG=41). 28 real-parser tests; comprehensive sweep green (571). Gaps: value-position if/when/try inline, inline-fun non-local return, getters/setters. Co-Authored-By: Claude Opus 4.8 (1M context) * feat(cfg): Dart CFG visitor + def/use harvest (#2195 U14) Add createDartCfgVisitor + dart-harvest (vendored tree-sitter-dart): if/else, C-for/for-in/while/do-while, switch (empty-case fallthrough + explicit continue-label) + switch_expression, try/on/catch/finally + rethrow + assert (throw edges), return/break/continue/throw, labeled loops, arrow bodies, closures. Dart splits a function into sibling signature + function_body nodes, so the body (or function_expression) is the CFG-bearing node. Wire into dartProvider. Every literal validated against the vendored grammar via the probe (only constant_pattern exists; removed speculative relational/logical pattern names). while (true) keeps EXIT reverse-reachable (production CDG probe: 3 edges). 34 real-parser tests; comprehensive sweep green (605). Gaps: labeled-loop grammar quirk (read via ERROR sibling), async straight-line, value-position if/switch inline. Co-Authored-By: Claude Opus 4.8 (1M context) * feat(cfg): Vue (reuse TS visitor) + worker-mode proof for all langs (#2195 U15) Vue SFC diff --git a/gitnexus/test/integration/cfg/pipeline-pdg.test.ts b/gitnexus/test/integration/cfg/pipeline-pdg.test.ts index 2177c761a..00f4ed8f7 100644 --- a/gitnexus/test/integration/cfg/pipeline-pdg.test.ts +++ b/gitnexus/test/integration/cfg/pipeline-pdg.test.ts @@ -2,10 +2,13 @@ import { describe, it, expect, afterAll } from 'vitest'; import fs from 'fs'; import os from 'os'; import path from 'path'; +import crypto from 'crypto'; import { runPipelineFromRepo } from '../../../src/core/ingestion/pipeline.js'; import type { PipelineResult } from '../../../src/types/pipeline.js'; import { decodeTaintPath } from '../../../src/core/ingestion/taint/path-codec.js'; import { fixtureTaintTotals } from '../../helpers/taint-fixture.js'; +import { isLanguageAvailable } from '../../../src/core/tree-sitter/parser-loader.js'; +import { SupportedLanguages } from '../../../src/config/supported-languages.js'; // U7 — end-to-end proof that the `--pdg` opt-in reaches BOTH sinks: the parse // worker builds a per-function CFG (workerData.pdg) and scope-resolution emits @@ -187,3 +190,356 @@ describe('U7 — end-to-end --pdg pipeline', () => { expect(cdg).toBe(0); }, 60000); }); + +// ── C-family worker-mode PDG (#2195 U7) ───────────────────────────────────── +// +// The same both-sinks proof as the TS block above, run through the REAL worker +// pipeline for each of C, C++, C#, Java, Go. Each language gets its own tiny +// repo (one hazard fixture with real branching AND a non-terminating +// loop/`select`) and we assert, under `--pdg`: +// - BasicBlock + CFG > 0 (the worker built a per-function CFG and emit wired it) +// - REACHING_DEF > 0 (the def/use harvest populates the data-dependence layer) +// - CDG > 0 AND ≥1 CDG edge is sourced INSIDE the non-terminating-loop +// function itself (`hazard`, below) — not merely an aggregate satisfied by +// any branching function in the fixture. This is the load-bearing claim: +// the post-dom/CDG pass was NOT skipped for the function whose loop traps +// EXIT, i.e. EXIT stays reverse-reachable end-to-end through the worker even +// with the non-terminating loop/`select`. (#2197 U3 — the prior whole- +// fixture `cdg > 0` aggregate did not isolate the hazard function.) +// and without `--pdg` (both the default run and an explicit `pdg:false` run): +// - BasicBlock + CFG + REACHING_DEF + CDG == 0 +// - the non-PDG graph is byte-identical between the two flag-off runs and +// matches a committed digest snapshot (the per-language byte-identical-off +// golden parity gate — R3; the cross-repo gate is pipeline-graph-golden). +// +// ⚠ Requires a FRESH `dist/parse-worker.js` — CFGs are built in the worker from +// `dist/`. A stale bundle silently zeros CFG output. `pretest:integration` (and +// the U7 verification recipe) run `node scripts/build.js` first. + +const C_FAMILY_FIXTURES = path.join(__dirname, 'fixtures'); + +// `hazard`: a substring of a BasicBlock's `text` that appears ONLY inside the +// fixture's non-terminating-loop function (`for(;;)` / `while(true)` / `for{}`). +// It locates that function's block anchor so the CDG assertion can prove the +// function specifically is CDG-bearing (see `cdgSourcedInHazardFunction`). C# +// has no such loop (its `Retry` goto-cycle is conditional and terminates), so +// it has no `hazard` and keeps the whole-fixture aggregate only. +const C_FAMILY: ReadonlyArray<{ lang: string; fixture: string; hazard?: string }> = [ + { lang: 'C', fixture: 'c-hazards.c', hazard: 'handle_request' }, // server_forever: for(;;) + { lang: 'C++', fixture: 'cpp-hazards.cpp', hazard: 'poll(' }, // run_forever: while(true) + { lang: 'C#', fixture: 'csharp-hazards.cs' }, // no non-terminating loop in the fixture + { lang: 'Java', fixture: 'java-hazards.java', hazard: 'ready(' }, // serve: while(true) + { lang: 'Go', fixture: 'go-hazards.go', hazard: 'handle(v)' }, // forInfinite: for{} +]; + +// ── Remaining-language worker-mode PDG (#2195 capstone) ───────────────────── +// +// The same both-sinks worker proof, run for the eight languages whose CFG +// visitors completed the PDG-language rollout AFTER the C-family: the dynamic +// languages (Python, PHP, Ruby), the systems/app languages (Rust, Swift, +// Kotlin, Dart), AND Vue (whose provider reuses the TypeScript CfgVisitor — the +// .vue file routes through the worker's Vue→TypeScript grammar mapping and the +// SFC +`; + const cfgs = cfgsOfSfc(sfc); + const loop = cfgs.find((c) => c.blocks.some((b) => b.text.includes('sum = sum + x'))); + expect(loop).toBeDefined(); + if (!loop) return; + + // The non-terminating loop has a back-edge but EXIT must still be reachable + // from EVERY block (the structural escape edge feeds the post-dom pass). + expect(edgeKinds(loop).has('loop-back')).toBe(true); + expect(isExitReachableFromAllBlocks(loop)).toBe(true); + + // Control dependence is computable and non-empty — the worker's CDG pass + // would emit > 0 edges for this function (matches the pipeline assertion). + const cd = computeControlDependence(loop); + expect(cd.edges.length).toBeGreaterThan(0); + for (const e of cd.edges) { + expect(['T', 'F']).toContain(e.label); + } + }); +});