116 lines
3.7 KiB
C++
116 lines
3.7 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/complexity.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");
|
|
}
|
|
|
|
std::vector<fold::Finding> run(const std::string& code, int threshold) {
|
|
auto unit = parse(code);
|
|
REQUIRE(unit);
|
|
return fold::detectComplexityInContext(unit->getASTContext(), threshold);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("complexity: a branchy function over threshold is flagged", "[complexity]") {
|
|
// Six independent decision points -> cyclomatic complexity 7.
|
|
auto findings = run(R"(
|
|
int f(int x) {
|
|
int r = 0;
|
|
if (x > 0) r++;
|
|
if (x > 1) r++;
|
|
if (x > 2) r++;
|
|
if (x > 3) r++;
|
|
if (x > 4) r++;
|
|
if (x > 5) r++;
|
|
return r;
|
|
}
|
|
)",
|
|
5);
|
|
REQUIRE_FALSE(findings.empty());
|
|
REQUIRE(findings[0].kind == fold::FindingKind::HighComplexity);
|
|
REQUIRE(findings[0].confidence == fold::Confidence::Syntactic);
|
|
// McCabe remains the compatibility gate and message score, while
|
|
// independent dimensions provide explainable ranking evidence.
|
|
REQUIRE(findings[0].message.find("complexity 7") != std::string::npos);
|
|
REQUIRE(findings[0].evidence.quality.has_value());
|
|
const auto& quality = *findings[0].evidence.quality;
|
|
REQUIRE(quality.classification == "complexity-hotspot");
|
|
REQUIRE(quality.metrics.size() == 6);
|
|
CHECK(quality.metrics[0].name == "mccabe");
|
|
CHECK(quality.metrics[0].value == 7.0);
|
|
CHECK(quality.rank_reasons.front() == "mccabe");
|
|
}
|
|
|
|
TEST_CASE("complexity: dimensions separate nesting, state, boolean, loops, and switches",
|
|
"[complexity][dimensions]") {
|
|
auto findings = run(R"(
|
|
int dimensions(int x) {
|
|
int result = 0;
|
|
for (int i = 0; i < x; ++i) {
|
|
if (i > 1 && i < 4) {
|
|
switch (i) {
|
|
case 2: result = 1; break;
|
|
default: result = 2; break;
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
)",
|
|
1);
|
|
REQUIRE(findings.size() == 1);
|
|
REQUIRE(findings[0].evidence.quality.has_value());
|
|
const auto& metrics = findings[0].evidence.quality->metrics;
|
|
const auto metric = [&metrics](const std::string& name) {
|
|
for (const auto& item : metrics) {
|
|
if (item.name == name)
|
|
return item.value;
|
|
}
|
|
return -1.0;
|
|
};
|
|
CHECK(metric("max_control_nesting") == 3.0);
|
|
CHECK(metric("state_mutations") == 3.0);
|
|
CHECK(metric("boolean_operators") == 1.0);
|
|
CHECK(metric("loops") == 1.0);
|
|
CHECK(metric("switch_cases") == 2.0);
|
|
}
|
|
|
|
TEST_CASE("complexity: a straight-line function is not flagged", "[complexity]") {
|
|
auto findings = run(R"(
|
|
int g(int x) {
|
|
int y = x + 1;
|
|
return y;
|
|
}
|
|
)",
|
|
5);
|
|
REQUIRE(findings.empty());
|
|
}
|
|
|
|
TEST_CASE("complexity: threshold boundary is inclusive", "[complexity]") {
|
|
// Three ifs -> complexity 4.
|
|
const std::string code = R"(
|
|
int h(int x) {
|
|
int r = 0;
|
|
if (x > 0) r++;
|
|
if (x > 1) r++;
|
|
if (x > 2) r++;
|
|
return r;
|
|
}
|
|
)";
|
|
// At threshold 4 the function fires (complexity == threshold).
|
|
REQUIRE_FALSE(run(code, 4).empty());
|
|
// At threshold 5 it does not (complexity < threshold).
|
|
REQUIRE(run(code, 5).empty());
|
|
}
|