feat(lint): const-local and constexpr-constant rules
Two rules the AST makes possible, plus the mutation analysis behind them.
const-local reports a local that is never written. It is restricted to SCALARS
— integers, bools, enums, floating types — and that restriction is what makes
the answer exact rather than a guess: a scalar has no member functions, so the
only ways to write one are assignment, ++/--, having its address taken, or
binding to a non-const reference. All four are now tracked in the walk:
- assignment and compound assignment visit their LEFT operand in a write
context, the right one normally;
- ++/-- and & write their operand;
- a call argument is checked against the callee's parameter type, so passing
to `const int&` or by value is a read while `int&` is a write;
- initialising a non-const reference writes what it binds to.
For a class type a non-const method call could mutate it, and deciding that is
the whole-program analysis clang-tidy does, so those are simply out of scope
rather than guessed at.
constexpr-constant promotes a const constant whose initialiser is made only of
literals and operators, so `const int A = 1 << 4;` qualifies and
`const int B = Compute();` does not.
On this repository const-local found 103 candidates, which was too many to be
useful, and the reason was informative: most were range-for bindings and
pointer locals. `for (T* const x : …)` and `T* const p` are not spellings
anybody writes, and the useful constness for a pointer is on the pointee, which
this rule cannot advise on. Excluding both leaves 36, all plain bool or enum
locals worth fixing — isWasm, isPe, exists, writes, isC and so on. Those 36 are
fixed in this commit; the compiler verified every one.
Both rules are report-only. The analysis is exact, but adding const is a
judgement about intent as much as mechanics, and a wrong suggestion should cost
a glance rather than a build. const-local also deliberately does not become a
transform: inserting `const` before a shared type would apply it to every
declarator in a multi-declarator statement, including any that IS written.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5ca2b3e1df
commit
651720e494
10 changed files with 370 additions and 39 deletions
74
lint-rules.h
74
lint-rules.h
|
|
@ -274,6 +274,80 @@ inline void AddProjectLintRules(Crafter::Configuration& cfg) {
|
|||
// groups, literals) are REWRITTEN automatically; anything the operand
|
||||
// scanner can't prove safe — raw-string lines, ternaries, mixed
|
||||
// operators, multi-line expressions — is reported for a human instead.
|
||||
// A local that is never written should say so. Restricted to SCALARS —
|
||||
// integers, bools, enums, pointers, floating types — which is what makes
|
||||
// the answer exact rather than a guess: a scalar has no member functions,
|
||||
// so the only ways to write one are assignment, ++/--, having its address
|
||||
// taken, or binding to a non-const reference, and the AST layer tracks all
|
||||
// four. For a class type, a non-const method call could mutate it and
|
||||
// deciding that needs the whole-program analysis clang-tidy does.
|
||||
//
|
||||
// Report-only. Adding const is a judgement about intent as much as
|
||||
// mechanics, and a wrong suggestion should cost a glance, not a build.
|
||||
cfg.AddAstLintRule("const-local", [](LintContext& ctx) {
|
||||
if (!IsCppFile(ctx)) return;
|
||||
std::span<const Crafter::LintDecl> decls = ctx.Decls();
|
||||
for (const Crafter::LintDecl& decl : decls) {
|
||||
if (decl.kind != Crafter::LintDeclKind::Variable) continue;
|
||||
if (decl.parent == Crafter::LintNoParent) continue;
|
||||
// Locals only: a namespace-scope or static variable may be written
|
||||
// from a translation unit this parse cannot see.
|
||||
Crafter::LintDeclKind enclosing = decls[decl.parent].kind;
|
||||
bool isLocal = enclosing == Crafter::LintDeclKind::Function || enclosing == Crafter::LintDeclKind::Method
|
||||
|| enclosing == Crafter::LintDeclKind::Constructor || enclosing == Crafter::LintDeclKind::Destructor;
|
||||
if (!isLocal || decl.isStatic) continue;
|
||||
if (decl.isConst || decl.isConstexpr) continue;
|
||||
if (!decl.isScalar || decl.isMutated) continue;
|
||||
if (decl.name.empty()) continue;
|
||||
// A range-for binding is not what a reader pictures as an
|
||||
// assignable variable, and `for (T* const x : …)` is not a spelling
|
||||
// anybody writes.
|
||||
if (decl.isLoopVariable) continue;
|
||||
// Likewise `T* const p` — the useful constness for a pointer local
|
||||
// is almost always on the pointee, which this rule cannot advise
|
||||
// on. Restricting to value types keeps the advice actionable.
|
||||
if (decl.type.contains('*')) continue;
|
||||
ctx.Report(decl.line, std::format("'{}' is never modified — declare it const", decl.name));
|
||||
}
|
||||
});
|
||||
|
||||
// A constant whose value is already a constant expression can be constexpr,
|
||||
// which puts it in the type system rather than leaving it to the optimiser.
|
||||
// Only fires when every token of the initialiser is a literal or an
|
||||
// operator, so `const int A = 1 << 4;` qualifies and
|
||||
// `const int B = Compute();` does not.
|
||||
cfg.AddAstLintRule("constexpr-constant", [](LintContext& ctx) {
|
||||
if (!IsCppFile(ctx)) return;
|
||||
std::span<const Crafter::LintToken> tokens = ctx.Tokens();
|
||||
for (const Crafter::LintDecl& decl : ctx.Decls()) {
|
||||
if (decl.kind != Crafter::LintDeclKind::Variable && decl.kind != Crafter::LintDeclKind::Field) continue;
|
||||
if (!decl.isConst || decl.isConstexpr || !decl.isScalar) continue;
|
||||
// A pointer's value is an address, which is rarely a constant
|
||||
// expression and never an interesting one to promote.
|
||||
if (decl.type.contains('*')) continue;
|
||||
|
||||
// Walk the declaration's own tokens, starting after the '='.
|
||||
bool sawAssign = false;
|
||||
bool allConstant = true;
|
||||
bool sawLiteral = false;
|
||||
for (const Crafter::LintToken& token : tokens) {
|
||||
if (token.offset < decl.nameOffset) continue;
|
||||
if (token.offset >= decl.end) break;
|
||||
std::string_view text = ctx.TokenText(token);
|
||||
if (!sawAssign) {
|
||||
if (text == "=") sawAssign = true;
|
||||
continue;
|
||||
}
|
||||
if (token.kind == Crafter::LintTokenKind::Literal) { sawLiteral = true; continue; }
|
||||
if (token.kind == Crafter::LintTokenKind::Punctuation) continue;
|
||||
allConstant = false; // an identifier or keyword: not a literal fold
|
||||
break;
|
||||
}
|
||||
if (!sawAssign || !sawLiteral || !allConstant) continue;
|
||||
ctx.Report(decl.line, std::format("'{}' is a literal constant — declare it constexpr", decl.name));
|
||||
}
|
||||
});
|
||||
|
||||
cfg.AddLintRule("format-concat", [](LintContext& ctx) {
|
||||
if (!IsCppFile(ctx)) return;
|
||||
const std::string& code = ctx.CommentStripped();
|
||||
|
|
|
|||
Loading…
Reference in a new issue