fold/tests/unit/test_uninit_pre_pass.cpp
trvon 84a3126c35
feat(symbolic): wrap clang::UninitVariablesAnalysis
Adds fold::symbolic::runUninitAnalysisOnTU(ctx) returning aggregate
counts of functions with local read-before-write uses, derived from
clang's UninitVariablesAnalysis. Scope reminder pinned by the 4
unit tests: this flags LOCAL variable uninit reads, NOT parameter
nullability. The original matrix C5 framing ("null-deref FP triage
pre-pass") was wrong-hypothesis — the analyzer treats parameters as
always-initialised, so it cannot drive null-deref triage.

CFG build options need AddInitializers + setAllAlwaysAdd before the
per-fn AnalysisDeclContext sees decl initializers; without that the
analyzer silently reports zero uses.

Scan-pipeline integration deferred — needs a per-TU accumulator
threaded through the priming consumer.

Refs docs/library-expansion-matrix.md row C5.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 23:02:39 -04:00

83 lines
2.8 KiB
C++

// Measurement-only first cut for matrix row C5
// (`clang::UninitializedValues` pre-pass). The original framing —
// "triage null-deref FPs via uninit pre-pass" — was a wrong-hypothesis:
// clang's analyzer flags LOCAL read-before-write, not parameter
// nullability. This test pins what the analyzer *does* report so the
// fold-side wrapper stays honest about its scope. Future sessions can
// decide whether to graduate the signal into a standalone detector.
#include <clang/Frontend/ASTUnit.h>
#include <clang/Tooling/Tooling.h>
#include <catch2/catch_test_macros.hpp>
#include <memory>
#include <string>
#include "fold/symbolic/uninit_pre_pass.hpp"
namespace {
std::unique_ptr<clang::ASTUnit> parse(const std::string& code) {
return clang::tooling::buildASTFromCodeWithArgs(code, {"-std=c++20"}, "input.cpp");
}
} // namespace
TEST_CASE("uninit pre-pass: function with no uninit reads scores 0", "[uninit-pre-pass][clean]") {
auto unit = parse(R"(
int safe(int x) {
int y = x + 1;
return y;
}
)");
REQUIRE(unit);
auto stats = fold::symbolic::runUninitAnalysisOnTU(unit->getASTContext());
REQUIRE(stats.total_functions >= 1);
REQUIRE(stats.functions_with_uninit_reads == 0);
REQUIRE(stats.total_uninit_reads == 0);
}
TEST_CASE("uninit pre-pass: read-before-write local is flagged",
"[uninit-pre-pass][always-uninit]") {
auto unit = parse(R"(
int oops() {
int y;
return y + 1;
}
)");
REQUIRE(unit);
auto stats = fold::symbolic::runUninitAnalysisOnTU(unit->getASTContext());
REQUIRE(stats.total_functions >= 1);
REQUIRE(stats.functions_with_uninit_reads >= 1);
REQUIRE(stats.total_uninit_reads >= 1);
}
TEST_CASE("uninit pre-pass: branch-conditional uninit is flagged",
"[uninit-pre-pass][sometimes-uninit]") {
auto unit = parse(R"(
int maybe(int cond) {
int y;
if (cond) y = 7;
return y;
}
)");
REQUIRE(unit);
auto stats = fold::symbolic::runUninitAnalysisOnTU(unit->getASTContext());
REQUIRE(stats.functions_with_uninit_reads >= 1);
}
TEST_CASE("uninit pre-pass: parameters are never flagged uninit (scope check)",
"[uninit-pre-pass][scope]") {
// The original C5 hypothesis was "null-deref FP triage via uninit
// pre-pass." This test pins the reason that hypothesis was wrong:
// clang's analyzer treats function parameters as always-initialized.
// A param can still be nullable, but the uninit analysis won't say so.
auto unit = parse(R"(
int param_only(int* p) {
return *p;
}
)");
REQUIRE(unit);
auto stats = fold::symbolic::runUninitAnalysisOnTU(unit->getASTContext());
REQUIRE(stats.functions_with_uninit_reads == 0);
}