2026-10-02 19:57:28 +02:00
// 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 \n void 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 " ) ;
}
2026-10-02 20:42:21 +02:00
// naming: wrong-case function/type/global flagged; camelCase local and
// static pass.
2026-10-02 19:57:28 +02:00
{
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 ) ;
2026-10-02 20:42:21 +02:00
Check ( HasFinding ( r . summary , " 'bad_global' should be camelCase " ) , " snake_case global flagged " ) ;
2026-10-02 19:57:28 +02:00
Check ( HasFinding ( r . summary , " 'lint_thing' should be PascalCase " ) , " lowercase type flagged " ) ;
Check ( HasFinding ( r . summary , " 'lower_func' should be PascalCase " ) , " lowercase function flagged " ) ;
2026-10-02 20:42:21 +02:00
Check ( ! HasFinding ( r . summary , " lowerStatic " ) , " camelCase static passes " ) ;
2026-10-02 19:57:28 +02:00
Check ( ! HasFinding ( r . summary , " fineLocal " ) , " camelCase local passes " ) ;
}
2026-10-02 20:42:21 +02:00
// naming: statics and constants are variables, so camelCase like any
// other wherever they live — a static local, a static or constexpr
// member, a namespace-scope constant or a plain global.
{
RuleRun r = RunRule ( " namespace { \n "
" constexpr int MaxSize = 4; \n "
" const int DEFAULT_PORT = 80; \n "
" static int kCount = 0; \n "
" int plainGlobal = 0; \n "
" } \n "
" struct Holder { \n "
" static int Instances; \n "
" static constexpr int kLimit = 8; \n "
" int Field = 0; \n "
" }; \n "
" void Fn() { \n "
" static int Counter = 0; \n "
" static constexpr int localMax = 2; \n "
" (void)Counter; \n "
" } \n " ,
" naming " , LintMode : : Report ) ;
Check ( HasFinding ( r . summary , " 'MaxSize' should be camelCase " ) , " naming: PascalCase constexpr constant flagged " ) ;
Check ( HasFinding ( r . summary , " 'DEFAULT_PORT' should be camelCase " ) , " naming: SCREAMING_CASE constant flagged " ) ;
Check ( ! HasFinding ( r . summary , " kCount " ) , " naming: camelCase static global passes " ) ;
Check ( ! HasFinding ( r . summary , " plainGlobal " ) , " naming: camelCase global passes " ) ;
Check ( HasFinding ( r . summary , " 'Instances' should be camelCase " ) , " naming: PascalCase static member flagged " ) ;
Check ( ! HasFinding ( r . summary , " kLimit " ) , " naming: camelCase static constexpr member passes " ) ;
Check ( HasFinding ( r . summary , " 'Field' should be camelCase " ) , " naming: PascalCase member flagged " ) ;
Check ( HasFinding ( r . summary , " 'Counter' should be camelCase " ) , " naming: PascalCase static local flagged " ) ;
Check ( ! HasFinding ( r . summary , " localMax " ) , " naming: camelCase static constexpr local passes " ) ;
}
2026-10-02 19:57:28 +02:00
{
// 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
2026-10-02 20:42:21 +02:00
// from a running {/} count, so a wrapped signature or a local shadowing a
// member escaped it.
2026-10-02 19:57:28 +02:00
{
RuleRun r = RunRule ( " #include <string> \n "
" namespace Outer { \n "
" int wrapped_function( \n "
" int first, \n "
" int second) { return first + second; } \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 " ) ;
// 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 ;
}