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.
165 lines
5.3 KiB
C++
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());
|
|
}
|