Initial import: the house-style lint rules as a crafter-build module
Moved out of Crafter.Build's lint-rules.h so a project.cpp pulls them in with `#pragma crafter dependency` + `import Crafter.LintRules;` instead of copying a header. The rule bodies are unchanged; format-concat now sits with the other transforms (it rewrites, it was registered as a report), and the helpers are module-internal. tests/HouseRules moved along with the rules and links crafter-build from git, or the sibling checkout with --local. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
commit
88139fc0fc
7 changed files with 2436 additions and 0 deletions
513
tests/HouseRules/main.cpp
Normal file
513
tests/HouseRules/main.cpp
Normal file
|
|
@ -0,0 +1,513 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
import std;
|
||||
import Crafter.Build;
|
||||
import Crafter.LintRules;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter;
|
||||
|
||||
// Validates the house-style ruleset — the transforms and reports every repo
|
||||
// that imports Crafter.LintRules relies 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);
|
||||
LintRules::AddRules(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 splits on the AST, so each declarator carries its own
|
||||
// resolved type. That is the whole reason it is not a token rule: copying
|
||||
// the head's type prefix turns `int* a, b;` into `int* a; int* b;` and
|
||||
// silently changes b from int to int*.
|
||||
{
|
||||
RuleRun r = RunRule("#include <vector>\n"
|
||||
"void F() {\n"
|
||||
" bool a = false, b = true;\n"
|
||||
" int* ptrA = nullptr, *ptrB = nullptr;\n"
|
||||
" int* mixed = nullptr, plain = 7;\n"
|
||||
" std::vector<int> tmplA{}, tmplB{};\n"
|
||||
" int callA = g(), callB = h();\n"
|
||||
" int keep = 1, kept = 2; // trailing comment\n"
|
||||
"}\n",
|
||||
"single-declaration", LintMode::Apply);
|
||||
Check(r.text.contains("bool a = false;\n bool b = true;"), "single-declaration: simple split");
|
||||
Check(r.text.contains("int* ptrA = nullptr;\n int* ptrB = nullptr;"), "single-declaration: pointers split, star kept on the type");
|
||||
// The case a token-based splitter gets wrong.
|
||||
Check(r.text.contains("int* mixed = nullptr;\n int plain = 7;"), "single-declaration: only the starred declarator is a pointer");
|
||||
Check(r.text.contains("std::vector<int> tmplA{};\n std::vector<int> tmplB{};"), "single-declaration: template arguments are not a bail-out");
|
||||
Check(r.text.contains("int callA = g();\n int callB = h();"), "single-declaration: call initialisers are not a bail-out");
|
||||
// A comment after the ';' is outside the replaced range, so it survives;
|
||||
// the line-based version refused the whole line instead.
|
||||
Check(r.text.contains("int keep = 1;\n int kept = 2; // trailing comment"), "single-declaration: trailing comment survives the split");
|
||||
RuleRun again = RunRule(r.text, "single-declaration", LintMode::Apply);
|
||||
Check(again.summary.changedFiles.empty(), "single-declaration: idempotent");
|
||||
}
|
||||
|
||||
// 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");
|
||||
}
|
||||
|
||||
// The guards used to be substring probes over the raw line, so a literal
|
||||
// whose TEXT contained `//` or `R"` looked like a comment or a raw string
|
||||
// and silently disabled the rule. Both shapes occur in this repo's own
|
||||
// sources — "MARKER" ends in the characters R", and any string mentioning
|
||||
// a lint-disable directive contains //.
|
||||
{
|
||||
RuleRun r = RunRule("void F() {\n" " auto hit = text.find(\"MARKER\",\n" " start);\n" "}\n", "wrap-join", LintMode::Apply);
|
||||
Check(r.text.contains("text.find(\"MARKER\", start);"), "R\" inside a literal no longer blocks wrap-join");
|
||||
}
|
||||
{
|
||||
RuleRun r = RunRule("void F() {\n" " Check(read() == \"// lint-disable-next-line trim\\n\",\n" " \"message\");\n" "}\n", "wrap-join", LintMode::Apply);
|
||||
Check(r.text.contains("\\n\", \"message\");"), "// inside a literal no longer blocks wrap-join");
|
||||
}
|
||||
|
||||
// A real trailing comment still blocks the join: text pulled up past a
|
||||
// `//` would be swallowed by it.
|
||||
{
|
||||
RuleRun r = RunRule("void F() {\n" " auto v = g(alpha, // why\n" " beta);\n" "}\n", "wrap-join", LintMode::Apply);
|
||||
Check(r.text.contains("g(alpha, // why\n"), "a real comment still blocks wrap-join");
|
||||
}
|
||||
|
||||
// A genuinely multi-line literal is never reflowed — joining its lines
|
||||
// would change the string's contents.
|
||||
{
|
||||
std::string_view source = "void F() {\n"
|
||||
" auto text = R\"sql(SELECT a,\n"
|
||||
" b FROM t)sql\";\n"
|
||||
"}\n";
|
||||
RuleRun r = RunRule(source, "wrap-join", LintMode::Apply);
|
||||
Check(r.text == source, "wrap-join leaves a multi-line raw string alone");
|
||||
RuleRun paren = RunRule(source, "paren-spacing", LintMode::Apply);
|
||||
Check(paren.text == source, "paren-spacing leaves a multi-line raw string alone");
|
||||
}
|
||||
|
||||
// Type keywords inside a literal are text, not declarations.
|
||||
{
|
||||
std::string_view source = "void F() {\n"
|
||||
" auto sql = R\"q(int x; unsigned long y;)q\";\n"
|
||||
"}\n";
|
||||
RuleRun r = RunRule(source, "fixed-width-types", LintMode::Apply);
|
||||
Check(r.text == source, "fixed-width-types leaves type names inside a raw string alone");
|
||||
}
|
||||
|
||||
// no-char-pointer reads the AST, so it distinguishes OUR char* from one
|
||||
// whose spelling belongs to somebody else's header. This is what replaced
|
||||
// the substring denylist (argv, getenv, c_str, reinterpret_cast, …): each
|
||||
// entry there disabled the rule for a whole line, and the list could only
|
||||
// grow as new libraries arrived.
|
||||
{
|
||||
RuleRun r = RunRule("#include <cstdlib>\n"
|
||||
"#include <string>\n"
|
||||
"extern \"C\" const char* CApiEntry(const char* path);\n"
|
||||
"char* OurBadApi(char* input) { return input; }\n"
|
||||
"void F() {\n"
|
||||
" char* fromLibc = std::getenv(\"HOME\");\n"
|
||||
" std::string mine = \"ok\";\n"
|
||||
" const char* toLibc = mine.c_str();\n"
|
||||
" auto raw = reinterpret_cast<char*>(&mine);\n"
|
||||
"}\n",
|
||||
"no-char-pointer", LintMode::Report);
|
||||
// Ours, so reported: the declaration and its parameter.
|
||||
Check(HasFinding(r.summary, "'OurBadApi'"), "no-char-pointer: our own char* return is reported");
|
||||
Check(HasFinding(r.summary, "'input'"), "no-char-pointer: our own char* parameter is reported");
|
||||
// Foreign, so exempt — each for a reason, not by name.
|
||||
Check(!HasFinding(r.summary, "'CApiEntry'"), "no-char-pointer: extern \"C\" declaration is exempt");
|
||||
Check(!HasFinding(r.summary, "'path'"), "no-char-pointer: extern \"C\" parameter is exempt");
|
||||
Check(!HasFinding(r.summary, "'fromLibc'"), "no-char-pointer: a value from libc is exempt");
|
||||
Check(!HasFinding(r.summary, "'toLibc'"), "no-char-pointer: a value from c_str() is exempt");
|
||||
// A local whose deduced type is char* is still our declaration, so it
|
||||
// is reported. The old denylist exempted every line mentioning
|
||||
// reinterpret_cast; a deliberate low-level cast now takes an explicit
|
||||
// lint-disable comment, which is at least visible at the site.
|
||||
Check(HasFinding(r.summary, "'raw'"), "no-char-pointer: a deduced char* local is still ours");
|
||||
}
|
||||
|
||||
// Cases the line-based heuristic structurally could not see. It only looked
|
||||
// at declarations starting at cumulative paren depth 0 and inferred scope
|
||||
// from a running {/} count, so a wrapped signature, a specifier split over
|
||||
// two lines, or a local shadowing a member all escaped it.
|
||||
{
|
||||
RuleRun r = RunRule("#include <string>\n"
|
||||
"namespace Outer {\n"
|
||||
" int wrapped_function(\n"
|
||||
" int first,\n"
|
||||
" int second) { return first + second; }\n"
|
||||
" static\n"
|
||||
" int splitStatic = 1;\n"
|
||||
" struct Holder {\n"
|
||||
" int Member = 0;\n"
|
||||
" void Method() {\n"
|
||||
" int Local = 1;\n"
|
||||
" (void)Local;\n"
|
||||
" }\n"
|
||||
" };\n"
|
||||
"}\n"
|
||||
"extern \"C\" int c_api_entry(const char* name);\n",
|
||||
"naming", LintMode::Report);
|
||||
// A signature wrapped over lines is still a function declaration.
|
||||
Check(HasFinding(r.summary, "'wrapped_function' should be PascalCase"), "naming: wrapped signature is seen");
|
||||
// `static` on its own line still applies to the declaration below it.
|
||||
Check(HasFinding(r.summary, "'splitStatic' should be PascalCase"), "naming: split specifier is seen");
|
||||
// Members are camelCase; the enclosing kind decides, not a brace count.
|
||||
Check(HasFinding(r.summary, "'Member' should be camelCase"), "naming: member is checked as a member");
|
||||
// A local inside a method is a local, not a member and not a global.
|
||||
Check(HasFinding(r.summary, "'Local' should be camelCase"), "naming: local inside a method is a local");
|
||||
// Named by libc, not by us.
|
||||
Check(!HasFinding(r.summary, "c_api_entry"), "naming: extern \"C\" declaration is exempt");
|
||||
Check(!HasFinding(r.summary, "'Method'"), "naming: PascalCase method passes");
|
||||
Check(!HasFinding(r.summary, "'Holder'"), "naming: PascalCase type passes");
|
||||
}
|
||||
|
||||
// fixed-width-types must not rewrite an integer whose width somebody else's
|
||||
// header chose — the rewrite would leave code that no longer matches the API
|
||||
// it calls. Derived from the AST, so the exemption is per-declaration rather
|
||||
// than the old per-line textual one, which both missed cases and disabled
|
||||
// the rule for anything else sharing a line with `argc`/`extern "`.
|
||||
{
|
||||
RuleRun r = RunRule("#include <cstdlib>\n"
|
||||
"extern \"C\" unsigned long CApiCall(unsigned int flags);\n"
|
||||
"long OurOwnApi(int value) { return value; }\n"
|
||||
"void F() {\n"
|
||||
" unsigned long fromLibc = std::strtoul(\"1\", nullptr, 10);\n"
|
||||
" unsigned ours = 1;\n"
|
||||
" (void)fromLibc; (void)ours;\n"
|
||||
"}\n"
|
||||
"int main(int argc, char** argv) {\n"
|
||||
" for (int i = 0; i < argc; ++i) { (void)argv[i]; }\n"
|
||||
" return 0;\n"
|
||||
"}\n",
|
||||
"fixed-width-types", LintMode::Apply);
|
||||
// Somebody else's widths, kept.
|
||||
Check(r.text.contains("extern \"C\" unsigned long CApiCall(unsigned int flags);"), "fixed-width: extern \"C\" signature keeps its widths");
|
||||
Check(r.text.contains("unsigned long fromLibc = std::strtoul"), "fixed-width: a value from a C library keeps its width");
|
||||
Check(r.text.contains("int main(int argc, char** argv)"), "fixed-width: main's signature is untouched");
|
||||
// Ours, converted.
|
||||
Check(r.text.contains("std::int64_t OurOwnApi(std::int32_t value)"), "fixed-width: our own signature converts");
|
||||
Check(r.text.contains("std::uint32_t ours = 1;"), "fixed-width: our own local converts");
|
||||
// The loop counter inside main's BODY is ordinary code. The old rule
|
||||
// skipped it because `argc` appeared on the same line; only the
|
||||
// signature is exempt now, not everything near it.
|
||||
Check(r.text.contains("for (std::int32_t i = 0;"), "fixed-width: main's body is not exempt, only its signature");
|
||||
}
|
||||
|
||||
// enum-class asks for the next TOKEN after `enum`, so a declaration split
|
||||
// over lines reads the same as one that is not. The regex it replaced
|
||||
// required the name to follow `enum` on the same line.
|
||||
{
|
||||
RuleRun r = RunRule("enum Plain { A };\n"
|
||||
"enum\n"
|
||||
" Split { B };\n"
|
||||
"enum class Scoped { C };\n"
|
||||
"enum\n"
|
||||
" class SplitScoped { D };\n"
|
||||
"enum struct AlsoFine { E };\n",
|
||||
"enum-class", LintMode::Report);
|
||||
Check(r.summary.findings.size() == 2, std::format("enum-class: exactly the two plain enums ({} found)", r.summary.findings.size()));
|
||||
const bool onFirst = std::any_of(r.summary.findings.begin(), r.summary.findings.end(), [](const LintFinding& f) { return f.line == 1; });
|
||||
const bool onSplit = std::any_of(r.summary.findings.begin(), r.summary.findings.end(), [](const LintFinding& f) { return f.line == 2; });
|
||||
Check(onFirst, "enum-class: single-line plain enum reported");
|
||||
Check(onSplit, "enum-class: line-split plain enum reported at the keyword");
|
||||
}
|
||||
|
||||
// const-local's mutation analysis is exact for scalars: assignment, ++/--,
|
||||
// address-of and binding to a non-const reference are the only ways to
|
||||
// write one, and all four are tracked. Both directions matter — a missed
|
||||
// write means advising const on something that cannot be const.
|
||||
{
|
||||
RuleRun r = RunRule("void Mutate(int& out);\n"
|
||||
"void ReadOnly(const int& in);\n"
|
||||
"void ByValue(int v);\n"
|
||||
"int Compute();\n"
|
||||
"void F() {\n"
|
||||
" int neverWritten = 1;\n"
|
||||
" int assigned = 1; assigned = 2;\n"
|
||||
" int incremented = 1; ++incremented;\n"
|
||||
" int compound = 1; compound += 2;\n"
|
||||
" int addressed = 1; int* taken = &addressed;\n"
|
||||
" int toMutatingRef = 1; Mutate(toMutatingRef);\n"
|
||||
" int toConstRef = 1; ReadOnly(toConstRef);\n"
|
||||
" int toByValue = 1; ByValue(toByValue);\n"
|
||||
" int boundToRef = 1; int& alias = boundToRef;\n"
|
||||
" for (int loop = 0; loop < 1; ++loop) { (void)loop; }\n"
|
||||
" (void)taken; (void)alias;\n"
|
||||
"}\n",
|
||||
"const-local", LintMode::Report);
|
||||
Check(HasFinding(r.summary, "'neverWritten'"), "const-local: an unwritten local is reported");
|
||||
Check(HasFinding(r.summary, "'toConstRef'"), "const-local: passing to a const& is not a write");
|
||||
Check(HasFinding(r.summary, "'toByValue'"), "const-local: passing by value is not a write");
|
||||
Check(!HasFinding(r.summary, "'assigned'"), "const-local: assignment is a write");
|
||||
Check(!HasFinding(r.summary, "'incremented'"), "const-local: ++ is a write");
|
||||
Check(!HasFinding(r.summary, "'compound'"), "const-local: += is a write");
|
||||
Check(!HasFinding(r.summary, "'addressed'"), "const-local: taking an address counts as a write");
|
||||
Check(!HasFinding(r.summary, "'toMutatingRef'"), "const-local: binding to a non-const& parameter is a write");
|
||||
Check(!HasFinding(r.summary, "'boundToRef'"), "const-local: binding to a non-const& local is a write");
|
||||
Check(!HasFinding(r.summary, "'loop'"), "const-local: a mutated loop counter is not reported");
|
||||
// Pointers and range-for bindings are excluded: `T* const p` and
|
||||
// `for (T* const x : …)` are not spellings anybody writes.
|
||||
Check(!HasFinding(r.summary, "'taken'"), "const-local: pointer locals are out of scope");
|
||||
}
|
||||
{
|
||||
RuleRun r = RunRule("void F() {\n" " for (int each : Range()) { (void)each; }\n" "}\n", "const-local", LintMode::Report);
|
||||
Check(!HasFinding(r.summary, "'each'"), "const-local: a range-for binding is not reported");
|
||||
}
|
||||
|
||||
// constexpr-constant asks clang's constant evaluator, not the tokens of the
|
||||
// initialiser. sizeof and a fold over other constants are constant
|
||||
// expressions that no token scan can recognise; a call result is not.
|
||||
{
|
||||
RuleRun r = RunRule("int Compute();\n"
|
||||
"constexpr int Base = 4;\n"
|
||||
"struct Big { int a, b; };\n"
|
||||
"void F() {\n"
|
||||
" const int literal = 4;\n"
|
||||
" const int folded = 1 << 4;\n"
|
||||
" const int fromSizeof = sizeof(Big);\n"
|
||||
" const int fromOtherConstant = Base + 1;\n"
|
||||
" const int fromCall = Compute();\n"
|
||||
" constexpr int already = 8;\n"
|
||||
" (void)literal; (void)folded; (void)fromSizeof;\n"
|
||||
" (void)fromOtherConstant; (void)fromCall; (void)already;\n"
|
||||
"}\n",
|
||||
"constexpr-constant", LintMode::Report);
|
||||
Check(HasFinding(r.summary, "'literal'"), "constexpr: a literal constant is reported");
|
||||
Check(HasFinding(r.summary, "'folded'"), "constexpr: an operator fold over literals is reported");
|
||||
// Neither of these is reachable from a token scan of the initialiser.
|
||||
Check(HasFinding(r.summary, "'fromSizeof'"), "constexpr: sizeof folds");
|
||||
Check(HasFinding(r.summary, "'fromOtherConstant'"), "constexpr: a fold over another constant folds");
|
||||
Check(!HasFinding(r.summary, "'fromCall'"), "constexpr: a call result is not a constant expression");
|
||||
Check(!HasFinding(r.summary, "'already'"), "constexpr: an existing constexpr is not re-reported");
|
||||
}
|
||||
|
||||
if (Failures > 0) {
|
||||
std::println(std::cerr, "{} assertions failed", Failures);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
Loading…
Reference in a new issue