lint rules lite

This commit is contained in:
Jorijn van der Graaf 2026-10-31 02:21:19 +01:00
commit 42a27016e3
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
4 changed files with 213 additions and 111 deletions

View file

@ -13,12 +13,19 @@ import Crafter.Build;
// ...
// LintRules::AddRules(cfg);
//
// AddLiteRules(cfg) in its place registers the same rules minus the style
// ones (layout, whitespace and naming), for a project that keeps its own.
//
// Deliberately not part of the crafter-build library, so the tool itself
// never nudges anyone toward one team's style.
//
// Report-only rules are registered first so their file:line numbers always
// match the on-disk file; transforms run last because each one sees the
// previous transform's output, which can shift line numbers.
// previous transform's output, which can shift line numbers. Lite has to
// keep that order too, so the rules are grouped twice over: by kind, and
// into style and core (everything that is not style). AddRules registers
// all four groups and AddLiteRules the two core ones, so dropping the style
// rules never reorders what is left.
//
// All detection runs on ctx.CommentStripped() — comments and string/char
// literal bodies are blanked to spaces with newlines preserved, so byte
@ -101,15 +108,12 @@ bool IsCharPointer(std::string_view type) {
if (base.starts_with("volatile ")) base.remove_prefix(9);
return base == "char";
}
}
export namespace Crafter::LintRules {
// ---------------- report-only rules ----------------
void AddRules(Crafter::Configuration& cfg) {
void AddCoreReports(Crafter::Configuration& cfg) {
using Crafter::LintContext;
// ---------------- report-only rules ----------------
// License header: SPDX identifier on line 1, copyright on line 2, blank
// line 3.
cfg.AddLintRule("spdx-header", [](LintContext& ctx) {
@ -125,75 +129,6 @@ void AddRules(Crafter::Configuration& cfg) {
}
});
cfg.AddLintRule("no-tabs", [](LintContext& ctx) {
if (!IsCppFile(ctx)) return;
for (std::size_t n = 1; n <= ctx.lines.size(); ++n) {
if (ctx.Line(n).contains('\t')) ctx.Report(n, "tab character (use spaces)");
}
});
// Naming: types and functions PascalCase, everything that holds a value
// camelCase — locals, members, globals, statics and constants alike.
// Storage and constness used to pick the case, which meant `static int x`
// and `namespace { int x; }` were spelled differently for no reason a
// reader could see; now where a variable lives changes nothing about
// its name.
//
// Reads the AST. The previous version needed four std::regex, a hand-rolled
// {/} scope stack, a cumulative paren-depth counter to avoid mistaking a
// wrapped parameter list for a declaration, a keyword denylist, and a
// "function-shaped line" guess whose own comment conceded it was heuristic.
// All of it existed to answer two questions clang answers directly: what
// kind of declaration is this, and what encloses it.
cfg.AddAstLintRule("naming", [](LintContext& ctx) {
if (!IsCppFile(ctx)) return;
using Kind = Crafter::LintDeclKind;
std::span<const Crafter::LintDecl> decls = ctx.Decls();
for (const Crafter::LintDecl& decl : decls) {
if (decl.name.empty()) continue; // anonymous namespace, unnamed struct
// A declaration with C language linkage is named by the C library
// it mirrors — `extern "C" int setenv(...)` is not ours to rename.
if (decl.isExternC) continue;
switch (decl.kind) {
case Kind::Class:
case Kind::Struct:
case Kind::Union:
case Kind::Enum:
if (!IsPascalCase(decl.name)) {
ctx.Report(decl.line, std::format("type '{}' should be PascalCase", decl.name));
}
break;
case Kind::TypeAlias:
if (!IsPascalCase(decl.name)) {
ctx.Report(decl.line, std::format("type alias '{}' should be PascalCase", decl.name));
}
break;
case Kind::Function:
case Kind::Method:
// main is spelled by the language; operators by their
// symbol. Constructors and destructors take their type's
// name and are separate kinds, so they never arrive here.
if (decl.name == "main" || decl.name.starts_with("operator")) break;
if (!IsPascalCase(decl.name)) {
ctx.Report(decl.line, std::format("function '{}' should be PascalCase", decl.name));
}
break;
case Kind::Variable:
case Kind::Field:
// A static data member arrives as a Variable whose parent
// is the class; a Field is a per-instance member. Both
// are camelCase, so the label is the only difference.
if (!IsCamelCase(decl.name)) {
ctx.Report(decl.line, std::format("{} '{}' should be camelCase",
decl.kind == Kind::Field ? "member" : "variable", decl.name));
}
break;
default:
break;
}
}
});
// Scoped enums only. Kept on tokens rather than moved to the AST, even
// though LintDecl carries an exact isScopedEnum: an AST is one
// configuration's slice, so a plain enum inside a preprocessor branch that
@ -326,8 +261,85 @@ void AddRules(Crafter::Configuration& cfg) {
ctx.Report(decl.line, std::format("'{}' has a constant initialiser — declare it constexpr", decl.name));
}
});
}
// ---------------- transforms (auto-fixed by `crafter-build format`) ----------------
void AddStyleReports(Crafter::Configuration& cfg) {
using Crafter::LintContext;
cfg.AddLintRule("no-tabs", [](LintContext& ctx) {
if (!IsCppFile(ctx)) return;
for (std::size_t n = 1; n <= ctx.lines.size(); ++n) {
if (ctx.Line(n).contains('\t')) ctx.Report(n, "tab character (use spaces)");
}
});
// Naming: types and functions PascalCase, everything that holds a value
// camelCase — locals, members, globals, statics and constants alike.
// Storage and constness used to pick the case, which meant `static int x`
// and `namespace { int x; }` were spelled differently for no reason a
// reader could see; now where a variable lives changes nothing about
// its name.
//
// Reads the AST. The previous version needed four std::regex, a hand-rolled
// {/} scope stack, a cumulative paren-depth counter to avoid mistaking a
// wrapped parameter list for a declaration, a keyword denylist, and a
// "function-shaped line" guess whose own comment conceded it was heuristic.
// All of it existed to answer two questions clang answers directly: what
// kind of declaration is this, and what encloses it.
cfg.AddAstLintRule("naming", [](LintContext& ctx) {
if (!IsCppFile(ctx)) return;
using Kind = Crafter::LintDeclKind;
std::span<const Crafter::LintDecl> decls = ctx.Decls();
for (const Crafter::LintDecl& decl : decls) {
if (decl.name.empty()) continue; // anonymous namespace, unnamed struct
// A declaration with C language linkage is named by the C library
// it mirrors — `extern "C" int setenv(...)` is not ours to rename.
if (decl.isExternC) continue;
switch (decl.kind) {
case Kind::Class:
case Kind::Struct:
case Kind::Union:
case Kind::Enum:
if (!IsPascalCase(decl.name)) {
ctx.Report(decl.line, std::format("type '{}' should be PascalCase", decl.name));
}
break;
case Kind::TypeAlias:
if (!IsPascalCase(decl.name)) {
ctx.Report(decl.line, std::format("type alias '{}' should be PascalCase", decl.name));
}
break;
case Kind::Function:
case Kind::Method:
// main is spelled by the language; operators by their
// symbol. Constructors and destructors take their type's
// name and are separate kinds, so they never arrive here.
if (decl.name == "main" || decl.name.starts_with("operator")) break;
if (!IsPascalCase(decl.name)) {
ctx.Report(decl.line, std::format("function '{}' should be PascalCase", decl.name));
}
break;
case Kind::Variable:
case Kind::Field:
// A static data member arrives as a Variable whose parent
// is the class; a Field is a per-instance member. Both
// are camelCase, so the label is the only difference.
if (!IsCamelCase(decl.name)) {
ctx.Report(decl.line, std::format("{} '{}' should be camelCase",
decl.kind == Kind::Field ? "member" : "variable", decl.name));
}
break;
default:
break;
}
}
});
}
// ---------------- transforms (auto-fixed by `crafter-build format`) ----------------
void AddCoreTransforms(Crafter::Configuration& cfg) {
using Crafter::LintContext;
// String building via `+` with a literal operand → std::format. Plain
// `a += b` accumulation (builder pattern) stays legal. Single-line chains
@ -664,6 +676,10 @@ void AddRules(Crafter::Configuration& cfg) {
for (const Rep& r : reps) out.replace(r.pos, r.len, r.to);
ctx.SetContent(std::move(out));
});
}
void AddStyleTransforms(Crafter::Configuration& cfg) {
using Crafter::LintContext;
// One declaration per statement. Auto-splits the simple initialized form
// `Type a = x, b = y;`; anything with pointers/references, parens, or
@ -965,5 +981,29 @@ void AddRules(Crafter::Configuration& cfg) {
}
});
}
}
export namespace Crafter::LintRules {
// The full house style.
void AddRules(Crafter::Configuration& cfg) {
AddCoreReports(cfg);
AddStyleReports(cfg);
AddCoreTransforms(cfg);
AddStyleTransforms(cfg);
}
// The house rules without the house look, for a project that keeps its own
// formatter and naming conventions. Leaves out every rule that only decides
// how code is laid out or named — braces, wrapping, paren spacing,
// whitespace, tabs, one declaration per statement, naming — and keeps the
// ones that ask for different code: the SPDX header, enum class,
// std::string over char*, const, constexpr, std::println, std::format and
// fixed-width integers. `crafter-build format` therefore still rewrites
// string concatenation and builtin integer types, and nothing else.
void AddLiteRules(Crafter::Configuration& cfg) {
AddCoreReports(cfg);
AddCoreTransforms(cfg);
}
}