mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-09 22:33:39 +00:00
* 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) <noreply@anthropic.com>
* 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) <noreply@anthropic.com>
* 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) <noreply@anthropic.com>
* 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) <noreply@anthropic.com>
* 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) <noreply@anthropic.com>
* 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) <noreply@anthropic.com>
* 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) <noreply@anthropic.com>
* 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) <noreply@anthropic.com>
* 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) <noreply@anthropic.com>
* 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) <noreply@anthropic.com>
* 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) <noreply@anthropic.com>
* 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) <noreply@anthropic.com>
* 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) <noreply@anthropic.com>
* 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) <noreply@anthropic.com>
* 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) <noreply@anthropic.com>
* feat(cfg): Vue (reuse TS visitor) + worker-mode proof for all langs (#2195 U15)
Vue SFC <script> blocks are extracted and parsed with the TS grammar
(parse-worker languageMap[Vue] = TypeScript.typescript), so wire
vueProvider.cfgVisitor = createTypeScriptCfgVisitor() -- pure reuse, no
Vue-specific visitor. vue-visitor.test.ts replicates the worker path
(extractVueScript -> TS parse -> CFG) and confirms branch edges + EXIT
reverse-reachable + CDG>0.
Extend pipeline-pdg.test.ts with a worker-mode block covering all eight
remaining languages (Python/PHP/Ruby/Rust/Swift/Kotlin/Dart/Vue): per-
language temp repo, real worker pool, BasicBlock+CFG+REACHING_DEF+CDG all
> 0 with --pdg (CDG>0 proves EXIT reverse-reachable end-to-end through the
worker despite each fixture's non-terminating loop), == 0 without. Counts
e.g. Ruby 122 BB/45 CDG, Vue 49 BB/11 CDG. 30 pipeline tests green.
COBOL: documented as the deliberate PDG non-goal (no grammar, exotic
PERFORM/GO-TO control flow) in cobol.ts + the worker-roundtrip gate.
This completes PDG language coverage: every supported language except
COBOL now builds CFG/REACHING_DEF/CDG under --pdg.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cfg): surface skippedUnsoundFunctions in per-language stats (#2195 U2)
emitFileCdg computes skippedUnsoundFunctions (functions whose CDG is
withheld because EXIT isn't reverse-reachable from all blocks) but run.ts
dropped it on the floor — only cdgEdges/cdgDropped were aggregated. Add
the aggregation + a stats-line segment so CDG coverage gaps are an
explicit signal, not silent. Establishes the baseline skip count that
makes the U1 synthetic-escape pass's effect (the drop to genuine
anomalies only) measurable.
Additive; no emit-logic change. The emit-side field is covered by
cfg-emit.test.ts (asserts skippedUnsoundFunctions===1 + the warn on a
disconnected-block CFG); the run.ts aggregation is a thin pass-through.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cfg): synthetic-escape pass restores CDG for exit-unreachable cycles (#2195 U1)
Unconditional goto-cycles (C/C++/C#/Go) wire a backward seq edge with no
structural exit-escape edge, so EXIT becomes non-reverse-reachable and
emitFileCdg silently skipped ALL control-dependence for the function.
New cfg/synthetic-escape.ts: a pure deterministic SCC routine (iterative
Tarjan, sorted adjacency) + augmentForPostDom(cfg). No-op when EXIT is
already reverse-reachable (terminating fns + visitor-escaped loops are
byte-identical — returns the same object). Otherwise it batch-bridges
every exit-less SCC by adding an ANALYSIS-ONLY escape edge from the SCC's
controlling block (highest out-degree branch; lowest-index tie-break) to
EXIT, on a shallow-cloned FunctionCfg — never mutating persisted
cfg.edges. emitFileCdg threads that augmented view through BOTH
isExitReachableFromAllBlocks AND computeControlDependence (the Ferrante
walk re-reads cfg.edges, so a tree-only augmentation would be wrong).
Precision (anti-masking): only a trapped region containing a control
point (>=2-successor block) is bridged — a branch-less trapped region
carries no recoverable control-dependence and is indistinguishable from a
genuine construction anomaly, so it stays on the skip path (the existing
disconnected-block skip test still skips, skippedUnsoundFunctions===1). A
residual non-cycle dangling block is never bridged.
repro `void handler(int a){ start: if(a>0){work();} goto start; }`:
before exitReachable=false/CDG=0 → after one synthetic 2->1 edge,
exitReachable=true, exact CDG = {2->2:T,2->2:F,2->3:T,2->4:T,2->4:F}
(pinned exactly, not CDG>0 — catches a wrong representative). AC2 property
test extended to the augmented graph; per-language goto-cycle regressions
(C/C++/C#/Go). 199 cfg tests green; bench --check fingerprints unchanged
(analysis-only, zero persisted drift).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(cfg): isolate the non-terminating-loop hazard in worker CDG asserts (#2195 U3)
The pipeline-pdg worker-mode blocks asserted a whole-fixture cdg>0
aggregate (satisfied by any branching fn) while the comment claimed it
proved the non-terminating-loop EXIT-reachability end-to-end. Add a per-
language `hazard` marker + isolate the assertion: locate the hazard
function's BasicBlocks by its anchor and assert >=1 CDG edge is sourced
within it (a marker mutation now fails the test — non-vacuous). C# keeps
the aggregate (its fixture has no infinite loop). Comments corrected.
Switch the 7 visitor unit tests (java/csharp/dart/kotlin/php/swift/c-cpp)
from the local exitReachableFromAll CFG-shape helper to the production
isExitReachableFromAllBlocks + computeControlDependence on the hazard
function, matching go/python/ruby/rust/vue. 241 unit + 30 pipeline green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(cfg): gate vendored-grammar worker assertions on isLanguageAvailable (#2195 U4)
The Swift/Kotlin/Dart worker-mode pipeline-pdg cases require a vendored
grammar prebuild that may be absent on a CI platform — they'd go red
there. Mark those three REMAINING_LANGS entries `vendored` and gate both
the --pdg-on and --pdg-off `it`s on isLanguageAvailable(SupportedLanguages
[lang]) → it.skip when the grammar can't load. Installed-grammar
languages stay unconditional. Grammars present here, so all 30 run green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(cfg): remove dead useCount() from swift + rust harvests (#2195 U5)
useCount() was declared on the local FactAccumulator in swift-harvest.ts
and rust-harvest.ts but never called (a copy-paste artifact; ruby's copy
IS used in an emit guard, so it stays). Pure deletion — the swift/rust
visitor suites stay green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(cfg): standardize harvester API table()->bindingTable() (#2195 U9)
The binding-table accessor was named table() in the C/C++/C#/Go harvests
but bindingTable() in the other 7. Rename the 4 (definitions + their
visitor call sites) to the majority name bindingTable(). Pure rename; the
4 visitor suites stay green and tsc confirms no call site was missed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(cfg): consolidate scope-tree substrate into ScopeTreeHarvester (#2195 U6)
The Go/Java/C#/C-C++ def/use harvesters each carried a byte-identical copy
of the lexical scope-tree machinery (Scope record, two-phase resolution
cache, openScope/nearestScopeOf/resolve/def/use/conditional/bindingTable,
~270 lines total). Extract it into an abstract ScopeTreeHarvester base; the
four harvesters now extend it and supply only their genuine per-language
variation (the prescan switch, plus Go's _-blank-identifier overrides of
declare/def/use). Net -422 lines. Mechanical and byte-equivalent: cfg unit
suite 613 passed, bench --check fingerprints unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(cfg): consolidate no-site def/use accumulator into DefUseAccumulator (#2195 U7)
The Kotlin/Python/Ruby/Rust/Dart/Swift harvesters each carried a
byte-identical copy of the no-site def/use accumulator (~270 lines total;
only Ruby's adds the live useCount() emit-guard helper). Extract it as an
exported DefUseAccumulator beside CallSiteFactAccumulator in
call-site-harvest.ts (the PR's own model for the with-site superset); the six
harvesters import it under their existing local FactAccumulator name. Pure
byte-equivalent move, no logic change: cfg unit suite 613 passed, tsc clean,
bench --check fingerprints unchanged (TS/Go paths untouched).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(cfg): consolidate copied visitor-test helpers into cfg-harness (#2195 U8)
The 13 *-visitor.test.ts files each copied a byte-identical set of CFG-shape
helpers (edgeKinds/block/reaches/reachable/bindingIdx/allSites/hasAnySites,
~380 lines total). Export them once from test/helpers/cfg-harness.ts and import
per file (only the subset each references). Also drop each file's local
exitReachableFromAll — a re-implementation of the production
isExitReachableFromAllBlocks (semantically identical: false iff some
entry-reachable non-EXIT block can't reach EXIT) — and point its live call
sites at the already-imported production function. Pure test-only mechanical
move, behavior-preserving: tsc clean, test/unit/cfg/ 613 passed unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(cfg): pin *-harvest.ts literals to their own grammar in the gate (#2195 U10)
The grammar-literal validation gate scans cfg/visitors/, but a <lang>-harvest.ts
basename was not in BASENAME_LANGS, so fileLanguages() fell it through to the
weak ALL_LANGS valid-if-any bucket — a node-type literal dead in its own grammar
but valid in some other grammar would pass undetected. Strip the -harvest suffix
and reuse the visitor basename map so go-harvest -> Go, c-cpp-harvest -> C+C++,
typescript-harvest -> TS, etc. The two language-agnostic harvesters
(call-site-harvest, scope-tree-harvest) name no grammar and stay valid-if-any.
Also corrects the now-inaccurate mode2Files comment. Adds a fileLanguages unit
test; the existing gate stays green (no harvest file has a dead literal), and a
scratch probe confirmed a bogus go-harvest literal is now caught.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(cfg): defensive per-statement cap on harvested taint sites (#2195 U11)
A statement's harvested sites[] had no explicit bound — a pathological or
machine-generated statement (hundreds of nested calls) could grow it without
limit. Add DEFAULT_PDG_MAX_SITES_PER_STATEMENT (512, mirroring the PDG edge/fact
cap style): openCallSite/addMemberRead check-before-push and stop at the cap,
keeping the first 512 sites fully intact and setting an observable
sitesTruncated flag. A cap-dropped openCallSite returns a -1 sentinel that
pushFrame/setSite*/the occurrence fan-out all tolerate (no dangling parent/via,
no clobber of kept sites). Generous enough that no real statement is affected:
bench --check fingerprints unchanged, cfg unit suite 617 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci(codeql): exclude nested test fixtures from the CodeQL gate (#2195)
The CodeQL results gate failed on test/integration/cfg/fixtures/python-hazards.py
('total' may be used before init, unused vars) — but that file is an intentional
CFG/PDG hazard fixture, exactly the synthetic broken-code the existing
'**/test/fixtures/**' exclusion is meant to skip. That glob does not match the
deeper test/integration/cfg/fixtures/ path, so the hazard fixtures leaked into
the scan. Add '**/test/**/fixtures/**' to cover fixtures nested anywhere under a
test tree. Analyze (python) and Analyze (javascript-typescript) both already pass
— production code is clean; this only silences fixture noise.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style(cfg): apply prettier + drop unused imports across the PDG files (#2195)
The merge of main into this branch pulled in the stricter quality gates
(prettier --check . and eslint .), which surfaced pre-existing formatting in the
PDG/CFG rollout (line-width wrapping across the visitor + harvest files, bench,
tests) plus 9 no-unused-imports errors. Mechanical autofix only — npm run
format + lint:fix equivalent, scoped to gitnexus/: removes unused FunctionCfg/
SiteRecord type imports left by the U8 helper consolidation and stale
FinalizerFrame imports in python.ts/ruby.ts. No behavior change: tsc clean, cfg
unit suite 617 passed, eslint 0 errors. (gitnexus-web class-order noise is a
local tailwind-plugin artifact CI does not flag — left untouched.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cfg): harvest C++ structured-binding defs (auto [a,b]=e) (#2195)
The C++ def/use harvester only recorded a def when an init_declarator's
declarator was a plain identifier, so a structured binding (auto [a,b] = mk(),
incl. the auto& reference form whose binding sits under a reference_declarator)
declared only the first name in phase 1 and emitted ZERO defs in phase 2 — a,b
were walked as spurious uses and later use(a)/use(b) resolved to a synthetic
module binding, silently corrupting REACHING_DEF/taint for an idiomatic C++17
shape. Unwrap the structured_binding_declarator in both phases and def every
identifier leaf; result-of-initializer flows to the whole list. Inert for C
(no structured bindings). Characterization tests added (plain + reference form).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cfg): model C++ co_return as a return terminator to EXIT (#2195)
co_return_statement was neither in CPP_CONTROL_FLOW_TYPES nor dispatched, so a
coroutine's co_return coalesced into a straight-line block and emitted a
spurious seq fallthrough to the following statement instead of an edge to EXIT
— statements after co_return looked reachable and the terminator edge was
missing, corrupting CFG/CDG for coroutines. Add the node type to the C++
control-flow set and dispatch it through visitReturn (block -> EXIT 'return',
no fallthrough). C path untouched; co_await/co_yield remain plain expressions.
Characterization test added; c-cpp suite + grammar-literal gate green, bench
--check unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cfg): harvest C# out-var and deconstruction-declaration defs (#2195)
Two idiomatic C# write shapes recorded ZERO defs, silently breaking
REACHING_DEF/taint:
- out-var (G(out var n) / G(out int n)) parses as a declaration_expression;
it was neither declared (phase 1) nor def'd (phase 2), so n resolved to a
synthetic module binding and the callee-written value had no reaching def.
- deconstruction declaration (var (a, b) = T()) has a variable_declarator whose
name slot is a tuple_pattern (null name field), so declareVariableDeclaration
+ the variable_declaration walk skipped it entirely (only the assignment form
(a,b)=T() was handled). Both a and b were dropped.
Declare + def the declaration_expression's identifier (must-def: out params are
definitely-assigned), and route a null-name variable_declarator through the
tuple_pattern via the existing declareForeachTarget/defTupleTargets helpers.
Characterization tests added; csharp suite 42 passed, grammar gate + bench
--check green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cfg): put embedded-script CFGs in file coordinates via lineOffset (#2195)
A Vue SFC <script> block parses at row 0 but lives at lineOffset in the .vue
file. Every other worker-emitted graph node adds lineOffset to reach file
coordinates, but collectFunctionCfgs built FunctionCfgs from the extracted
script's raw rows and never offset them. Two consequences for .vue files:
- inter-procedural taint silently resolved NOTHING — the summary-harvest join
keys graph Function/Method nodes by their (offset) startLine but looked up the
CFG's (unoffset) functionStartLine, missing by exactly lineOffset, so no
FunctionSummary was ever produced;
- persisted BasicBlock startLine/endLine (and the id's functionStartLine
segment) pointed at the wrong .vue line, breaking source mapping.
Thread lineOffset into collectFunctionCfgs and shift every CFG source-line field
(functionStartLine/End, block start/end, statement + non-synthetic binding
lines) into file coordinates at the one production chokepoint. A 0 offset
returns the CFG unchanged, so .ts/.js/etc. stay byte-identical (bench --check
fingerprints unchanged; worker-roundtrip + pipeline-pdg green). Unit tests for
the shift + the 0-offset no-op added.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(cfg): surface CDG soundness skips at warn, not just debug (#2195)
skippedUnsoundFunctions (a function whose EXIT is not reverse-reachable from
all blocks, so control dependence is withheld) was only reported inside the
per-language logger.debug stats line — while the taint/RD coverage-gap and
cap-drop counts surface unconditionally at warn. A language that systematically
trapped EXIT (an unmodeled non-terminating / multi-terminal shape the
synthetic-escape pass can't bridge) would silently lose all CDG. Add a parallel
unconditional warn (R8) alongside the R4 taint-gap warn. Observability only —
no graph change; emit-layer skip counting stays covered by cfg-emit's
skippedUnsoundFunctions test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cfg): harvest Kotlin x++/--x as a def (#2195)
The Kotlin harvester had no postfix_expression/prefix_expression case, so an
increment/decrement fell to the default descent and recorded its operand as a
use only — never a def. Every sibling harvester (Java/C#/C++/Dart/TS/PHP) models
inc/dec, so a Kotlin counting loop (while/for using i++) silently dropped the
loop-carried reaching-def of the counter. Add the case: def AND use the operand
when it is a plain simple_identifier and the operator is ++/-- (other pre/postfix
forms — -x, !x, x!!, x? — stay pure reads, byte-identical to the old descent).
Characterization tests for postfix + prefix added; kotlin suite 30 passed,
grammar gate + bench --check green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cfg): harvest Go select channel-receive binding as a def (#2195)
walkValue had no receive_statement case, so a select receive (case v := <-ch:)
fell to the default descent: v was recorded as a USE of an uninitialized var
and the channel-sourced definition was invisible to REACHING_DEF/taint —
channels are a primary taint source in Go. Add the case mirroring
short_var_declaration: def each left identifier, use the <-ch right, attach
resultDefs for the := short form. prescan already declared the binding; this
completes the phase-2 fact. go:branchy bench fingerprint unchanged; go suite
40 passed, grammar gate green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cfg): harvest all names of a Dart multi-variable declaration (#2195)
`var a = 1, b = 2;` is one initialized_variable_definition whose first binding
is the name/value field pair and whose subsequent bindings are trailing
initialized_identifier children. Both prescan (declareInitializedVar) and the
walkValue case read only the name/value fields, so every name after the first
was never declared or def'd — `b` resolved to a synthetic module binding and
its REACHING_DEF/taint flow was lost. Iterate the trailing initialized_identifier
nodes in both phases. (Dart-3 record/list pattern declarations `var (a,b)=pair`
remain a separate follow-up.) dart suite 35 passed, tsc + grammar gate green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cfg): bind Swift switch-case value patterns (case let n) (#2195)
A switch value-binding (case let n where …, case .some(let v)) was never
declared in prescan, so n/v resolved to a synthetic module binding and a body
use(n) did not link to any def — a very common Swift idiom silently lost its
data dependence. Declare the switch_pattern's bindings (prescan, reusing
declarePattern) and emit them as MAY-defs on the dispatch block (a case may not
match) via a new switchPatternFacts, propagated into the case body. swift suite
25 passed, tsc + grammar gate green. (The rare ?? / ternary-arm may-def — Swift
assignment-as-expression — remains a separate follow-up.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cfg): wire Swift multi-catch throw edges to every handler (#2195)
visitDo routed the protected body's throw edge only to handlerEntries[0], so a
do { try r() } catch A {} catch {} left the 2nd..Nth catch handlers UNREACHABLE
from ENTRY — orphaned blocks whose error bindings + def/use facts were stranded
in a dead component (a soundness gap for idiomatic Swift typed multi-catch).
Swift tries the catch clauses in order and the thrown type is unknown at CFG
time, so every protected block may reach ANY clause: edge each protected block
to every handlerEntry. Found by the per-language CFG/CDG verification swarm
(reproduced: 2-catch=1, 3-catch=3 unreachable blocks). swift suite 26 passed,
tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cfg): synthesize a protected block for an empty Kotlin try {} (#2195)
An empty `try {}` body produced zero protected blocks, so visitTry's throw-edge
loop wired nothing to the catch and the try's entry fell through to the finally
— leaving the catch handler block + its error binding orphaned (unreachable from
ENTRY), a malformed CFG with stranded def/use facts. Mirror the existing
empty-`catch` synthesis: when the try body is empty and there is a catch or
finally, synthesize one protected block so the catch handler(s) are wired and
the try entry is the body, not the finally. Found by the per-language CFG
verification swarm. Non-empty try is byte-identical; kotlin suite 31 passed,
tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* perf(cfg): bound the reaching-defs fixpoint with a per-block visit ceiling (#2195)
The per-language verification swarm reproduced, AT PRODUCTION DEFAULTS, a
reaching-defs blow-up: a machine-generated ~2000-line all-loops function (under
DEFAULT_PDG_MAX_FUNCTION_LINES) reaches ~10k basic blocks because loops emit ~5
blocks/line, and the dataflow fixpoint is O(blocks^2.3) on deep loop nests —
measured 62s (C/C++) and 2.05s + 810MB (Go) for ONE function. maxFacts does not
help: the fact count stays LINEAR, so it never fires.
Iterative reaching-defs on a reducible CFG converges in O(loop-nesting-depth)
passes, so a worklist re-visits each block a small multiple of times for real
code. Add a maxBlockVisits ceiling (emit passes blocks.length × 64 — far beyond
any hand-written nesting depth, ~15) that bails when the fixpoint has not
converged. An unconverged fixpoint's in/out sets are not sound, so it returns
NO facts (status 'truncated', like the existing 'overflow' guard) — a per-
function coverage gap, never wrong facts. Real code is byte-identical: full cfg
suites 725 passed, bench --check fingerprints unchanged.
NOTE: computeControlDependence's O(N²) up-walk on deep post-dom chains is the
sibling concern but stays ~13ms in production (bounded by the line cap + the
CDG materialization cap); a CDG work-budget is a documented follow-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cfg): raise the parse-worker stack limit for deep CFG recursion (#2195)
The CFG visitors build per-function control-flow graphs by recursive descent
over the tree-sitter AST, so deeply-nested source overflows the worker thread's
call stack (~1.5k nesting levels) — caught per-function (R4 try/catch) but the
function silently gets no PDG. A worker thread's stack is governed by
resourceLimits.stackSizeMb (Node default 4 MB); the main process's
--stack-size=4096 flag does NOT propagate to worker threads (confirmed by prior-
art research on Node worker_threads). Raise it to 16 MB, pushing the overflow
threshold to several-thousand nesting levels — far beyond any hand-written code,
so only machine-generated/obfuscated nesting can still hit it (and that stays a
caught per-function skip, never a crash). Complements a future proactive depth
guard. pipeline-pdg worker tests 30 passed, tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* perf(cfg): compute control dependence as a reverse-CFG dominance frontier (#2195)
The Ferrante §3.1.1 up-walk re-climbed the ipdom chain once per CFG edge,
which is Θ(N²) on a deep post-dom chain (a single branch fanning into a
shared spine took ~7.1s at 16k blocks). Replace it with the reverse-CFG
post-dominance-frontier formulation (Cytron, Ferrante, Rosen, Wegman &
Zadeck 1991): control dependence IS the dominance frontier of the reverse
CFG, computed bottom-up over the post-dom tree (PDF_local from a node's CFG
in-edges + PDF_up from its post-dom-tree children) in O(N + E + output).
LLVM (ReverseIDFCalculator), Joern (CdgPass) and WALA use the same form.
Output is the IDENTICAL deduped/sorted (controller, dependent, label) set:
verified byte-identical across all cfg unit+integration suites, the
cdg-snapshot oracle, and bench --check fingerprints (unchanged). The PDF
unions a label SET per (controller, dependent) pair, preserving the
multi-label rows the old per-row dedup kept on opposite-sense (goto-cycle)
arms. buildArmSenses, labelFor, the final sort and the maxEdges truncation
cap are kept verbatim; the post-order walk is iterative so a chain-deep
post-dom forest cannot overflow the stack.
Adds three regressions: multi-label-per-pair preservation, the literal
self-edge / NO_IPDOM seed guard (a !== x), and a fan-into-chain perf
tripwire (linear vs the former quadratic up-walk).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(cfg): record the reaching-defs WTO no-go decision (#2195)
Weak-topological-order / loop-aware iteration (Bourdoncle 1993) was
evaluated as the fix for the O(blocks²) deep-loop-nest blow-up and
rejected: a faithful WTO solver was 104/104 byte-identical to the RPO
worklist but 0% faster — the cost is inherent dense-set propagation +
lattice merges, not visitation order, and the loop-body-skip shortcut is
unsound on irreducible (goto) CFGs. Document this at the RPO-order site
and the emit.ts revisit-ceiling constant so the shipped blocks×64 bound
reads as the sound backstop it is, with SSA-sparse reaching-defs named as
the deferred real fix. Comment-only; no behavior change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(cfg): proactive visitor nesting-depth guard + observable CFG skips (#2195)
The CFG visitors are recursive-descent with no shared base, so a
pathologically nested function (machine-generated / adversarial) could
overflow the worker's native stack — a nondeterministic RangeError that
escaped to the language-group catch and silently dropped EVERY remaining
file's CFG.
Guard it proactively: CfgBuilder tracks live recursive-descent nesting
depth via enterNesting/exitNesting, called at each visitor's visitBody and
visitSeq choke points (visitBody covers nested control constructs incl.
else-if ladders; visitSeq covers deeply-nested bare blocks). Exceeding
MAX_CFG_NESTING_DEPTH (500, far below the ~1.2k+ native limit and far above
real code's ≤~50) throws a typed, DETERMINISTIC CfgNestingDepthError instead
of waiting for the engine's nondeterministic overflow.
collectFunctionCfgs now isolates the build PER FUNCTION: the depth bail or
any other throw is caught, counted, and skipped — one bad function no longer
loses the whole file's CFGs. CollectedCfgs.skipped widens from a bare number
to reason-counted buckets (tooManyLines / tooDeeplyNested / buildError). The
worker stops discarding that count (parse-worker.ts), aggregates it
per-language onto ParseWorkerResult.cfgSkipped (survives the parse cache via
slim's `...result`), and mergeChunkResults merges + warns per-language so a
CFG coverage gap is observable, not silent.
Behavior-preserving on normal code: the guard never fires below 500 nesting,
so the cfg unit+integration suites (731), the CDG/RD/CFG snapshots and the
bench --check fingerprints are all byte-identical. The worker stackSizeMb
4→16MB bump shipped earlier (de4c43a4).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(cfg): unify the nesting guard behind CfgBuilder.withNesting (#2195)
Tri-review flagged the visitBody/visitSeq guard asymmetry (visitBody used
try/finally; visitSeq placed a bare exitNesting before its tail return). It
is not a bug today — the CfgBuilder is per-function and discarded on a bail,
so a leaked counter is never read — but a future mid-loop return in visitSeq
would silently corrupt the depth count. Replace both hand-paired sites in all
12 visitors with a single `CfgBuilder.withNesting(fn)` helper that enters on
the way in and exits in a finally, so the pair can never drift. Also document
that block-bodied constructs pass through BOTH choke points, so the effective
lexical ceiling is ~MAX_CFG_NESTING_DEPTH/2 (~250).
Behavior-preserving: 733 cfg tests + bench --check fingerprints byte-identical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cfg): include cfgSkipped in all parse-worker result initializers (#2195)
Tri-review found cfgSkipped omitted from the two reset/fallback
ParseWorkerResult initializers (only the main one carried it). The field is
optional with `?? {}` reads so there is no runtime bug, but the zero-state
initializers should be complete and consistent. Also correct the field's
doc-comment: the per-language merge + warn lives in `dispatchChunkParse`
(alongside skippedLanguages), not `mergeChunkResults` — and, like that
sibling telemetry, the warn fires for freshly-parsed chunks, not on a warm
cache hit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(cfg): scope the CDG byte-identical claim to the untruncated output (#2195)
Tri-review noted the rewrite's `maxEdges` truncation now trims the SORTED
edge set, whereas the old up-walk broke mid-walk in CFG-edge-iteration order
— so a truncated PREFIX can differ at the cap boundary (only when a single
function exceeds maxEdges; the FULL untruncated set is byte-identical, now
also confirmed by ~1M-case differential fuzz). Clarify the module doc and the
maxEdges param doc: the cap bounds OUTPUT count (peak working set ≈ output in
the DF formulation, not the old pre-dedup spike), and the byte-identical
guarantee is scoped to the untruncated output. Comments only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(cfg): strengthen depth-guard, perf-tripwire and buildError coverage (#2195)
Tri-review test-quality findings:
- cfg-builder: capture the thrown CfgNestingDepthError unconditionally (a
catch-only assertion silently passes if a future change stops throwing);
add a withNesting test that the counter balances on the THROW path too.
- control-dependence perf tripwire: assert controller IDENTITY (every edge
controlled by block 0, distinct dependents in range), not just length, so a
fast-but-wrong reimplementation can't pass on the M-1 count alone.
- worker-roundtrip: add the missing buildError test — a generic (non-depth)
buildFunctionCfg throw is caught per function, counted under buildError, and
does NOT drop the file's sibling CFGs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cfg): model Kotlin value-position when/if as control dependence (#2205)
Idiomatic Kotlin uses `if`/`when` as EXPRESSIONS (`val x = when (k) { … }`,
`return if (c) a else b`, `fun f() = when (k) { … }`), but the visitor only
modeled them as control flow in statement position (the `isStatementPosition`
gate) — value-position branches collapsed into one straight-line block, so
their arms emitted no control dependence. Cross-language PDG validation
(#2195, PR #2197) measured the result: two Kotlin repos at 3% / 6% CDG per
BasicBlock vs 18–76% for every other language (incl. the other
expression-conditional languages, Rust 30% / Swift 24%).
Model value-position `when` (≥2 arms) and `if`/`else` as control flow in the
three dominant carriers — `property_declaration` (rejoin the arms at a
binding continuation carrying the bound name's def), `return`, and the
`fun f() = …` expression body (each arm returns) — mirroring the Rust
visitor's value-position `let` handling. `visitWhen`/`visitIf` are reused
unchanged; `isControlFlow` now routes a value-branch `val`/`var` decl to the
branch handler instead of coalescing it.
Measured: Exposed CDG 3636→5644 (+55%, 6%→9% of BasicBlocks), turbine
67→82 (+22%). Argument-position branches, assignment RHS, and value-position
`try` are left inline — a remaining gap tracked on #2205.
Behavior-preserving for non-Kotlin (bench --check byte-identical; 739 cfg
tests incl. 6 new value-position regressions).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cfg): model Ruby value-position if/case on assignment RHS as control dependence (#2195)
`if`/`case` are expressions in Ruby; as an `assignment` RHS
(`x = if c then a else b end`, `x = case k … end`) the visitor previously
left the arms INLINE in one coalesced block (a gap documented at ruby.ts
§"if/case/begin are EXPRESSIONS"), emitting no control dependence. Model the
RHS branch as control flow and bind the LHS at the rejoin: `assignmentBranch`
detects the carrier, `visitSeq` routes it out of the coalescing path, and
`visitBindBranch` reuses `visitIf`/`visitCase` + a facts-only continuation
carrying the LHS def (new `harvest.assignmentDefFacts`). Mirrors the Kotlin
(#2205) and Rust value-position handling.
Honest impact: SMALL in practice — rack CDG 1269→1303, sinatra 1212→1232
(~+2–3%). `x = if/case` is far rarer in idiomatic Ruby than the Kotlin
analog (Ruby favors ternary / `||=` / guard modifiers), and Ruby's low
CDG/BB is mostly structural (micro-branches `&&`/`||`/`?:`/`&.` excluded by
design, plus many straight-line `.each`/`.map` block CFGs). This closes the
documented gap correctly; it is not a large ratio mover. Explicit
`return if … end` is NOT a carrier — tree-sitter-ruby drops that value; the
idiomatic implicit-last-expression conditional was already modeled.
Behavior-preserving for non-Ruby (bench --check byte-identical; 744 cfg
tests incl. 5 new value-position regressions).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cfg): keep an empty-arm Kotlin when wired to the join (#2195)
An all-empty-arm `when` with an `else` — `when(k){0->{};else->{}}`, idiomatic
`else -> {}` "do nothing" — left the dispatch block with ZERO successors:
empty arms got no `switch-case` edge, and the `else` suppressed the no-match
edge. The dispatch and its join then became orphaned, so
isExitReachableFromAllBlocks returned false and emitFileCdg silently dropped
the ENTIRE function's control dependence (counted cdgSkippedUnsound). The
#2205 value-position fix newly routes `val x = when(…)` / `return when(…)` /
`fun f() = when(…)` through visitWhen, exposing it in those carriers too.
Wire every arm (empty or not) to the join, so the dispatch always has a
successor. Found by the per-language CFG verification swarm. Behavior-
preserving elsewhere (bench --check byte-identical; cfg suite green).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cfg): wire C/C++ throw edges to every catch handler, not just the first (#2195)
`visitTry` edged every protected-region block only to `handlerEntries[0]`, so
in a multi-`catch` (`catch(int e){…} catch(double d){…} catch(...){…}`) the
2nd..Nth handlers were orphaned — unreachable from ENTRY, their catch-param
binding and body control/data flow silently lost. The runtime catch that
matches a thrown type is not statically known, so over-approximate: edge each
protected block to EVERY handler entry (mirrors the Swift multi-catch
handling). Found by the per-language CFG verification swarm; the two existing
exception tests both used a single catch, so it was never exercised.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cfg): keep a bare continue in a Dart switch case — it targets the loop (#2195)
`caseStatements` stripped EVERY `continue_statement` from a case body, but only
a LABELED `continue LABEL;` is a switch fallthrough-spill (handled via
caseContinueLabel). A bare `continue;` targets the ENCLOSING LOOP (valid Dart);
dropping it removed the jump and fabricated a false case → next-statement
fall-through edge (e.g. `case 1: tainted(); continue; default: sink();` made
tainted() flow directly into sink()). Only strip the labeled form; a bare
`continue;` stays in the body and routes to the loop. Found by the per-language
CFG verification swarm.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cfg): harvest Rust match-arm pattern bindings as defs (taint propagation) (#2206)
`visitMatch` visited arm bodies but never harvested the arm PATTERN's bindings,
so `match x { Some(n) => sink(n) }` left `n` with a use and no def/may-def —
taint from the matched subject could not propagate into the arm. Add
`matchArmPatternFacts` (the binders as MAY-defs, since only the matching arm
binds) and attach it to the dispatch block, co-located with the subject's use.
Found by the per-language CFG verification swarm; the match tests asserted
`hasUse` but never `hasDef`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cfg): harvest Swift guard case / if case enum-pattern bindings as locals (#2206)
`guard case .some(let v) = e` / `if case let .x(n) = e` nest the binder inside a
`pattern` condition child, not a direct `bound_identifier`. Both the declaration
(declareOptionalBindings) and the def-facts (conditionFacts, which ran walkValue
= a USE on the pattern) missed it, so the binding resolved to a synthetic
`@module` global with a use and no def — breaking taint propagation from the
subject. Declare the `pattern` child and def its leaves (may-def when
conditional). Found by the per-language CFG verification swarm.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cfg): treat Dart switch-expression arm writes as may-defs, not hard kills (#2206)
`DartHarvester.walkValue` had no `switch_expression` case, so an assignment in an
arm value — `var y = switch(x){ 1 => z = 10, _ => z = 20 }` — became an
unconditional def that KILLED the prior `z`, even though only one arm runs (the
module docstring claimed it was a may-def, but the code didn't implement it).
Walk the subject always and each `switch_expression_case` under `conditional(…)`,
so arm writes are may-defs — mirroring `conditional_expression`. Found by the
per-language CFG verification swarm.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cfg): model the C# `using var` declaration-form dispose finalizer (#2206)
`using var f = Open();` (C# 8) parses as a `local_declaration_statement` with a
leading `using` keyword, not a `using_statement`, so visitUsing never ran — no
dispose block, and a `return`/`break`/`continue` in its scope got no
`finally-*` completion edge. Unlike the delimited block form, its dispose runs
at ENCLOSING-SCOPE exit, so visitSeq now treats the REST of the sequence as the
protected body: the acquisition (`var f = e`) is a normal block outside the
dispose region (a throw there means the resource was never acquired), and
`buildUsingDeclScope` wraps the remainder in a synthetic dispose finalizer
(normal + exception exit, early exits thread through) — mirroring
buildProtectedSynthetic. Closes the last #2206 item. Found by the per-language
CFG verification swarm.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
423 lines
19 KiB
TypeScript
423 lines
19 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest';
|
|
import { createRequire } from 'node:module';
|
|
import { requireVendoredGrammar } from '../../../src/core/tree-sitter/vendored-grammars.js';
|
|
import {
|
|
createCCfgVisitor,
|
|
createCppCfgVisitor,
|
|
} from '../../../src/core/ingestion/cfg/visitors/c-cpp.js';
|
|
import {
|
|
makeCfgHarness,
|
|
type CfgHarness,
|
|
block,
|
|
edgeKinds,
|
|
reaches,
|
|
reachable,
|
|
bindingIdx,
|
|
allSites,
|
|
hasAnySites,
|
|
} from '../../helpers/cfg-harness.js';
|
|
import { isExitReachableFromAllBlocks } from '../../../src/core/ingestion/cfg/post-dominators.js';
|
|
import { augmentForPostDom } from '../../../src/core/ingestion/cfg/synthetic-escape.js';
|
|
import { computeControlDependence } from '../../../src/core/ingestion/cfg/control-dependence.js';
|
|
|
|
// U2 — the C/C++ CfgVisitor, one hazard per test (KTD5: real-parser regression,
|
|
// NOT snapshot-pinning). Each fixture's distinctive statement text (step(),
|
|
// done(), handle(e), …) lets us locate the block for a region by text and assert
|
|
// the control-flow topology around it.
|
|
|
|
const cGrammar = requireVendoredGrammar('tree-sitter-c') as Parameters<typeof makeCfgHarness>[0];
|
|
const cppGrammar = createRequire(import.meta.url)('tree-sitter-cpp') as Parameters<
|
|
typeof makeCfgHarness
|
|
>[0];
|
|
|
|
const c: CfgHarness = makeCfgHarness(cGrammar, createCCfgVisitor(), 'fixture.c');
|
|
const cpp: CfgHarness = makeCfgHarness(cppGrammar, createCppCfgVisitor(), 'fixture.cpp');
|
|
|
|
describe('C CfgVisitor — structure', () => {
|
|
it('straight-line body: ENTRY → block → EXIT (seq)', () => {
|
|
const cfg = c.cfgOf(`void f() { a(); b(); c(); }`);
|
|
expect(cfg.blocks.filter((b) => b.kind === 'normal')).toHaveLength(1);
|
|
const body = block(cfg, 'a();');
|
|
expect(cfg.edges).toContainEqual({ from: cfg.entryIndex, to: body, kind: 'seq' });
|
|
expect(reaches(cfg, body, cfg.exitIndex)).toBe(true);
|
|
});
|
|
|
|
it('empty body: ENTRY → EXIT', () => {
|
|
const cfg = c.cfgOf(`void f() {}`);
|
|
expect(cfg.blocks).toHaveLength(2);
|
|
expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true);
|
|
});
|
|
|
|
it('malformed/unmodeled body returns undefined without throwing', () => {
|
|
// A forward-declaration prototype (`int f(int);`) has no compound_statement
|
|
// body — buildFunctionCfg must return undefined rather than throw.
|
|
const root = c.parse(`int f(int);`);
|
|
const fns = c.collectFunctions(root);
|
|
// No function_definition (only a declaration) → nothing to build.
|
|
expect(fns).toHaveLength(0);
|
|
// And a body-less function node yields undefined, not a throw.
|
|
const decl = c.parse(`void g() { x(); }`);
|
|
const fn = c.collectFunctions(decl)[0];
|
|
expect(() => createCCfgVisitor().buildFunctionCfg(fn, 'f.c')).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('C CfgVisitor — branching', () => {
|
|
it('if/else: cond-true to then, cond-false to else, both reach the join', () => {
|
|
const cfg = c.cfgOf(`void f(int x) { if (x) { a(); } else { b(); } c(); }`);
|
|
const kinds = edgeKinds(cfg);
|
|
expect(kinds.has('cond-true')).toBe(true);
|
|
expect(kinds.has('cond-false')).toBe(true);
|
|
const join = block(cfg, 'c();');
|
|
expect(reaches(cfg, block(cfg, 'a();'), join)).toBe(true);
|
|
expect(reaches(cfg, block(cfg, 'b();'), join)).toBe(true);
|
|
});
|
|
|
|
it('plain if (no else): condition reaches both the body and the join', () => {
|
|
const cfg = c.cfgOf(`void f(int x) { if (x) { a(); } b(); }`);
|
|
const cond = block(cfg, 'x');
|
|
expect(reaches(cfg, cond, block(cfg, 'a();'))).toBe(true);
|
|
expect(reaches(cfg, cond, block(cfg, 'b();'))).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('C CfgVisitor — loops', () => {
|
|
it('while loop: header + back-edge + exit', () => {
|
|
const cfg = c.cfgOf(`void f(int x) { while (x > 0) { step(); } done(); }`);
|
|
const header = block(cfg, 'x > 0');
|
|
const body = block(cfg, 'step();');
|
|
expect(cfg.edges).toContainEqual({ from: body, to: header, kind: 'loop-back' });
|
|
expect(edgeKinds(cfg).has('cond-true')).toBe(true);
|
|
expect(reaches(cfg, header, block(cfg, 'done();'))).toBe(true);
|
|
});
|
|
|
|
it('do-while runs the body BEFORE testing, then loops back from the bottom', () => {
|
|
const cfg = c.cfgOf(`void f(int x) { do { step(); } while (x > 0); done(); }`);
|
|
const body = block(cfg, 'step();');
|
|
const cond = block(cfg, 'x > 0');
|
|
expect(reaches(cfg, cfg.entryIndex, body)).toBe(true); // body runs first
|
|
// The back-edge / condition tests at the BOTTOM (cond reachable from body).
|
|
expect(reaches(cfg, body, cond)).toBe(true);
|
|
expect(cfg.edges).toContainEqual({ from: cond, to: body, kind: 'loop-back' });
|
|
expect(reaches(cfg, cond, block(cfg, 'done();'))).toBe(true);
|
|
});
|
|
|
|
it('C-style for: init once, condition header, back-edge through increment', () => {
|
|
const cfg = c.cfgOf(`void f() { for (int i = 0; i < n; i++) { step(); } done(); }`);
|
|
const init = block(cfg, 'int i = 0');
|
|
const header = block(cfg, 'i < n');
|
|
const incr = block(cfg, 'i++');
|
|
const body = block(cfg, 'step();');
|
|
expect(cfg.edges).toContainEqual({ from: cfg.entryIndex, to: init, kind: 'seq' });
|
|
expect(reaches(cfg, body, incr)).toBe(true);
|
|
expect(cfg.edges).toContainEqual({ from: incr, to: header, kind: 'loop-back' });
|
|
expect(reaches(cfg, header, block(cfg, 'done();'))).toBe(true);
|
|
});
|
|
|
|
it('for(;;) {} keeps EXIT reverse-reachable AND emits CDG > 0', () => {
|
|
// The header has a cond-false escape to the loop-exit even with no condition;
|
|
// the inner `if` is a real control point. Assert through the production
|
|
// post-dom/CDG passes (matching go/python/ruby/rust/vue) — CDG is only
|
|
// computed when EXIT stays reverse-reachable, so a non-empty CDG proves the
|
|
// structural exit-escape edge keeps the function CDG-bearing.
|
|
const cfg = c.cfgOf(`void f(int x) { for (;;) { if (x) { g(); } } }`);
|
|
expect(edgeKinds(cfg).has('cond-false')).toBe(true);
|
|
expect(isExitReachableFromAllBlocks(cfg)).toBe(true);
|
|
expect(computeControlDependence(cfg).edges.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe('C CfgVisitor — switch (C-style fallthrough)', () => {
|
|
it('a case without break falls into the next case (switch-case + fallthrough)', () => {
|
|
const cfg = c.cfgOf(`void f(int x) {
|
|
switch (x) {
|
|
case 1: one();
|
|
case 2: two(); break;
|
|
default: other();
|
|
}
|
|
after();
|
|
}`);
|
|
expect(edgeKinds(cfg).has('switch-case')).toBe(true);
|
|
expect(edgeKinds(cfg).has('fallthrough')).toBe(true);
|
|
// case 1 (no break) FALLS THROUGH into case 2.
|
|
expect(reaches(cfg, block(cfg, 'one();'), block(cfg, 'two();'))).toBe(true);
|
|
// both cases reach the post-switch continuation.
|
|
expect(reaches(cfg, block(cfg, 'two();'), block(cfg, 'after();'))).toBe(true);
|
|
});
|
|
|
|
it('break-terminated case does not fall into the next case', () => {
|
|
const cfg = c.cfgOf(`void f(int x) {
|
|
switch (x) { case 1: one(); break; case 2: two(); break; }
|
|
after();
|
|
}`);
|
|
expect(reaches(cfg, block(cfg, 'one();'), block(cfg, 'two();'))).toBe(false);
|
|
expect(reaches(cfg, block(cfg, 'one();'), block(cfg, 'after();'))).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('C CfgVisitor — goto / labels', () => {
|
|
it('backward goto wires to an already-seen label block', () => {
|
|
const cfg = c.cfgOf(
|
|
`void f() { int i = 0; loop: work(); i++; if (i < 10) goto loop; done(); }`,
|
|
);
|
|
const gotoB = block(cfg, 'goto loop;');
|
|
const label = block(cfg, 'work();');
|
|
expect(reaches(cfg, gotoB, label)).toBe(true);
|
|
expect(cfg.edges.some((e) => e.from === gotoB && e.to === label)).toBe(true);
|
|
});
|
|
|
|
it('forward goto wires to a label that appears later', () => {
|
|
const cfg = c.cfgOf(`void f(int x) { if (x) goto end; work(); end: done(); }`);
|
|
const gotoB = block(cfg, 'goto end;');
|
|
const label = block(cfg, 'done();');
|
|
expect(reaches(cfg, gotoB, label)).toBe(true);
|
|
// the goto skips work() on its path.
|
|
expect(reachable(cfg, block(cfg, 'work();'))).toBe(true);
|
|
});
|
|
|
|
it('goto to an UNDEFINED label routes to EXIT (single-exit preserved) and warns', () => {
|
|
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
try {
|
|
const cfg = c.cfgOf(`void f() { work(); goto missing; }`);
|
|
const gotoB = block(cfg, 'goto missing;');
|
|
expect(reaches(cfg, gotoB, cfg.exitIndex)).toBe(true);
|
|
expect(warn).toHaveBeenCalled();
|
|
} finally {
|
|
warn.mockRestore();
|
|
}
|
|
});
|
|
|
|
// #2197 U1 — an UNCONDITIONAL goto-cycle traps EXIT (the `goto start` has no
|
|
// exit path), so without the synthetic-escape pass `emitFileCdg` would withhold
|
|
// ALL control dependence. After the pass the cycle is bridged and CDG is
|
|
// emitted. The conditional goto tests above already had an exit path (the
|
|
// if-false arm reaches `done()`), so they did NOT exercise this gap.
|
|
it('unconditional goto-cycle: bridged → EXIT reachable AND CDG emitted (C)', () => {
|
|
const cfg = c.cfgOf(`void handler(int a){ start: if(a>0){work();} goto start; }`);
|
|
expect(isExitReachableFromAllBlocks(cfg)).toBe(false); // trapped without the pass
|
|
const view = augmentForPostDom(cfg);
|
|
expect(isExitReachableFromAllBlocks(view)).toBe(true);
|
|
expect(computeControlDependence(view).edges.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('unconditional goto-cycle: bridged → EXIT reachable AND CDG emitted (C++)', () => {
|
|
const cfg = cpp.cfgOf(`void handler(int a){ start: if(a>0){work();} goto start; }`);
|
|
expect(isExitReachableFromAllBlocks(cfg)).toBe(false);
|
|
const view = augmentForPostDom(cfg);
|
|
expect(isExitReachableFromAllBlocks(view)).toBe(true);
|
|
expect(computeControlDependence(view).edges.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe('C CfgVisitor — def/use harvest', () => {
|
|
it('int x = a + b; use(x); produces a def of x and a use in the consumer', () => {
|
|
const cfg = c.cfgOf(`void f(int a, int b) { int x = a + b; use(x); }`);
|
|
const x = bindingIdx(cfg, 'x');
|
|
// x is defined somewhere…
|
|
const defined = cfg.blocks.some((bl) => bl.statements?.some((s) => s.defs.includes(x)));
|
|
expect(defined).toBe(true);
|
|
// …and used somewhere.
|
|
const used = cfg.blocks.some((bl) => bl.statements?.some((s) => s.uses.includes(x)));
|
|
expect(used).toBe(true);
|
|
});
|
|
|
|
it('if (a && (x = f())) records x as a MAY-def, not a must-kill', () => {
|
|
const cfg = c.cfgOf(`void f(int a) { int x = 0; if (a && (x = g())) h(x); }`);
|
|
const x = bindingIdx(cfg, 'x');
|
|
const hasMayDef = cfg.blocks.some((bl) =>
|
|
bl.statements?.some((s) => (s.mayDefs ?? []).includes(x)),
|
|
);
|
|
expect(hasMayDef).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('C++ CfgVisitor — structured bindings (#2195 P1)', () => {
|
|
const isDef = (cfg: FunctionCfg, idx: number): boolean =>
|
|
cfg.blocks.some((bl) => bl.statements?.some((s) => s.defs.includes(idx)));
|
|
|
|
it('auto [a, b] = mk(); defines BOTH a and b (not just the first / neither)', () => {
|
|
const cfg = cpp.cfgOf(`void f() { auto [a, b] = mk(); use(a); use(b); }`);
|
|
expect(isDef(cfg, bindingIdx(cfg, 'a'))).toBe(true);
|
|
expect(isDef(cfg, bindingIdx(cfg, 'b'))).toBe(true);
|
|
// a later use(a) must resolve to the SAME binding the declaration defs —
|
|
// i.e. `a` is a real local, not a synthetic module binding.
|
|
const a = bindingIdx(cfg, 'a');
|
|
const usedA = cfg.blocks.some((bl) => bl.statements?.some((s) => s.uses.includes(a)));
|
|
expect(usedA).toBe(true);
|
|
});
|
|
|
|
it('auto& [a, b] = ref(); (reference structured binding) defines both names', () => {
|
|
const cfg = cpp.cfgOf(`void f() { auto& [a, b] = ref(); sink(a, b); }`);
|
|
expect(isDef(cfg, bindingIdx(cfg, 'a'))).toBe(true);
|
|
expect(isDef(cfg, bindingIdx(cfg, 'b'))).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('C++ CfgVisitor — coroutines (#2195 P1)', () => {
|
|
it('co_return edges to EXIT (return), not a seq fallthrough to the next statement', () => {
|
|
const cfg = cpp.cfgOf(`Task f(int x) { if (x) co_return early(); main(); }`);
|
|
const co = block(cfg, 'co_return early();');
|
|
// co_return is a return terminator: it edges to EXIT…
|
|
expect(
|
|
cfg.edges.some((e) => e.from === co && e.to === cfg.exitIndex && e.kind === 'return'),
|
|
).toBe(true);
|
|
// …and never falls through to the following statement.
|
|
expect(cfg.edges.some((e) => e.from === co && e.kind === 'seq')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('C CfgVisitor — functionStartColumn', () => {
|
|
it('two same-line functions get distinct functionStartColumn', () => {
|
|
const cfgs = c.cfgsOf(`int a(){return 1;} int b(){return 2;}`);
|
|
expect(cfgs).toHaveLength(2);
|
|
expect(cfgs[0].functionStartLine).toBe(cfgs[1].functionStartLine); // same line
|
|
expect(cfgs[0].functionStartColumn).not.toBe(cfgs[1].functionStartColumn); // distinct column
|
|
});
|
|
});
|
|
|
|
describe('C++ CfgVisitor — exceptions', () => {
|
|
it('try/catch: a throw edge runs from each protected block to the handler', () => {
|
|
const cfg = cpp.cfgOf(`void f() {
|
|
try { risky(); deeper(); } catch (std::exception& e) { handle(e); }
|
|
after();
|
|
}`);
|
|
expect(edgeKinds(cfg).has('throw')).toBe(true);
|
|
const handler = block(cfg, 'handle(e);');
|
|
// every protected-region block reaches the handler.
|
|
expect(reaches(cfg, block(cfg, 'risky();'), handler)).toBe(true);
|
|
// and after() is still reachable (handler completion rejoins).
|
|
expect(reachable(cfg, block(cfg, 'after();'))).toBe(true);
|
|
});
|
|
|
|
it('multi-catch: a body throw reaches EVERY handler (clauses 2..N not orphaned)', () => {
|
|
const cfg = cpp.cfgOf(`void f() {
|
|
try { risky(); } catch (int e) { a(e); } catch (double d) { b(d); } catch (...) { c(); }
|
|
after();
|
|
}`);
|
|
const risky = block(cfg, 'risky();');
|
|
// The matching catch is dynamic, so the body throw must reach all three handlers.
|
|
expect(reaches(cfg, risky, block(cfg, 'a(e);'))).toBe(true);
|
|
expect(reaches(cfg, risky, block(cfg, 'b(d);'))).toBe(true);
|
|
expect(reaches(cfg, risky, block(cfg, 'c();'))).toBe(true);
|
|
// none of the later handlers is orphaned (all reachable from ENTRY); the
|
|
// post-try continuation still rejoins.
|
|
expect(reachable(cfg, block(cfg, 'b(d);'))).toBe(true);
|
|
expect(reachable(cfg, block(cfg, 'after();'))).toBe(true);
|
|
});
|
|
|
|
it('throw inside a branched try body reaches the handler from the interior block', () => {
|
|
const cfg = cpp.cfgOf(`void f(int x) {
|
|
try { guard(); if (x) { deep(); } } catch (int e) { onErr(); }
|
|
}`);
|
|
const handler = block(cfg, 'onErr();');
|
|
expect(reaches(cfg, block(cfg, 'deep();'), handler)).toBe(true);
|
|
});
|
|
|
|
it('throw with NO enclosing try routes to EXIT and ends its block', () => {
|
|
const cfg = cpp.cfgOf(`void f(int x) { if (x) { throw 1; } done(); }`);
|
|
const thr = block(cfg, 'throw 1;');
|
|
expect(cfg.edges).toContainEqual({ from: thr, to: cfg.exitIndex, kind: 'throw' });
|
|
// throw terminates its block — control does not fall into done() from it.
|
|
expect(reaches(cfg, thr, block(cfg, 'done();'))).toBe(false);
|
|
expect(reachable(cfg, block(cfg, 'done();'))).toBe(true); // via the if false branch
|
|
});
|
|
});
|
|
|
|
describe('C++ CfgVisitor — range-for', () => {
|
|
it('for_range_loop: header + body + loop-back + exit', () => {
|
|
const cfg = cpp.cfgOf(`void f(std::vector<int>& xs) { for (int x : xs) { use(x); } done(); }`);
|
|
const body = block(cfg, 'use(x);');
|
|
expect(edgeKinds(cfg).has('cond-true')).toBe(true);
|
|
expect(edgeKinds(cfg).has('loop-back')).toBe(true);
|
|
// the loop body loops back to the header and the loop has an exit to done().
|
|
const header = cfg.edges.find((e) => e.kind === 'loop-back' && e.from === body)?.to;
|
|
expect(header).toBeDefined();
|
|
expect(reachable(cfg, block(cfg, 'done();'))).toBe(true);
|
|
expect(isExitReachableFromAllBlocks(cfg)).toBe(true);
|
|
});
|
|
|
|
it('range-for declarator defines the loop variable; iterated expr is a use', () => {
|
|
const cfg = cpp.cfgOf(`void f(std::vector<int>& xs) { for (int x : xs) { use(x); } }`);
|
|
const x = bindingIdx(cfg, 'x');
|
|
const defined = cfg.blocks.some((bl) => bl.statements?.some((s) => s.defs.includes(x)));
|
|
expect(defined).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('C++ CfgVisitor — lambdas are CFG-bearing functions', () => {
|
|
it('a lambda body yields its own well-formed CFG', () => {
|
|
const cfgs = cpp.cfgsOf(`void f() { auto g = [](int x) { if (x) { a(); } return x; }; }`);
|
|
// f and the lambda are both CFG-bearing.
|
|
expect(cfgs.length).toBeGreaterThanOrEqual(2);
|
|
for (const cfg of cfgs) {
|
|
expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true);
|
|
}
|
|
});
|
|
});
|
|
|
|
// U6 — call-site `sites[]` taint substrate. INERT BY DESIGN: no C-family taint
|
|
// model is registered, so these sites produce zero TAINTED edges; they only
|
|
// give the deferred per-language source/sink model something to match against.
|
|
// The assertions verify the substrate is harvested in the TS `SiteRecord` shape.
|
|
describe('C CfgVisitor — call-site sites[] substrate', () => {
|
|
it('a bare call records a `call` site with callee name + arg occurrence', () => {
|
|
const cfg = c.cfgOf(`void f(int cmd) { exec(cmd); }`);
|
|
const sites = allSites(cfg);
|
|
const exec = sites.find((s) => s.kind === 'call' && s.callee === 'exec');
|
|
expect(exec).toBeDefined();
|
|
// `cmd` (binding 0) occurs at argument position 0.
|
|
expect(exec?.args?.[0]).toContainEqual(bindingIdx(cfg, 'cmd'));
|
|
});
|
|
|
|
it('a method call (`db.query(x)`) records the receiver binding + dotted callee', () => {
|
|
const cfg = c.cfgOf(`void f(int x) { db.query(x); }`);
|
|
const site = allSites(cfg).find((s) => s.kind === 'call' && s.callee === 'db.query');
|
|
expect(site).toBeDefined();
|
|
expect(site?.receiver).toBe(bindingIdx(cfg, 'db'));
|
|
expect(site?.args?.[0]).toContainEqual(bindingIdx(cfg, 'x'));
|
|
});
|
|
|
|
it('a nested call (`exec(escape(x))`) via-tags the inner site (sanitizer substrate)', () => {
|
|
const cfg = c.cfgOf(`void f(int x) { exec(escape(x)); }`);
|
|
const sites = allSites(cfg);
|
|
const exec = sites.findIndex((s) => s.callee === 'exec');
|
|
const escape = sites.findIndex((s) => s.callee === 'escape');
|
|
expect(exec).toBeGreaterThanOrEqual(0);
|
|
expect(escape).toBeGreaterThanOrEqual(0);
|
|
const x = bindingIdx(cfg, 'x');
|
|
// escape's arg 0 carries a plain `x`; exec's arg 0 carries `[x, escapeSiteIdx]`.
|
|
expect(sites[escape].args?.[0]).toContainEqual(x);
|
|
expect(sites[exec].args?.[0]).toContainEqual([x, escape]);
|
|
});
|
|
|
|
it('a call assigned to a variable records resultDefs', () => {
|
|
const cfg = c.cfgOf(`void f(int a) { int y = load(a); }`);
|
|
const site = allSites(cfg).find((s) => s.callee === 'load');
|
|
expect(site?.resultDefs).toContain(bindingIdx(cfg, 'y'));
|
|
});
|
|
|
|
it('a CFG-only function (no calls) emits NO sites key (omit-when-empty)', () => {
|
|
const cfg = c.cfgOf(`void f(int a, int b) { int x = a + b; }`);
|
|
expect(hasAnySites(cfg)).toBe(false);
|
|
expect(allSites(cfg)).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('C++ CfgVisitor — call-site sites[] substrate', () => {
|
|
it('a `new Foo(x)` records a `new` site with the constructor as callee', () => {
|
|
const cfg = cpp.cfgOf(`void f(int x) { auto p = new Foo(x); }`);
|
|
const site = allSites(cfg).find((s) => s.kind === 'new');
|
|
expect(site).toBeDefined();
|
|
expect(site?.callee).toBe('Foo');
|
|
expect(site?.args?.[0]).toContainEqual(bindingIdx(cfg, 'x'));
|
|
// `auto p = new Foo(x)` attaches resultDefs of `p` to the new site.
|
|
expect(site?.resultDefs).toContain(bindingIdx(cfg, 'p'));
|
|
});
|
|
|
|
it('a `ns::g(z)` namespace call folds `::` into a dotted callee path', () => {
|
|
const cfg = cpp.cfgOf(`void f(int z) { ns::g(z); }`);
|
|
const site = allSites(cfg).find((s) => s.kind === 'call' && s.callee === 'ns.g');
|
|
expect(site).toBeDefined();
|
|
expect(site?.args?.[0]).toContainEqual(bindingIdx(cfg, 'z'));
|
|
});
|
|
});
|