2026-07-23 01:24:42 +02:00
// 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 \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: 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 " ) ;
}
fix(lint): make the reflow guards token-accurate instead of textual
The transforms guard themselves against comments and raw strings before
joining or rewriting a line, because pulling text up past a `//` buries it
and reflowing a multi-line literal changes the string. Those guards were
substring probes over the raw line, so they answered the wrong question:
Line(n).contains("//") fires on // inside a string literal
Line(n).contains("R\"") fires on the characters R" inside a literal, and
MISSES a raw string opened on an earlier line
Both misfire on this repo's own sources. "MARKER" ends in R", and any string
mentioning a lint-disable directive contains //. Two wrapped call sites in
tests/Lint were being left unjoined for exactly these reasons; they join now,
and the results are in this commit.
Replaced by LineHasComment() (added with the token layer) and a new
LineHasMultiLineToken(), which reports whether any token actually covering
that line spans a line boundary — a raw string or a block comment. Backed by
a per-line bitmap derived from the token cache and invalidated with it.
The guards themselves stay: joining across a real comment or a real
multi-line literal is still unsafe, and there are tests for both. What
changes is that they now fire on comments and literals rather than on the
characters that spell them.
format-concat gets narrower as a result. It used to refuse any line
containing R" and ask for a manual fix; now only a literal that genuinely
spans lines does that, because a single-line raw string reduces to R"…" in
the stripped view, fails the plain-literal test, and travels through as an
argument with its spelling intact.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 03:08:06 +02:00
// 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 " ) ;
}
2026-07-23 01:24:42 +02:00
if ( Failures > 0 ) {
std : : println ( std : : cerr , " {} assertions failed " , Failures ) ;
return 1 ;
}
return 0 ;
}