fold/tests/unit/test_quality_precision.cpp
trvon 2a730e3a0e
Some checks failed
build / build-linux (push) Failing after 7m36s
build / build-macos (push) Has been cancelled
build / release (push) Has been cancelled
perf(uninit): skip functions without local candidates
2026-09-02 06:59:54 -06:00

658 lines
22 KiB
C++

#include <clang/AST/RecursiveASTVisitor.h>
#include <clang/Tooling/Tooling.h>
#include <catch2/catch_test_macros.hpp>
#include <memory>
#include <string>
#include <vector>
#include "fold/detect/redundant_store.hpp"
#include "fold/detect/uninit_read.hpp"
#include "fold/detect/unused_function.hpp"
#include "fold/detect/use_after_move.hpp"
#include "fold/frontend/statement_count.hpp"
#include "fold/report/finding.hpp"
#include "fold/symbolic/budget.hpp"
#include "fold/symbolic/theory.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> useAfterMove(const std::string& code,
fold::SolverStats* stats = nullptr) {
auto unit = parse(code);
REQUIRE(unit);
fold::SolverBudget budget;
fold::SolverStats local_stats;
auto findings = fold::detectUseAfterMoveInContext(unit->getASTContext(), budget, local_stats);
if (stats)
*stats = local_stats;
return findings;
}
std::vector<fold::Finding> uninitReads(const std::string& code) {
auto unit = parse(code);
REQUIRE(unit);
fold::SolverBudget budget;
fold::SolverStats stats;
return fold::detectUninitReadsInContext(unit->getASTContext(), budget, stats);
}
class FunctionFinder final : public clang::RecursiveASTVisitor<FunctionFinder> {
public:
explicit FunctionFinder(std::string name) : name_(std::move(name)) {}
bool VisitFunctionDecl(clang::FunctionDecl* function) {
if (function && function->hasBody() && function->getNameAsString() == name_)
function_ = function;
return true;
}
clang::FunctionDecl* function() const { return function_; }
private:
std::string name_;
clang::FunctionDecl* function_ = nullptr;
};
clang::FunctionDecl* findFunction(clang::ASTContext& context, const std::string& name) {
FunctionFinder finder(name);
finder.TraverseDecl(context.getTranslationUnitDecl());
return finder.function();
}
std::vector<fold::Finding> redundantStores(const std::string& code,
fold::SolverStats* stats = nullptr) {
auto unit = parse(code);
REQUIRE(unit);
fold::SolverBudget budget;
fold::SolverStats local_stats;
auto findings =
fold::detectRedundantStoresInContext(unit->getASTContext(), budget, local_stats);
if (stats)
*stats = local_stats;
return findings;
}
std::vector<fold::Finding> unusedFunctions(const std::string& code) {
auto unit = parse(code);
REQUIRE(unit);
return fold::detectUnusedFunctionsInContext(unit->getASTContext());
}
constexpr const char* move_support = R"(
namespace std {
template <class T> T&& move(T& value) { return static_cast<T&&>(value); }
}
struct Token {
Token();
Token(Token&&);
Token& operator=(Token&&);
int value() const;
};
)";
} // namespace
TEST_CASE("clone sizing counts executable statements instead of expression AST nodes",
"[quality-precision][clone]") {
auto unit = parse(R"(
int accessor(int value) { return value + value * 2; }
int workflow(int value) {
int first = value + 1;
int second = first * 2;
if (second > 10)
return second;
return first;
}
)");
REQUIRE(unit);
int accessor_count = -1;
int workflow_count = -1;
for (const clang::Decl* decl : unit->getASTContext().getTranslationUnitDecl()->decls()) {
const auto* function = clang::dyn_cast<clang::FunctionDecl>(decl);
if (!function || !function->hasBody())
continue;
if (function->getName() == "accessor")
accessor_count = fold::countExecutableStatements(function->getBody());
if (function->getName() == "workflow")
workflow_count = fold::countExecutableStatements(function->getBody());
}
CHECK(accessor_count == 1);
CHECK(workflow_count == 5);
}
TEST_CASE("use-after-move: the move expression is not its own use",
"[quality-precision][use-after-move]") {
auto findings = useAfterMove(std::string(move_support) + R"(
int clean() {
Token source;
Token destination(std::move(source));
return destination.value();
}
)");
REQUIRE(findings.empty());
}
TEST_CASE("use-after-move: std::move-free functions are prechecked safe",
"[quality-precision][use-after-move][prefilter]") {
// A function with no std::move call can never populate the moved-from
// set, so the walker is skipped entirely and the precheck counter rises.
fold::SolverStats stats;
auto findings = useAfterMove(std::string(move_support) + R"(
int no_move(int x) {
int y = x + 1;
if (y > 0)
return y;
return -y;
}
)",
&stats);
REQUIRE(findings.empty());
CHECK(stats.functions_prechecked_safe >= 1);
}
TEST_CASE("use-after-move: a later read is SAT-confirmed", "[quality-precision][use-after-move]") {
fold::SolverStats stats;
auto findings = useAfterMove(std::string(move_support) + R"(
int broken() {
Token source;
Token destination(std::move(source));
return source.value() + destination.value();
}
)",
&stats);
REQUIRE(findings.size() == 1);
CHECK(findings.front().kind == fold::FindingKind::UseAfterMove);
CHECK(findings.front().confidence == fold::Confidence::Proved);
CHECK(stats.queries == 1);
CHECK(stats.sat == 1);
}
TEST_CASE("use-after-move: overloaded assignment reinitializes the moved object",
"[quality-precision][use-after-move][lifetime]") {
auto findings = useAfterMove(std::string(move_support) + R"(
Token refresh(Token&&);
int clean_assignment() {
Token source;
source = refresh(std::move(source));
return source.value();
}
)");
REQUIRE(findings.empty());
}
TEST_CASE("use-after-move: unsupported try control flow suppresses prefix candidates",
"[quality-precision][use-after-move]") {
fold::SolverStats stats;
auto findings = useAfterMove(std::string(move_support) + R"(
int uncertain() {
Token source;
Token destination(std::move(source));
try {
return source.value() + destination.value();
} catch (...) {
return 0;
}
}
)",
&stats);
REQUIRE(findings.empty());
CHECK(stats.bailed_functions == 1);
}
TEST_CASE("redundant-store: condition reads keep scalar initializers live",
"[quality-precision][redundant-store]") {
auto findings = redundantStores(R"(
int live(int input) {
int value = input;
if (value > 0)
return 1;
return 0;
}
)");
REQUIRE(findings.empty());
}
TEST_CASE("redundant-store: store-free function is prechecked safe",
"[quality-precision][redundant-store][prefilter]") {
// A function with no assignment to a scalar local can never create a
// pending store, so neither checkWrites nor drainPending can emit; the
// walker is skipped and the precheck counter rises.
fold::SolverStats stats;
auto findings = redundantStores(R"(
struct Box { int get() const; };
int read_only(const Box& box) {
return box.get() + 1;
}
)",
&stats);
REQUIRE(findings.empty());
CHECK(stats.functions_prechecked_safe >= 1);
}
TEST_CASE("redundant-store: a straight-line overwritten scalar is retained",
"[quality-precision][redundant-store]") {
auto findings = redundantStores(R"(
int dead() {
int value = 1;
value = 2;
return value;
}
)");
REQUIRE(findings.size() == 1);
CHECK(findings.front().kind == fold::FindingKind::RedundantStore);
CHECK(findings.front().confidence == fold::Confidence::Heuristic);
REQUIRE(findings.front().evidence.quality.has_value());
CHECK(findings.front().evidence.quality->classification == "redundant-store");
CHECK(findings.front().evidence.quality->incomplete);
CHECK(findings.front().evidence.quality->uncertainty_reasons ==
std::vector<std::string>{"bounded-ast-dataflow"});
}
TEST_CASE("redundant-store identity survives unrelated preceding statements",
"[quality-precision][redundant-store][identity]") {
const auto baseline = redundantStores(R"(
int dead() {
int value = 1;
value = 2;
return value;
}
)");
const auto transformed = redundantStores(R"(
int dead() {
int unrelated = 0;
int value = 1;
value = 2;
return value + unrelated;
}
)");
REQUIRE(baseline.size() == 1);
REQUIRE(transformed.size() == 1);
REQUIRE(baseline.front().id == transformed.front().id);
}
TEST_CASE("redundant-store: class lifetime and RAII are not modeled as dead stores",
"[quality-precision][redundant-store]") {
auto findings = redundantStores(R"(
struct Guard { Guard(); ~Guard(); };
void guarded() { Guard lock; }
)");
REQUIRE(findings.empty());
}
TEST_CASE("redundant-store: unsupported range-for suppresses prefix observations",
"[quality-precision][redundant-store]") {
fold::SolverStats stats;
auto findings = redundantStores(R"(
int sum(const int (&values)[3]) {
int total = 0;
for (int value : values)
total += value;
return total;
}
)",
&stats);
REQUIRE(findings.empty());
CHECK(stats.bailed_functions == 1);
}
TEST_CASE("uninit-read precheck requires a potentially uninitialized local",
"[quality-precision][uninit-read]") {
auto unit = parse(R"cpp(
int initialized_only(int input) { int local = input; return local; }
int uninitialized_read() { int local; return local; }
int nested_uninitialized() { int outer = 0; { int local; return local; } }
)cpp");
REQUIRE(unit);
auto& context = unit->getASTContext();
CHECK_FALSE(fold::uninit_read_detail::functionMayReadUninitializedLocal(
findFunction(context, "initialized_only")));
CHECK(fold::uninit_read_detail::functionMayReadUninitializedLocal(
findFunction(context, "uninitialized_read")));
CHECK(fold::uninit_read_detail::functionMayReadUninitializedLocal(
findFunction(context, "nested_uninitialized")));
}
TEST_CASE("uninit-read: modeled C APIs initialize opaque output objects",
"[quality-precision][uninit-read][api-init]") {
auto findings = uninitReads(R"(
using Actions = void*;
extern "C" int posix_spawn_file_actions_init(Actions*);
extern "C" int posix_spawn_file_actions_addclose(Actions*, int);
int configure() {
Actions actions;
if (posix_spawn_file_actions_init(&actions) != 0)
return -1;
return posix_spawn_file_actions_addclose(&actions, 3);
}
)");
REQUIRE(findings.empty());
}
TEST_CASE("uninit-read: unknown pointer APIs do not imply initialization",
"[quality-precision][uninit-read][api-init]") {
auto findings = uninitReads(R"(
extern "C" void observe(int*);
int inspect() {
int value;
observe(&value);
return value;
}
)");
REQUIRE_FALSE(findings.empty());
}
TEST_CASE("unused-function: implicit destructor invocation counts as lifetime use",
"[quality-precision][unused-function][lifetime]") {
auto findings = unusedFunctions(R"(
namespace {
struct Guard {
Guard() {}
~Guard() {}
};
}
int use() {
Guard guard;
return 0;
}
)");
REQUIRE(findings.empty());
}
TEST_CASE("unused-function: CRTP traversal callbacks are template-invoked",
"[quality-precision][unused-function][crtp]") {
auto findings = unusedFunctions(R"(
namespace clang {
template <class Derived> struct RecursiveASTVisitor {
void TraverseDecl(int*) { static_cast<Derived*>(this)->VisitFunctionDecl(nullptr); }
};
}
namespace {
struct Visitor : clang::RecursiveASTVisitor<Visitor> {
bool VisitFunctionDecl(int*) { return true; }
bool TraverseWhileStmt(int*) { return true; }
int unusedHelper() { return 7; }
};
}
int use() { Visitor visitor; visitor.TraverseDecl(nullptr); return 0; }
)");
REQUIRE(findings.size() == 1);
CHECK(findings.front().locations.front().function.find("unusedHelper") != std::string::npos);
}
TEST_CASE("unused-function: standard-library callback operators are template-invoked",
"[quality-precision][unused-function][callback]") {
auto findings = unusedFunctions(R"(
namespace {
struct Finalizer { void operator()(int*) const {} };
struct Key { bool operator==(const Key&) const = default; };
struct Hash { unsigned long operator()(const Key&) const { return 0; } };
}
template <class Deleter> void consume(Deleter deleter) { deleter(nullptr); }
int use() { consume(Finalizer{}); return 0; }
)");
REQUIRE(findings.empty());
}
TEST_CASE("unused-function: dependent template lookup counts helper candidates as called",
"[quality-precision][unused-function]") {
auto findings = unusedFunctions(R"(
static int joinAnd(int value) { return value + 1; }
template <class Container>
int appendListClause(const Container& values) {
return joinAnd(values.front());
}
int use() {
struct Values { int front() const { return 1; } } values;
return appendListClause(values);
}
)");
REQUIRE(findings.empty());
}
TEST_CASE("unused-function: constructor expressions count as calls",
"[quality-precision][unused-function]") {
auto findings = unusedFunctions(R"(
namespace {
class DisjointSet {
public:
explicit DisjointSet(int size) : size_(size) {}
int size() const { return size_; }
private:
int size_;
};
}
int use() {
DisjointSet semanticComponents(3);
DisjointSet reciprocalComponents(4);
return semanticComponents.size() + reciprocalComponents.size();
}
)");
REQUIRE(findings.empty());
}
TEST_CASE("unused-function identity survives harmless preceding insertion",
"[quality-precision][unused-function][identity]") {
const auto baseline = unusedFunctions(R"(
static int unusedHelper(int value) { return value + 1; }
int use(int value) { return value; }
)");
const auto transformed = unusedFunctions(R"(
int unrelated() { return 0; }
static int unusedHelper(int value) { return value + 1; }
int use(int value) { return value; }
)");
REQUIRE(baseline.size() == 1);
REQUIRE(transformed.size() == 1);
REQUIRE(baseline.front().id == transformed.front().id);
}
TEST_CASE("unused-function: genuinely unused internal function remains reported",
"[quality-precision][unused-function]") {
auto findings = unusedFunctions(R"(
static int unusedHelper(int value) { return value + 1; }
int use(int value) { return value; }
)");
REQUIRE(findings.size() == 1);
CHECK(findings.front().kind == fold::FindingKind::UnusedFunction);
}
TEST_CASE("uninit-read: known output-param API initializes the local",
"[quality-precision][uninit-read][api-init]") {
// CFNumberGetValue(..., &size) writes the third argument; the modeled
// output-param contract must treat the local as initialized after the call.
auto findings = uninitReads(R"(
extern "C" int CFNumberGetValue(const void*, unsigned long, void*);
int f() {
int x;
if (CFNumberGetValue(0, 0, &x)) {
return x;
}
return 0;
}
)");
REQUIRE(findings.empty());
}
TEST_CASE("uninit-read: CFNumberGetValue drive-info success shape initializes output",
"[quality-precision][uninit-read][api-init][chromium-shape]") {
auto findings = uninitReads(R"(
extern "C" int CFNumberGetValue(const void*, unsigned long, void*);
using Boolean = int;
long long read_size() {
long long size;
Boolean success = CFNumberGetValue(nullptr, 0, &size);
if (success) {
return size;
}
return 0;
}
)");
REQUIRE(findings.empty());
}
TEST_CASE("uninit-read: CFNumberGetValue failure path does not read output",
"[quality-precision][uninit-read][api-init][chromium-shape]") {
auto findings = uninitReads(R"(
extern "C" int CFNumberGetValue(const void*, unsigned long, void*);
long long read_size() {
long long size;
if (!CFNumberGetValue(nullptr, 0, &size)) {
return 0;
}
return size;
}
)");
REQUIRE(findings.empty());
}
TEST_CASE("uninit-read: CFNumberGetValue comparison success guard initializes output",
"[quality-precision][uninit-read][api-init][chromium-shape]") {
auto findings = uninitReads(R"(
extern "C" int CFNumberGetValue(const void*, unsigned long, void*);
long long read_size() {
long long size;
int success = CFNumberGetValue(nullptr, 0, &size);
if (success != 0) {
return size;
}
return 0;
}
)");
REQUIRE(findings.empty());
}
TEST_CASE("uninit-read: CFNumberGetValue comparison failure path avoids output read",
"[quality-precision][uninit-read][api-init][chromium-shape]") {
auto findings = uninitReads(R"(
extern "C" int CFNumberGetValue(const void*, unsigned long, void*);
long long read_size() {
long long size;
int success = CFNumberGetValue(nullptr, 0, &size);
if (success == 0) {
return 0;
}
return size;
}
)");
REQUIRE(findings.empty());
}
TEST_CASE("uninit-read: CFNumberGetValue success short-circuit guard initializes output",
"[quality-precision][uninit-read][api-init][chromium-shape]") {
auto findings = uninitReads(R"(
extern "C" int CFNumberGetValue(const void*, unsigned long, void*);
extern "C" int ready();
long long read_size() {
long long size;
int success = CFNumberGetValue(nullptr, 0, &size);
if (success && ready()) {
return size;
}
return 0;
}
)");
REQUIRE(findings.empty());
}
TEST_CASE("uninit-read: CFNumberGetValue success OR guard does not prove output",
"[quality-precision][uninit-read][api-init][chromium-shape]") {
auto findings = uninitReads(R"(
extern "C" int CFNumberGetValue(const void*, unsigned long, void*);
extern "C" int ready();
long long read_size() {
long long size;
int success = CFNumberGetValue(nullptr, 0, &size);
if (success || ready()) {
return size;
}
return 0;
}
)");
REQUIRE_FALSE(findings.empty());
}
TEST_CASE("uninit-read: CFNumberGetValue failure short-circuit path remains uncertain",
"[quality-precision][uninit-read][api-init][chromium-shape]") {
auto findings = uninitReads(R"(
extern "C" int CFNumberGetValue(const void*, unsigned long, void*);
extern "C" int ready();
long long read_size() {
long long size;
int success = CFNumberGetValue(nullptr, 0, &size);
if (!success && ready()) {
return 0;
}
return size;
}
)");
REQUIRE_FALSE(findings.empty());
}
TEST_CASE("uninit-read: negated OR false branch proves CFNumberGetValue success",
"[quality-precision][uninit-read][api-init][chromium-shape]") {
auto findings = uninitReads(R"(
extern "C" int CFNumberGetValue(const void*, unsigned long, void*);
extern "C" int ready();
long long read_size() {
long long size;
int success = CFNumberGetValue(nullptr, 0, &size);
if (!success || ready()) {
return 0;
}
return size;
}
)");
REQUIRE(findings.empty());
}
TEST_CASE("uninit-read: negated AND true branch does not prove CFNumber success",
"[quality-precision][uninit-read][api-init][chromium-shape]") {
auto findings = uninitReads(R"(
extern "C" int CFNumberGetValue(const void*, unsigned long, void*);
extern "C" int ready();
long long read_size() {
long long size;
int success = CFNumberGetValue(nullptr, 0, &size);
if (!(success && ready())) {
return size;
}
return 0;
}
)");
REQUIRE_FALSE(findings.empty());
}
TEST_CASE("uninit-read: ignored CFNumberGetValue result leaves output uncertain",
"[quality-precision][uninit-read][api-init][chromium-shape]") {
auto findings = uninitReads(R"(
extern "C" int CFNumberGetValue(const void*, unsigned long, void*);
long long read_size() {
long long size;
CFNumberGetValue(nullptr, 0, &size);
return size;
}
)");
REQUIRE_FALSE(findings.empty());
}
TEST_CASE("uninit-read: unmodeled pointer APIs still flag uninit locals",
"[quality-precision][uninit-read][api-init]") {
// Design contract: unknown pointer APIs do NOT imply initialization.
auto findings = uninitReads(R"(
extern "C" void observe(int*);
int g() {
int y;
observe(&y);
return y;
}
)");
REQUIRE_FALSE(findings.empty());
}