Some checks failed
CI / build-test-release (push) Failing after 7m15s
260 lines
13 KiB
C++
260 lines
13 KiB
C++
// SPDX-License-Identifier: LGPL-3.0-only
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
import std;
|
|
import Crafter.Build;
|
|
#include "../../lint-rules.h"
|
|
namespace fs = std::filesystem;
|
|
using namespace Crafter;
|
|
|
|
// Validates the house-style ruleset in lint-rules.h — the transforms and
|
|
// reports this repo (and sibling repos that copy the header) rely on. Each
|
|
// case writes a scratch file, runs ONE rule via the glob filter, and asserts
|
|
// the findings and/or rewritten bytes.
|
|
namespace {
|
|
std::int32_t Failures = 0;
|
|
|
|
void Check(bool cond, std::string_view msg) {
|
|
if (!cond) {
|
|
std::println(std::cerr, "FAIL: {}", msg);
|
|
++Failures;
|
|
}
|
|
}
|
|
|
|
struct RuleRun {
|
|
LintSummary summary;
|
|
std::string text; // file content after the run
|
|
};
|
|
|
|
RuleRun RunRule(std::string_view content, std::string_view rule, LintMode mode) {
|
|
static std::int32_t Counter = 0;
|
|
fs::path dir = fs::temp_directory_path() / "crafter-build-house-rules" / std::format("case-{}", Counter++);
|
|
fs::remove_all(dir);
|
|
fs::create_directories(dir);
|
|
{
|
|
std::ofstream f(dir / "f.cpp", std::ios::binary | std::ios::trunc);
|
|
f.write(content.data(), static_cast<std::streamsize>(content.size()));
|
|
}
|
|
Configuration cfg;
|
|
cfg.path = dir;
|
|
cfg.name = "house-fixture";
|
|
cfg.outputName = "house-fixture";
|
|
cfg.target = HostTarget();
|
|
std::array<fs::path, 0> ifaces = {};
|
|
std::array<fs::path, 1> impls = { "f" };
|
|
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
|
ProjectLint::AddProjectLintRules(cfg);
|
|
RunLintOptions opts;
|
|
opts.mode = mode;
|
|
opts.globs = { std::string(rule) };
|
|
RuleRun run;
|
|
run.summary = RunLint(cfg, opts);
|
|
std::ifstream f(dir / "f.cpp", std::ios::binary);
|
|
std::stringstream buffer;
|
|
buffer << f.rdbuf();
|
|
run.text = std::move(buffer).str();
|
|
return run;
|
|
}
|
|
|
|
bool HasFinding(const LintSummary& s, std::string_view fragment) {
|
|
return std::any_of(s.findings.begin(), s.findings.end(), [&](const LintFinding& f) { return f.message.contains(fragment); });
|
|
}
|
|
}
|
|
|
|
int main() {
|
|
// fixed-width-types: int/long long convert; main/argc and extern "C" stay.
|
|
{
|
|
RuleRun r = RunRule("int Foo(long long v) { int x = 5; return x; }\n" "extern \"C\" int setenv(const char* n, const char* v, int o);\n" "int main(int argc, char** argv) { return 0; }\n", "fixed-width-types", LintMode::Apply);
|
|
Check(r.text.contains("std::int32_t Foo(std::int64_t v) { std::int32_t x = 5;"), "fixed-width converts int and long long");
|
|
Check(r.text.contains("extern \"C\" int setenv"), "extern \"C\" prototype keeps int");
|
|
Check(r.text.contains("int main(int argc"), "main/argc keep int");
|
|
}
|
|
|
|
// fixed-width-types is signed/unsigned aware, in any specifier order;
|
|
// bare char (text) and long double (floating) are not integers.
|
|
{
|
|
RuleRun r = RunRule("void F() {\n"
|
|
" unsigned a = 1;\n"
|
|
" unsigned int b = 2;\n"
|
|
" unsigned long long c = 3;\n"
|
|
" long unsigned int d = 4;\n"
|
|
" unsigned short e = 5;\n"
|
|
" signed f = 6;\n"
|
|
" signed char g = 7;\n"
|
|
" auto h = static_cast<unsigned char>(g);\n"
|
|
" char text = 'x';\n"
|
|
" long double pi = 3.14L;\n"
|
|
"}\n",
|
|
"fixed-width-types", LintMode::Apply);
|
|
Check(r.text.contains("std::uint32_t a = 1;"), "bare unsigned -> uint32");
|
|
Check(r.text.contains("std::uint32_t b = 2;"), "unsigned int -> uint32");
|
|
Check(r.text.contains("std::uint64_t c = 3;"), "unsigned long long -> uint64");
|
|
Check(r.text.contains("std::uint64_t d = 4;"), "long unsigned int (reordered) -> uint64");
|
|
Check(r.text.contains("std::uint16_t e = 5;"), "unsigned short -> uint16");
|
|
Check(r.text.contains("std::int32_t f = 6;"), "bare signed -> int32");
|
|
Check(r.text.contains("std::int8_t g = 7;"), "signed char -> int8");
|
|
Check(r.text.contains("static_cast<std::uint8_t>(g)"), "unsigned char -> uint8");
|
|
Check(r.text.contains("char text = 'x';"), "bare char stays (text, not an integer)");
|
|
Check(r.text.contains("long double pi = 3.14L;"), "long double stays (floating type)");
|
|
}
|
|
|
|
// brace-style: Allman brace joins; standalone scope block stays.
|
|
{
|
|
RuleRun r = RunRule("void Foo()\n{\n}\n\nvoid Bar() {\n Baz();\n {\n Qux();\n }\n}\n", "brace-style", LintMode::Apply);
|
|
Check(r.text.contains("void Foo() {"), "Allman brace joined onto header");
|
|
Check(r.text.contains("Baz();\n {"), "scope block brace untouched");
|
|
}
|
|
|
|
// if-single-line: short body joins; braced body stays.
|
|
{
|
|
RuleRun r = RunRule("void F(bool b) {\n if (b)\n Run();\n if (b) {\n Run();\n }\n}\n", "if-single-line", LintMode::Apply);
|
|
Check(r.text.contains("if (b) Run();"), "single-statement if body joined");
|
|
Check(r.text.contains("if (b) {"), "braced if body untouched");
|
|
}
|
|
|
|
// single-declaration: simple multi-decl splits; pointer decl only reports.
|
|
{
|
|
RuleRun r = RunRule("void F() {\n bool a = false, b = true;\n}\n", "single-declaration", LintMode::Apply);
|
|
Check(r.text.contains("bool a = false;\n bool b = true;"), "multi-declaration split");
|
|
}
|
|
|
|
// wrap-join: short wrapped call joins; operator chain joins; long stays.
|
|
{
|
|
RuleRun r = RunRule("void F() {\n G(alpha,\n beta);\n bool x = alpha\n && beta;\n}\n", "wrap-join", LintMode::Apply);
|
|
Check(r.text.contains("G(alpha, beta);"), "wrapped call arguments joined");
|
|
Check(r.text.contains("bool x = alpha && beta;"), "operator continuation joined");
|
|
}
|
|
|
|
// format-concat: literal-adjacent + rewrites; += RHS rewrites; args survive.
|
|
{
|
|
RuleRun r = RunRule("void F(std::string name, std::string cmd) {\n"
|
|
" std::string a = name + \".cpp\";\n"
|
|
" std::string b = \"pre-\" + name + \"-post\";\n"
|
|
" cmd += \" \" + name;\n"
|
|
" std::string keep = name + cmd;\n"
|
|
" fs::path p;\n"
|
|
" std::string c = p.stem().string() + \".pcm\";\n"
|
|
" fs::path d = std::string(cmd) + \".so\";\n"
|
|
"}\n",
|
|
"format-concat", LintMode::Apply);
|
|
Check(r.text.contains("std::string a = std::format(\"{}.cpp\", name);"), "trailing literal converts");
|
|
Check(!r.text.contains("namestd::format"), "replacement splices at the operand boundary");
|
|
Check(r.text.contains("std::string b = std::format(\"pre-{}-post\", name);"), "sandwich chain converts");
|
|
Check(r.text.contains("cmd += std::format(\" {}\", name);"), "+= RHS converts");
|
|
Check(r.text.contains("std::string keep = name + cmd;"), "literal-free + is left alone");
|
|
Check(r.text.contains("std::string c = std::format(\"{}.pcm\", p.stem().string());"), "call-chain left operand consumed whole (the stringstd regression)");
|
|
Check(r.text.contains("fs::path d = std::format(\"{}.so\", std::string(cmd));"), "constructor-call left operand consumed whole");
|
|
}
|
|
{
|
|
// Ternary and raw-string lines are reported, never rewritten.
|
|
RuleRun r = RunRule("std::string F(bool b, std::string n) { return b ? n + \".x\" : n; }\n", "format-concat", LintMode::Report);
|
|
Check(HasFinding(r.summary, "not auto-fixable"), "ternary concat reports instead of fixing");
|
|
Check(r.text.contains("n + \".x\""), "ternary concat untouched on disk");
|
|
}
|
|
|
|
// paren-spacing: single-space padding removed; wrapped call ends keep.
|
|
{
|
|
RuleRun r = RunRule("void F() {\n G( x );\n}\n", "paren-spacing", LintMode::Apply);
|
|
Check(r.text.contains("G(x);"), "paren padding removed");
|
|
}
|
|
|
|
// naming: wrong-case function/type/static/global flagged; camel local passes.
|
|
{
|
|
RuleRun r = RunRule("namespace {\n"
|
|
" std::int32_t bad_global = 0;\n"
|
|
" struct lint_thing {};\n"
|
|
"}\n"
|
|
"void lower_func() {\n"
|
|
" static bool lowerStatic = false;\n"
|
|
" std::int32_t fineLocal = 1;\n"
|
|
"}\n",
|
|
"naming", LintMode::Report);
|
|
Check(HasFinding(r.summary, "'bad_global' should be PascalCase"), "lowercase global flagged");
|
|
Check(HasFinding(r.summary, "'lint_thing' should be PascalCase"), "lowercase type flagged");
|
|
Check(HasFinding(r.summary, "'lower_func' should be PascalCase"), "lowercase function flagged");
|
|
Check(HasFinding(r.summary, "'lowerStatic' should be PascalCase"), "camel static flagged");
|
|
Check(!HasFinding(r.summary, "fineLocal"), "camelCase local passes");
|
|
}
|
|
{
|
|
// Statement calls with inline lambda arguments are NOT function
|
|
// definitions (the any_of regression); a genuine lowercase one-liner
|
|
// method still is.
|
|
RuleRun r = RunRule("struct Holder {\n"
|
|
" bool clean() const { return true; }\n"
|
|
"};\n"
|
|
"void T(std::vector<std::int32_t>& v) {\n"
|
|
" bool x = std::any_of(v.begin(), v.end(), [](std::int32_t n) { return n > 0; });\n"
|
|
" std::erase_if(v, [](std::int32_t n) { return n < 0; });\n"
|
|
"}\n",
|
|
"naming", LintMode::Report);
|
|
Check(!HasFinding(r.summary, "any_of"), "call with lambda argument is not a function definition");
|
|
Check(!HasFinding(r.summary, "erase_if"), "bare statement call is not a function definition");
|
|
Check(HasFinding(r.summary, "'clean' should be PascalCase"), "lowercase one-liner method still flagged");
|
|
}
|
|
|
|
// enum-class + no-iostream-print reports.
|
|
{
|
|
RuleRun r = RunRule("enum Color { Red };\n", "enum-class", LintMode::Report);
|
|
Check(HasFinding(r.summary, "enum class"), "plain enum flagged");
|
|
}
|
|
{
|
|
RuleRun r = RunRule("void F() { std::cout << 1; }\n", "no-iostream-print", LintMode::Report);
|
|
Check(HasFinding(r.summary, "std::println"), "std::cout flagged");
|
|
}
|
|
|
|
// The whole ruleset is idempotent: a second Apply changes nothing.
|
|
{
|
|
std::string_view source = "int Foo()\n{\n std::string s = std::string(\"a\") + \"b\";\n if (true)\n return 1;\n return 0;\n}\n";
|
|
static constexpr std::string_view AllRules = "*";
|
|
RuleRun first = RunRule(source, AllRules, LintMode::Apply);
|
|
RuleRun second = RunRule(first.text, AllRules, LintMode::Apply);
|
|
Check(second.summary.changedFiles.empty(), "second apply of the full ruleset is a no-op");
|
|
Check(first.text != source, "first apply actually changed the fixture");
|
|
}
|
|
|
|
// Suppression directives against house transforms: the driver reverts
|
|
// line-preserving edits (fixed-width) and the count-changing rules check
|
|
// Suppressed() themselves (wrap-join).
|
|
{
|
|
RuleRun r = RunRule("void F() {\n"
|
|
" // lint-disable-next-line fixed-width-types\n"
|
|
" int keep = 1;\n"
|
|
" int convert = 2;\n"
|
|
"}\n",
|
|
"fixed-width-types", LintMode::Apply);
|
|
Check(r.text.contains("int keep = 1;"), "next-line directive keeps the suppressed int");
|
|
Check(r.text.contains("std::int32_t convert = 2;"), "unsuppressed line still converts");
|
|
}
|
|
{
|
|
RuleRun r = RunRule("void F() {\n"
|
|
" // lint-disable-next-line wrap-join\n"
|
|
" G(alpha,\n"
|
|
" beta);\n"
|
|
" H(alpha,\n"
|
|
" beta);\n"
|
|
"}\n",
|
|
"wrap-join", LintMode::Apply);
|
|
Check(r.text.contains("G(alpha,\n"), "suppressed wrap stays wrapped");
|
|
Check(r.text.contains("H(alpha, beta);"), "unsuppressed wrap still joins");
|
|
}
|
|
|
|
// wrap-join converges in ONE run: joining the inner paren wrap balances
|
|
// the && line, which only then becomes an operator-joinable continuation.
|
|
{
|
|
RuleRun r = RunRule("void F() {\n"
|
|
" bool ok = alpha\n"
|
|
" && beta(gamma,\n"
|
|
" delta);\n"
|
|
"}\n",
|
|
"wrap-join", LintMode::Apply);
|
|
Check(r.text.contains("bool ok = alpha && beta(gamma, delta);"), "self-enabling joins reach the fixpoint in one apply");
|
|
RuleRun second = RunRule(r.text, "wrap-join", LintMode::Apply);
|
|
Check(second.summary.changedFiles.empty(), "wrap-join is idempotent after the fixpoint");
|
|
}
|
|
|
|
if (Failures > 0) {
|
|
std::println(std::cerr, "{} assertions failed", Failures);
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|