lint rules lite
This commit is contained in:
parent
e906f797da
commit
42a27016e3
4 changed files with 213 additions and 111 deletions
40
README.md
40
README.md
|
|
@ -21,6 +21,8 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For a project that keeps its own formatting and naming, call `LintRules::AddLiteRules(cfg)` instead. It registers only the rules marked lite below, leaving layout, whitespace and naming alone; under `format` it still applies `format-concat` and `fixed-width-types`, and nothing else.
|
||||||
|
|
||||||
Then `crafter-build lint` reports, `crafter-build format` rewrites, and `crafter-build format --check` gates CI. Pin a revision for reproducible builds:
|
Then `crafter-build lint` reports, `crafter-build format` rewrites, and `crafter-build format --check` gates CI. Pin a revision for reproducible builds:
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
|
|
@ -31,22 +33,22 @@ Suppress a finding with `// lint-disable-next-line <rule>` or a whole file with
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
| Rule | Kind | What it wants |
|
| Rule | Kind | Lite | What it wants |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `spdx-header` | report | `// SPDX-License-Identifier:` on line 1, `// SPDX-FileCopyrightText:` on line 2, blank line 3 |
|
| `spdx-header` | report | ✓ | `// SPDX-License-Identifier:` on line 1, `// SPDX-FileCopyrightText:` on line 2, blank line 3 |
|
||||||
| `no-tabs` | report | spaces |
|
| `no-tabs` | report | | spaces |
|
||||||
| `naming` | report, AST | PascalCase types and functions; camelCase variables, including statics and constants |
|
| `naming` | report, AST | | PascalCase types and functions; camelCase variables, including statics and constants |
|
||||||
| `enum-class` | report | `enum class`, never a plain `enum` |
|
| `enum-class` | report | ✓ | `enum class`, never a plain `enum` |
|
||||||
| `no-iostream-print` | report | `std::println` instead of `std::cout`/`std::cerr` streaming |
|
| `no-iostream-print` | report | ✓ | `std::println` instead of `std::cout`/`std::cerr` streaming |
|
||||||
| `no-char-pointer` | report, AST | `std::string` / `std::string_view` over `char*` outside C interop |
|
| `no-char-pointer` | report, AST | ✓ | `std::string` / `std::string_view` over `char*` outside C interop |
|
||||||
| `const-local` | report, AST | `const` on a scalar local that is never written |
|
| `const-local` | report, AST | ✓ | `const` on a scalar local that is never written |
|
||||||
| `constexpr-constant` | report, AST | `constexpr` on a constant with a constant initialiser |
|
| `constexpr-constant` | report, AST | ✓ | `constexpr` on a constant with a constant initialiser |
|
||||||
| `format-concat` | transform | `std::format` instead of `+` with string literals |
|
| `format-concat` | transform | ✓ | `std::format` instead of `+` with string literals |
|
||||||
| `fixed-width-types` | transform, AST | `std::int32_t` and friends instead of `int`, `long`, … |
|
| `fixed-width-types` | transform, AST | ✓ | `std::int32_t` and friends instead of `int`, `long`, … |
|
||||||
| `single-declaration` | transform, AST | one declaration per statement |
|
| `single-declaration` | transform, AST | | one declaration per statement |
|
||||||
| `brace-style` | transform | K&R braces |
|
| `brace-style` | transform | | K&R braces |
|
||||||
| `if-single-line` | transform | `if (x) return;` on one line when it fits |
|
| `if-single-line` | transform | | `if (x) return;` on one line when it fits |
|
||||||
| `wrap-join` | transform | join wrapped statements that fit on one line |
|
| `wrap-join` | transform | | join wrapped statements that fit on one line |
|
||||||
| `paren-spacing` | transform | `if (` and `for (`, no space before a call's `(` |
|
| `paren-spacing` | transform | | `if (` and `for (`, no space before a call's `(` |
|
||||||
| `trim-trailing-ws` | transform | no trailing whitespace |
|
| `trim-trailing-ws` | transform | | no trailing whitespace |
|
||||||
| `final-newline` | transform | files end with a newline |
|
| `final-newline` | transform | | files end with a newline |
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,19 @@ import Crafter.Build;
|
||||||
// ...
|
// ...
|
||||||
// LintRules::AddRules(cfg);
|
// 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
|
// Deliberately not part of the crafter-build library, so the tool itself
|
||||||
// never nudges anyone toward one team's style.
|
// never nudges anyone toward one team's style.
|
||||||
//
|
//
|
||||||
// Report-only rules are registered first so their file:line numbers always
|
// 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
|
// 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
|
// All detection runs on ctx.CommentStripped() — comments and string/char
|
||||||
// literal bodies are blanked to spaces with newlines preserved, so byte
|
// 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);
|
if (base.starts_with("volatile ")) base.remove_prefix(9);
|
||||||
return base == "char";
|
return base == "char";
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export namespace Crafter::LintRules {
|
// ---------------- report-only rules ----------------
|
||||||
|
|
||||||
void AddRules(Crafter::Configuration& cfg) {
|
void AddCoreReports(Crafter::Configuration& cfg) {
|
||||||
using Crafter::LintContext;
|
using Crafter::LintContext;
|
||||||
|
|
||||||
// ---------------- report-only rules ----------------
|
|
||||||
|
|
||||||
// License header: SPDX identifier on line 1, copyright on line 2, blank
|
// License header: SPDX identifier on line 1, copyright on line 2, blank
|
||||||
// line 3.
|
// line 3.
|
||||||
cfg.AddLintRule("spdx-header", [](LintContext& ctx) {
|
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
|
// Scoped enums only. Kept on tokens rather than moved to the AST, even
|
||||||
// though LintDecl carries an exact isScopedEnum: an AST is one
|
// though LintDecl carries an exact isScopedEnum: an AST is one
|
||||||
// configuration's slice, so a plain enum inside a preprocessor branch that
|
// 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));
|
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
|
// String building via `+` with a literal operand → std::format. Plain
|
||||||
// `a += b` accumulation (builder pattern) stays legal. Single-line chains
|
// `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);
|
for (const Rep& r : reps) out.replace(r.pos, r.len, r.to);
|
||||||
ctx.SetContent(std::move(out));
|
ctx.SetContent(std::move(out));
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void AddStyleTransforms(Crafter::Configuration& cfg) {
|
||||||
|
using Crafter::LintContext;
|
||||||
|
|
||||||
// One declaration per statement. Auto-splits the simple initialized form
|
// One declaration per statement. Auto-splits the simple initialized form
|
||||||
// `Type a = x, b = y;`; anything with pointers/references, parens, or
|
// `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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
21
project.cpp
21
project.cpp
|
|
@ -7,9 +7,6 @@ namespace fs = std::filesystem;
|
||||||
using namespace Crafter;
|
using namespace Crafter;
|
||||||
|
|
||||||
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
|
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
|
||||||
std::vector<std::string> depArgs(args.begin(), args.end());
|
|
||||||
const bool useLocal = std::erase(depArgs, "--local") > 0;
|
|
||||||
|
|
||||||
Configuration cfg;
|
Configuration cfg;
|
||||||
cfg.path = "./";
|
cfg.path = "./";
|
||||||
cfg.name = "Crafter.LintRules";
|
cfg.name = "Crafter.LintRules";
|
||||||
|
|
@ -28,15 +25,15 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
|
|
||||||
// The rule test compiles the module as its own interface and links
|
// The rule test compiles the module as its own interface and links
|
||||||
// crafter-build to run the linter in-process: from git by default, or
|
// crafter-build to run the linter in-process: from git by default, or
|
||||||
// the sibling checkout with `crafter-build test --local`. Host only,
|
// the sibling checkout with `crafter-build test --local`. Only when this
|
||||||
// like the rules themselves, and only when this is the project being
|
// is the project being built: crafter-build itself loads this file as a
|
||||||
// built: crafter-build itself loads this file as a dependency, and the
|
// dependency, and the test's GitProject would then load crafter-build's
|
||||||
// test's GitProject would then load crafter-build's project file again
|
// project file again and recurse into the cycle guard. No target check:
|
||||||
// and recurse into the cycle guard.
|
// crafter-build only runs on Linux, and the hard-coded triple this used
|
||||||
if (IsTopLevelProject() && cfg.target == "x86_64-pc-linux-gnu") {
|
// to compare against silently dropped the test on any host that spells
|
||||||
Configuration* build = useLocal
|
// its triple differently (the Flatpak's x86_64-unknown-linux-gnu).
|
||||||
? LocalProject({ .projectFile = "../Crafter.Build/project.cpp", .args = depArgs })
|
if (IsTopLevelProject()) {
|
||||||
: GitProject({ .source = { .url = "https://forgejo.catcrafts.net/Catcrafts/Crafter.Build.git" }, .args = depArgs });
|
Configuration* build = GitProject({ .source = { .url = "https://forgejo.catcrafts.net/Catcrafts/Crafter.Build.git" }, .args = depArgs });
|
||||||
cfg.AddTest("HouseRules", ifaces).Dependencies({ build });
|
cfg.AddTest("HouseRules", ifaces).Dependencies({ build });
|
||||||
}
|
}
|
||||||
return cfg;
|
return cfg;
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,10 @@ using namespace Crafter;
|
||||||
|
|
||||||
// Validates the house-style ruleset — the transforms and reports every repo
|
// Validates the house-style ruleset — the transforms and reports every repo
|
||||||
// that imports Crafter.LintRules relies on. Each case writes a scratch file,
|
// 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
|
// runs ONE rule via the glob filter (or all of them, for the cases about the
|
||||||
// rewritten bytes.
|
// ruleset as a whole), and asserts the findings and/or rewritten bytes. The
|
||||||
|
// lite cases pin AddLiteRules to AddRules minus the style rules: a style rule
|
||||||
|
// leaking into lite, or a core rule falling out of either set, fails them.
|
||||||
namespace {
|
namespace {
|
||||||
std::int32_t Failures = 0;
|
std::int32_t Failures = 0;
|
||||||
|
|
||||||
|
|
@ -26,7 +28,7 @@ namespace {
|
||||||
std::string text; // file content after the run
|
std::string text; // file content after the run
|
||||||
};
|
};
|
||||||
|
|
||||||
RuleRun RunRule(std::string_view content, std::string_view rule, LintMode mode) {
|
RuleRun RunRule(std::string_view content, std::string_view rule, LintMode mode, void (*addRules)(Configuration&) = LintRules::AddRules) {
|
||||||
static std::int32_t Counter = 0;
|
static std::int32_t Counter = 0;
|
||||||
fs::path dir = fs::temp_directory_path() / "crafter-build-house-rules" / std::format("case-{}", Counter++);
|
fs::path dir = fs::temp_directory_path() / "crafter-build-house-rules" / std::format("case-{}", Counter++);
|
||||||
fs::remove_all(dir);
|
fs::remove_all(dir);
|
||||||
|
|
@ -43,7 +45,7 @@ namespace {
|
||||||
std::array<fs::path, 0> ifaces = {};
|
std::array<fs::path, 0> ifaces = {};
|
||||||
std::array<fs::path, 1> impls = { "f" };
|
std::array<fs::path, 1> impls = { "f" };
|
||||||
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
||||||
LintRules::AddRules(cfg);
|
addRules(cfg);
|
||||||
RunLintOptions opts;
|
RunLintOptions opts;
|
||||||
opts.mode = mode;
|
opts.mode = mode;
|
||||||
opts.globs = { std::string(rule) };
|
opts.globs = { std::string(rule) };
|
||||||
|
|
@ -534,6 +536,67 @@ int main() {
|
||||||
Check(!HasFinding(r.summary, "'already'"), "constexpr: an existing constexpr is not re-reported");
|
Check(!HasFinding(r.summary, "'already'"), "constexpr: an existing constexpr is not re-reported");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddLiteRules is AddRules minus the style rules — the rest unchanged and
|
||||||
|
// in the same order, so reports still run before every transform. Both
|
||||||
|
// lists are spelled out: the full one so a rule lost while regrouping
|
||||||
|
// fails here, the lite one so a style rule slipping into a core group
|
||||||
|
// does.
|
||||||
|
{
|
||||||
|
auto namesOf = [](void (*addRules)(Configuration&)) {
|
||||||
|
Configuration cfg;
|
||||||
|
addRules(cfg);
|
||||||
|
std::vector<std::string> names;
|
||||||
|
for (const LintRule& rule : cfg.lintRules) names.push_back(rule.name);
|
||||||
|
return names;
|
||||||
|
};
|
||||||
|
std::vector<std::string> lite = namesOf(LintRules::AddLiteRules);
|
||||||
|
std::vector<std::string> full = namesOf(LintRules::AddRules);
|
||||||
|
std::vector<std::string> expectedLite = {
|
||||||
|
"spdx-header", "enum-class", "no-iostream-print", "no-char-pointer", "const-local", "constexpr-constant",
|
||||||
|
"format-concat", "fixed-width-types",
|
||||||
|
};
|
||||||
|
std::vector<std::string> expectedFull = {
|
||||||
|
"spdx-header", "enum-class", "no-iostream-print", "no-char-pointer", "const-local", "constexpr-constant",
|
||||||
|
"no-tabs", "naming",
|
||||||
|
"format-concat", "fixed-width-types",
|
||||||
|
"single-declaration", "brace-style", "if-single-line", "wrap-join", "paren-spacing", "trim-trailing-ws", "final-newline",
|
||||||
|
};
|
||||||
|
Check(lite == expectedLite, std::format("lite: exactly the non-style rules, in order (got {})", lite));
|
||||||
|
Check(full == expectedFull, std::format("full: all 17 rules, reports before transforms (got {})", full));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The same file through both entry points. It breaks the style rules — an
|
||||||
|
// Allman brace, tabs, snake_case functions, a wrapped call padded inside
|
||||||
|
// its parens, trailing whitespace, no final newline — and two core ones:
|
||||||
|
// a plain enum, which lite reports, and an `int`, which lite rewrites.
|
||||||
|
// Lite must do exactly that and leave every other byte alone. The full run
|
||||||
|
// is the control: it shows this fixture really does trip the style rules,
|
||||||
|
// which is what makes lite leaving them alone mean something.
|
||||||
|
{
|
||||||
|
std::string_view source = "// SPDX-License-Identifier: MIT\n"
|
||||||
|
"// SPDX-FileCopyrightText: Copyright (C) 2026 Fixture\n"
|
||||||
|
"\n"
|
||||||
|
"#include <cstdint>\n"
|
||||||
|
"enum Color { Red };\n"
|
||||||
|
"void call_me(bool alpha, bool beta);\n"
|
||||||
|
"int snake_case_fn(bool alpha, bool beta)\n"
|
||||||
|
"{\n"
|
||||||
|
"\tcall_me( alpha,\n"
|
||||||
|
"\t beta ); \n"
|
||||||
|
"\treturn 0;\n"
|
||||||
|
"}";
|
||||||
|
std::string expected(source);
|
||||||
|
expected.replace(expected.find("int snake_case_fn"), 3, "std::int32_t");
|
||||||
|
RuleRun lite = RunRule(source, "*", LintMode::Apply, LintRules::AddLiteRules);
|
||||||
|
Check(lite.text == expected, "lite: rewrites the int and leaves every style violation byte for byte");
|
||||||
|
Check(lite.summary.errors == 0, "lite: every rule ran, the AST ones included");
|
||||||
|
Check(lite.summary.findings.size() == 1 && lite.summary.findings[0].rule == "enum-class", std::format("lite: the plain enum is the only finding ({} found)", lite.summary.findings.size()));
|
||||||
|
RuleRun full = RunRule(source, "*", LintMode::Apply);
|
||||||
|
Check(full.text != lite.text, "full: the style transforms rewrite the same file further");
|
||||||
|
Check(HasFinding(full.summary, "should be PascalCase"), "full: naming reports the same file");
|
||||||
|
Check(HasFinding(full.summary, "tab character"), "full: no-tabs reports the same file");
|
||||||
|
}
|
||||||
|
|
||||||
if (Failures > 0) {
|
if (Failures > 0) {
|
||||||
std::println(std::cerr, "{} assertions failed", Failures);
|
std::println(std::cerr, "{} assertions failed", Failures);
|
||||||
return 1;
|
return 1;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue