fold/tests/unit/test_dead_statement.cpp
trvon fd4a68dfff
test: add coverage for project graph, pcode encoder, and detector behaviors
Unit + integration coverage for the session's work: incremental project
graph/index, pcode encoder, unreachable/dead-branch, equiv/cross-TU,
div-by-zero/sign-conversion/int-truncation, use-after-free, quality-precision,
scan-router profile, source TU runner, detector plan, and the eval fixtures.
2026-08-16 16:46:07 -06:00

165 lines
5.3 KiB
C++

#include <clang/Frontend/ASTUnit.h>
#include <clang/Tooling/Tooling.h>
#include <catch2/catch_test_macros.hpp>
#include <memory>
#include <string>
#include <vector>
#include "fold/detect/dead_statement.hpp"
#include "fold/report/finding.hpp"
namespace {
std::unique_ptr<clang::ASTUnit> parse(const std::string& code) {
return clang::tooling::buildASTFromCodeWithArgs(
code, {"-std=c++20", "-fcxx-exceptions", "-fexceptions"}, "input.cpp");
}
std::vector<fold::Finding> 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<std::string>{"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 <unordered_map>
#include <string>
void f(const std::unordered_map<std::string, int>& src,
std::unordered_map<std::string, int>& 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());
}