#include #include #include #include #include #include #include "fold/detect/dead_statement.hpp" #include "fold/report/finding.hpp" namespace { std::unique_ptr parse(const std::string& code) { return clang::tooling::buildASTFromCodeWithArgs( code, {"-std=c++20", "-fcxx-exceptions", "-fexceptions"}, "input.cpp"); } std::vector run(const std::string& code) { auto unit = parse(code); REQUIRE(unit); return fold::detectDeadStatementsInContext(unit->getASTContext()); } } // namespace TEST_CASE("dead-statement: code after return is flagged", "[dead-statement]") { auto findings = run(R"( int f(int x) { return x; int y = x + 1; return y; } )"); REQUIRE_FALSE(findings.empty()); REQUIRE(findings[0].kind == fold::FindingKind::DeadStatement); REQUIRE(findings[0].evidence.quality.has_value()); REQUIRE(findings[0].evidence.quality->classification == "cfg-unreachable-statement"); REQUIRE(findings[0].evidence.quality->rank_reasons == std::vector{"entry-unreachable", "direct-control-transfer"}); } TEST_CASE("dead-statement: no direct-transfer sibling skips CFG build", "[dead-statement][prefilter]") { // A function whose compounds never have a direct control transfer // followed by a later sibling cannot satisfy followsDirectControlTransfer, // so the CFG build is skipped and no finding is produced. auto findings = run(R"( int f(int x) { int y = x + 1; if (y > 0) return y; return -y; } )"); REQUIRE(findings.empty()); } TEST_CASE("dead-statement: catch handler body is reachable (EH edges)", "[dead-statement][catch]") { // Without `AddEHEdges = true` in the CFG builder, the catch block has no // predecessor and every line inside it gets flagged as unreachable. This // test pins the fix. auto findings = run(R"( int g(); int f() { try { return g(); } catch (...) { int x = 7; int y = x + 1; return y; } } )"); REQUIRE(findings.empty()); } TEST_CASE("dead-statement: code after throw inside catch is still flagged", "[dead-statement][catch]") { // EH edges keep the catch block reachable, but lines after `throw` inside // the catch are still unreachable and should still fire. auto findings = run(R"( int g(); int f() { try { return g(); } catch (...) { throw; int x = 7; return x; } } )"); REQUIRE_FALSE(findings.empty()); } TEST_CASE("dead-statement: clean function emits nothing", "[dead-statement][negative]") { auto findings = run("int f(int x) { return x + 1; }"); REQUIRE(findings.empty()); } TEST_CASE("dead-statement: range-for + structured binding body is reachable", "[dead-statement][negative][range-for]") { // Regression test for the precision bug surfaced by the fold-on-fold // perf-audit pass (2026-05-23): clang's CFG for a range-for whose // loop variable is a structured binding (`for (const auto& [k, v] // : map)`) leaves the body block without an incoming CFG edge from // the loop condition. The reachability walk in // DeadStatementVisitor then flagged every body statement. // The fix filters out anchors whose ancestor chain contains a // CXXForRangeStmt. auto findings = run(R"( #include #include void f(const std::unordered_map& src, std::unordered_map& dst) { for (const auto& [k, v] : src) dst[k] = v; } )"); REQUIRE(findings.empty()); } TEST_CASE("dead-statement: constexpr-pruned feature fallback belongs to dead-branch analysis", "[dead-statement][negative][constexpr]") { auto findings = run(R"( constexpr bool enabled = true; const char* featureName() { if (enabled) return "enabled"; return "fallback"; } )"); REQUIRE(findings.empty()); } TEST_CASE("dead-statement: exhaustive enum fallback is defensive, not post-transfer code", "[dead-statement][negative][switch]") { auto findings = run(R"( enum class State { ready, stopped }; const char* stateName(State state) { switch (state) { case State::ready: return "ready"; case State::stopped: return "stopped"; } return "unknown"; } )"); REQUIRE(findings.empty()); } TEST_CASE("dead-statement: exhaustive enum default remains a defensive fallback", "[dead-statement][negative][switch]") { auto findings = run(R"( enum class State { ready, stopped }; const char* stateName(State state) { switch (state) { case State::ready: return "ready"; case State::stopped: return "stopped"; default: return "unknown"; } } )"); REQUIRE(findings.empty()); }