fold/tests/unit/test_uninit_local_read.cpp
trvon b10bbd3da6
fix(detect): honor statement-expression write ordering in uninit-read
ARM NEON reductions (vaddvq_*/vminvq_*/vmaxvq_*) expand to GNU statement
expressions `({ ...; __ret = builtin(x); __ret; })`. The uninit-read walker
flat-traversed the statement expression, flagging the result read `__ret`
before noticing the assignment that writes it inside the same expression.
Track variables assigned within a StmtExpr (TraverseStmtExpr) so the result
read is not a false uninit-read.

Verified: simeon neon.cpp range_neon 2->0, statement-expression repro 1->0,
genuine uninit local still fires; full suite 137/137.
2026-08-23 15:47:21 -06:00

141 lines
4.8 KiB
C++

// Matrix row C5-wiring: graduate the UninitializedValues pre-pass
// from a measurement scaffold into a real detector that emits
// `uninit-local-read` findings. C5's library function was already
// shipped (see test_uninit_pre_pass.cpp); this test pins the
// scan-pipeline-facing entry point.
#include <clang/Frontend/ASTUnit.h>
#include <clang/Tooling/Tooling.h>
#include <catch2/catch_test_macros.hpp>
#include <memory>
#include <string>
#include "fold/detect/uninit_local_read.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"}, "input.cpp");
}
} // namespace
TEST_CASE("uninit-local-read: int x; int y = x; emits a finding", "[uninit-local-read][positive]") {
auto unit = parse(R"(
int leak() {
int x;
int y = x;
return y;
}
)");
REQUIRE(unit);
auto findings = fold::detectUninitLocalReadInContext(unit->getASTContext());
REQUIRE(findings.size() >= 1);
REQUIRE(findings[0].kind == fold::FindingKind::UninitLocalRead);
}
TEST_CASE("uninit-local-read: int y = x where x is initialised does not fire",
"[uninit-local-read][negative]") {
auto unit = parse(R"(
int safe(int x) {
int y = x;
return y;
}
)");
REQUIRE(unit);
auto findings = fold::detectUninitLocalReadInContext(unit->getASTContext());
REQUIRE(findings.empty());
}
TEST_CASE("uninit-local-read: parameter reads are NOT flagged",
"[uninit-local-read][param-scope]") {
// C5's calibrated scope: clang's analyzer flags only LOCAL
// read-before-write. Parameters are always treated as initialised
// (they're not — but encoding nullability requires a different
// analyzer). Pin the scope.
auto unit = parse(R"(
int reads_param(int p) {
return p;
}
)");
REQUIRE(unit);
auto findings = fold::detectUninitLocalReadInContext(unit->getASTContext());
REQUIRE(findings.empty());
}
TEST_CASE("uninit-local-read: finding location points at the read, not the decl",
"[uninit-local-read][location]") {
auto unit = parse(R"(
int leak() {
int x;
return x;
}
)");
REQUIRE(unit);
auto findings = fold::detectUninitLocalReadInContext(unit->getASTContext());
REQUIRE(findings.size() >= 1);
REQUIRE_FALSE(findings[0].locations.empty());
// The `return x;` line is 4 in the parsed snippet (line 1 is the
// opening blank-after-R"(); int leak() is line 2; int x; is line
// 3; return x; is line 4). The decl-only location would be line 3.
REQUIRE(findings[0].locations[0].line == 4);
}
TEST_CASE("uninit-local-read: self-init `int x = x` is flagged", "[uninit-local-read][self-init]") {
auto unit = parse(R"(
int self_init() {
int x = x;
return x;
}
)");
REQUIRE(unit);
auto findings = fold::detectUninitLocalReadInContext(unit->getASTContext());
REQUIRE(findings.size() >= 1);
}
TEST_CASE("uninit-local-read: strtol endptr write counts as initialisation",
"[uninit-local-read][strtol]") {
auto unit = parse(R"(
extern "C" long strtol(const char*, char**, int);
int parse_tail() {
char input[] = "7";
char* end;
(void)strtol(input, &end, 10);
return *end == '\0';
}
)");
REQUIRE(unit);
auto findings = fold::detectUninitLocalReadInContext(unit->getASTContext());
REQUIRE(findings.empty());
}
TEST_CASE("uninit-local-read: statement expression result read is not flagged",
"[uninit-local-read][stmt-expr]") {
// ARM NEON intrinsics (vaddvq_*/vminvq_*/vmaxvq_*) expand to GNU statement
// expressions whose result reads an internal `__ret` written by a preceding
// assignment inside the same expression. A flat traversal would flag the
// result read; the walker must honor the intra-expression write ordering.
auto unit = parse(R"(
extern "C" float g(void*);
float f(void* acc) {
float sum = ({ void* __a = acc; float __ret; __ret = g(__a); __ret; });
return sum;
}
)");
REQUIRE(unit);
auto findings = fold::detectUninitLocalReadInContext(unit->getASTContext());
REQUIRE(findings.empty());
}
TEST_CASE("uninit-local-read: genuinely uninitialized local still fires",
"[uninit-local-read][stmt-expr][negative]") {
auto unit = parse(R"(
int f() { int x; return x; }
)");
REQUIRE(unit);
auto findings = fold::detectUninitLocalReadInContext(unit->getASTContext());
REQUIRE(findings.size() >= 1);
REQUIRE(findings[0].kind == fold::FindingKind::UninitLocalRead);
}