lint rules lite
This commit is contained in:
parent
e906f797da
commit
42a27016e3
4 changed files with 213 additions and 111 deletions
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue