Skip to content

Commit 5b00277

Browse files
committed
[clang-tidy] Add readability-constant-operand-order check
This introduces a new clang-tidy readability check that enforces a consistent position for constant operands in binary comparison expressions. The check warns when a constant appears on the non-preferred side of an operator and can automatically fix simple cases (no side effects, not in macros). Configurable options: * PreferredConstantSide (Right | Left, default = Right) * BinaryOperators (comma-separated list, default = ==,!=,<,<=,>,>=) Example: if (0 == x) → if (x == 0) Includes tests, documentation, and integration with the Readability module.
1 parent 6500bb4 commit 5b00277

File tree

7 files changed

+302
-22
lines changed

7 files changed

+302
-22
lines changed
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
#include "ConstantOperandOrderCheck.h"
10+
#include "clang/AST/Decl.h"
11+
#include "clang/AST/Expr.h"
12+
#include "clang/AST/OperationKinds.h"
13+
#include "clang/ASTMatchers/ASTMatchFinder.h"
14+
#include "clang/Lex/Lexer.h"
15+
#include "llvm/ADT/SmallVector.h"
16+
#include "llvm/ADT/StringExtras.h"
17+
18+
using namespace clang::ast_matchers;
19+
20+
namespace clang::tidy::readability {
21+
22+
/// Out-of-line ctor so vtable is emitted.
23+
ConstantOperandOrderCheck::ConstantOperandOrderCheck(StringRef Name,
24+
ClangTidyContext *Context)
25+
: ClangTidyCheck(Name, Context) {
26+
// Read options (StringRef -> std::string).
27+
PreferredSide = Options.get(PreferredSideOption, "Right").str();
28+
29+
// Parse BinaryOperators option (comma-separated).
30+
std::string OpsCSV =
31+
Options.get(BinaryOperatorsOption, "==,!=,<,<=,>,>=").str();
32+
33+
llvm::SmallVector<llvm::StringRef, 8> Tokens;
34+
llvm::StringRef(OpsCSV).split(Tokens, ',');
35+
Operators.clear();
36+
for (auto Tok : Tokens) {
37+
llvm::StringRef Trim = Tok.trim();
38+
if (!Trim.empty())
39+
Operators.emplace_back(Trim.str());
40+
}
41+
}
42+
43+
ConstantOperandOrderCheck::~ConstantOperandOrderCheck() = default;
44+
45+
void ConstantOperandOrderCheck::storeOptions(
46+
ClangTidyOptions::OptionMap &Opts) {
47+
Options.store(Opts, PreferredSideOption, PreferredSide);
48+
Options.store(Opts, BinaryOperatorsOption,
49+
llvm::join(Operators.begin(), Operators.end(), ","));
50+
}
51+
52+
// ------------------------ helpers ------------------------
53+
54+
namespace {
55+
56+
static const Expr *strip(const Expr *E) {
57+
return E ? E->IgnoreParenImpCasts() : nullptr;
58+
}
59+
60+
static bool isSimpleConstantExpr(const Expr *E) {
61+
E = strip(E);
62+
if (!E)
63+
return false;
64+
65+
if (isa<IntegerLiteral>(E) || isa<FloatingLiteral>(E) ||
66+
isa<StringLiteral>(E) || isa<CharacterLiteral>(E) ||
67+
isa<CXXBoolLiteralExpr>(E) || isa<CXXNullPtrLiteralExpr>(E) ||
68+
isa<GNUNullExpr>(E))
69+
return true;
70+
71+
if (const auto *DRE = dyn_cast<DeclRefExpr>(E)) {
72+
if (isa<EnumConstantDecl>(DRE->getDecl()))
73+
return true;
74+
if (const auto *VD = dyn_cast<VarDecl>(DRE->getDecl()))
75+
return VD->isConstexpr() || VD->getType().isConstQualified();
76+
}
77+
78+
return false;
79+
}
80+
81+
static bool hasSideEffectsExpr(const Expr *E, ASTContext &Ctx) {
82+
E = strip(E);
83+
return E && E->HasSideEffects(Ctx);
84+
}
85+
86+
static std::string invertOperatorText(llvm::StringRef Op) {
87+
if (Op == "<")
88+
return ">";
89+
if (Op == ">")
90+
return "<";
91+
if (Op == "<=")
92+
return ">=";
93+
if (Op == ">=")
94+
return "<=";
95+
// symmetric: ==, !=
96+
return Op.str();
97+
}
98+
99+
} // namespace
100+
101+
// ------------------------ matchers ------------------------
102+
103+
void ConstantOperandOrderCheck::registerMatchers(MatchFinder *Finder) {
104+
if (Operators.empty())
105+
return;
106+
107+
for (const auto &Op : Operators) {
108+
Finder->addMatcher(binaryOperator(hasOperatorName(Op),
109+
hasLHS(expr().bind("lhs")),
110+
hasRHS(expr().bind("rhs")))
111+
.bind("binop"),
112+
this);
113+
}
114+
}
115+
116+
// ------------------------ check / fixit ------------------------
117+
118+
void ConstantOperandOrderCheck::check(const MatchFinder::MatchResult &Result) {
119+
const auto *Bin = Result.Nodes.getNodeAs<BinaryOperator>("binop");
120+
const auto *L = Result.Nodes.getNodeAs<Expr>("lhs");
121+
const auto *R = Result.Nodes.getNodeAs<Expr>("rhs");
122+
if (!Bin || !L || !R)
123+
return;
124+
125+
const ASTContext &Ctx = *Result.Context;
126+
SourceManager &SM = *Result.SourceManager;
127+
128+
const Expr *LCore = strip(L);
129+
const Expr *RCore = strip(R);
130+
const bool LIsConst = isSimpleConstantExpr(LCore);
131+
const bool RIsConst = isSimpleConstantExpr(RCore);
132+
133+
// Only when exactly one side is constant.
134+
if (LIsConst == RIsConst)
135+
return;
136+
137+
const bool PreferRight = (PreferredSide == "Right");
138+
139+
// If it's already on the preferred side -> nothing to do.
140+
if ((PreferRight && RIsConst && !LIsConst) ||
141+
(!PreferRight && LIsConst && !RIsConst))
142+
return;
143+
144+
// At this point: exactly one side is constant, and it's on the *wrong* side.
145+
// Emit diagnosis (tests expect a warning even when we won't provide a fix).
146+
auto D =
147+
diag(Bin->getOperatorLoc(), "constant operand should be on the %0 side")
148+
<< PreferredSide;
149+
150+
// Conservative: don't offer fix-its if swapping would move side-effects or if
151+
// we're inside a macro expansion.
152+
const bool LSE = L->HasSideEffects(Ctx);
153+
const bool RSE = R->HasSideEffects(Ctx);
154+
const bool AnyMacro = L->getBeginLoc().isMacroID() ||
155+
R->getBeginLoc().isMacroID() ||
156+
Bin->getOperatorLoc().isMacroID();
157+
if (LSE || RSE || AnyMacro)
158+
return; // warning-only: no FixIt attached.
159+
160+
// Get token ranges for the two operands.
161+
CharSourceRange LRange = CharSourceRange::getTokenRange(L->getSourceRange());
162+
CharSourceRange RRange = CharSourceRange::getTokenRange(R->getSourceRange());
163+
if (LRange.isInvalid() || RRange.isInvalid())
164+
return;
165+
166+
llvm::StringRef LText = Lexer::getSourceText(LRange, SM, Ctx.getLangOpts());
167+
llvm::StringRef RText = Lexer::getSourceText(RRange, SM, Ctx.getLangOpts());
168+
if (LText.empty() || RText.empty())
169+
return;
170+
171+
// Compute operator replacement (invert for asymmetric operators).
172+
llvm::StringRef OpName = Bin->getOpcodeStr();
173+
std::string NewOp = invertOperatorText(OpName);
174+
175+
// Apply operand swaps as two independent replacements (safer than replacing
176+
// the whole Bin range).
177+
// Replace left operand with right text:
178+
D << FixItHint::CreateReplacement(LRange, RText.str());
179+
// Replace right operand with left text:
180+
D << FixItHint::CreateReplacement(RRange, LText.str());
181+
182+
// If needed, replace the operator token too.
183+
if (NewOp != OpName.str()) {
184+
// Compute an operator token range robustly: operator start and end.
185+
SourceLocation OpStart = Bin->getOperatorLoc();
186+
SourceLocation OpEnd =
187+
Lexer::getLocForEndOfToken(OpStart, 0, SM, Ctx.getLangOpts());
188+
if (OpStart.isValid() && OpEnd.isValid()) {
189+
SourceRange OpRange(OpStart, OpEnd);
190+
CharSourceRange OpTok = CharSourceRange::getTokenRange(OpRange);
191+
if (!OpTok.isInvalid())
192+
D << FixItHint::CreateReplacement(OpTok, NewOp);
193+
}
194+
}
195+
}
196+
197+
} // namespace clang::tidy::readability
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_READABILITY_CONSTANTOPERANDORDERCHECK_H
10+
#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_READABILITY_CONSTANTOPERANDORDERCHECK_H
11+
12+
#include "../ClangTidyCheck.h"
13+
#include "clang/ASTMatchers/ASTMatchFinder.h"
14+
15+
#include <string>
16+
#include <vector>
17+
18+
namespace clang::tidy::readability {
19+
/// Finds binary expressions where the constant operand appears on the
20+
/// non-preferred side (configurable) and offers a fix-it that swaps operands
21+
/// (and inverts the operator for asymmetric operators like `<` / `>`).
22+
/// Options
23+
/// - BinaryOperators: comma-separated list of operators to check
24+
/// (default: "==,!=,<,<=,>,>=")
25+
/// - PreferredConstantSide: "Left" or "Right" (default: "Right")
26+
class ConstantOperandOrderCheck : public ClangTidyCheck {
27+
public:
28+
ConstantOperandOrderCheck(StringRef Name, ClangTidyContext *Context);
29+
~ConstantOperandOrderCheck() override;
30+
31+
// Persist options so they show up in config files.
32+
void storeOptions(ClangTidyOptions::OptionMap &Opts) override;
33+
34+
void registerMatchers(ast_matchers::MatchFinder *Finder) override;
35+
void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
36+
37+
bool isLanguageVersionSupported(const LangOptions &LangOpts) const override {
38+
// This check is primarily for C/C++, keep it limited to C++ for now.
39+
return LangOpts.CPlusPlus;
40+
}
41+
42+
private:
43+
// Option keys (used when storing & reading options)
44+
static constexpr llvm::StringLiteral BinaryOperatorsOption =
45+
"BinaryOperators";
46+
static constexpr llvm::StringLiteral PreferredSideOption =
47+
"PreferredConstantSide";
48+
49+
// Runtime values, populated from Options in the constructor (or storeOptions)
50+
std::string PreferredSide; // "Left" or "Right"
51+
std::vector<std::string> Operators; // list of operator names, e.g. "=="
52+
53+
// Implementation helpers live in the .cpp file.
54+
};
55+
56+
} // namespace clang::tidy::readability
57+
58+
#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_READABILITY_CONSTANTOPERANDORDERCHECK_H

clang-tools-extra/clang-tidy/readability/ReadabilityTidyModule.cpp

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,6 @@ class ReadabilityModule : public ClangTidyModule {
8989
"readability-const-return-type");
9090
CheckFactories.registerCheck<ConstantOperandOrderCheck>(
9191
"readability-constant-operand-order");
92-
CheckFactories.registerCheck<ConstantOperandOrderCheck>(
93-
"readability-constant-operand-order");
9492
CheckFactories.registerCheck<ContainerContainsCheck>(
9593
"readability-container-contains");
9694
CheckFactories.registerCheck<ContainerDataPointerCheck>(

clang-tools-extra/docs/ReleaseNotes.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,11 @@ New checks
226226
Finds virtual function overrides with different visibility than the function
227227
in the base class.
228228

229+
- New :doc:`readability-constant-operand-order
230+
<clang-tidy/checks/readability/constant-operand-order>` check.
231+
232+
FIXME: Write a short description.
233+
229234
- New :doc:`readability-redundant-parentheses
230235
<clang-tidy/checks/readability/redundant-parentheses>` check.
231236

clang-tools-extra/docs/clang-tidy/checks/list.rst

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,13 @@ Clang-Tidy Checks
178178
:doc:`bugprone-unused-return-value <bugprone/unused-return-value>`,
179179
:doc:`bugprone-use-after-move <bugprone/use-after-move>`,
180180
:doc:`bugprone-virtual-near-miss <bugprone/virtual-near-miss>`, "Yes"
181+
:doc:`cert-dcl58-cpp <cert/dcl58-cpp>`,
182+
:doc:`cert-env33-c <cert/env33-c>`,
181183
:doc:`cert-err33-c <cert/err33-c>`,
184+
:doc:`cert-err52-cpp <cert/err52-cpp>`,
182185
:doc:`cert-err60-cpp <cert/err60-cpp>`,
186+
:doc:`cert-flp30-c <cert/flp30-c>`,
187+
:doc:`cert-mem57-cpp <cert/mem57-cpp>`,
183188
:doc:`cert-msc50-cpp <cert/msc50-cpp>`,
184189
:doc:`cert-msc51-cpp <cert/msc51-cpp>`,
185190
:doc:`cert-oop58-cpp <cert/oop58-cpp>`,
@@ -373,8 +378,9 @@ Clang-Tidy Checks
373378
:doc:`readability-avoid-nested-conditional-operator <readability/avoid-nested-conditional-operator>`,
374379
:doc:`readability-avoid-return-with-void-value <readability/avoid-return-with-void-value>`, "Yes"
375380
:doc:`readability-avoid-unconditional-preprocessor-if <readability/avoid-unconditional-preprocessor-if>`,
376-
:doc:`readability-braces-around-statements <readability/braces-around-statements>`, "Yes"
381+
:doc:`readability-braces-around-statements <readability/braces-around-statements>`,
377382
:doc:`readability-const-return-type <readability/const-return-type>`, "Yes"
383+
:doc:`readability-constant-operand-order <readability/constant-operand-order>`, "Yes"
378384
:doc:`readability-container-contains <readability/container-contains>`, "Yes"
379385
:doc:`readability-container-data-pointer <readability/container-data-pointer>`, "Yes"
380386
:doc:`readability-container-size-empty <readability/container-size-empty>`, "Yes"
@@ -442,20 +448,15 @@ Check aliases
442448
:doc:`cert-dcl50-cpp <cert/dcl50-cpp>`, :doc:`modernize-avoid-variadic-functions <modernize/avoid-variadic-functions>`,
443449
:doc:`cert-dcl51-cpp <cert/dcl51-cpp>`, :doc:`bugprone-reserved-identifier <bugprone/reserved-identifier>`, "Yes"
444450
:doc:`cert-dcl54-cpp <cert/dcl54-cpp>`, :doc:`misc-new-delete-overloads <misc/new-delete-overloads>`,
445-
:doc:`cert-dcl58-cpp <cert/dcl58-cpp>`, :doc:`bugprone-std-namespace-modification <bugprone/std-namespace-modification>`,
446451
:doc:`cert-dcl59-cpp <cert/dcl59-cpp>`, :doc:`google-build-namespaces <google/build-namespaces>`,
447-
:doc:`cert-env33-c <cert/env33-c>`, :doc:`bugprone-command-processor <bugprone/command-processor>`,
448452
:doc:`cert-err09-cpp <cert/err09-cpp>`, :doc:`misc-throw-by-value-catch-by-reference <misc/throw-by-value-catch-by-reference>`,
449453
:doc:`cert-err34-c <cert/err34-c>`, :doc:`bugprone-unchecked-string-to-number-conversion <bugprone/unchecked-string-to-number-conversion>`,
450-
:doc:`cert-err52-cpp <cert/err52-cpp>`, :doc:`modernize-avoid-setjmp-longjmp <modernize/avoid-setjmp-longjmp>`,
451454
:doc:`cert-err58-cpp <cert/err58-cpp>`, :doc:`bugprone-throwing-static-initialization <bugprone/throwing-static-initialization>`,
452455
:doc:`cert-err61-cpp <cert/err61-cpp>`, :doc:`misc-throw-by-value-catch-by-reference <misc/throw-by-value-catch-by-reference>`,
453456
:doc:`cert-exp42-c <cert/exp42-c>`, :doc:`bugprone-suspicious-memory-comparison <bugprone/suspicious-memory-comparison>`,
454457
:doc:`cert-fio38-c <cert/fio38-c>`, :doc:`misc-non-copyable-objects <misc/non-copyable-objects>`,
455-
:doc:`cert-flp30-c <cert/flp30-c>`, :doc:`bugprone-float-loop-counter <bugprone/float-loop-counter>`,
456458
:doc:`cert-flp37-c <cert/flp37-c>`, :doc:`bugprone-suspicious-memory-comparison <bugprone/suspicious-memory-comparison>`,
457459
:doc:`cert-int09-c <cert/int09-c>`, :doc:`readability-enum-initial-value <readability/enum-initial-value>`, "Yes"
458-
:doc:`cert-mem57-cpp <cert/mem57-cpp>`, :doc:`bugprone-default-operator-new-on-overaligned-type <bugprone/default-operator-new-on-overaligned-type>`,
459460
:doc:`cert-msc24-c <cert/msc24-c>`, :doc:`bugprone-unsafe-functions <bugprone/unsafe-functions>`,
460461
:doc:`cert-msc30-c <cert/msc30-c>`, :doc:`cert-msc50-cpp <cert/msc50-cpp>`,
461462
:doc:`cert-msc32-c <cert/msc32-c>`, :doc:`cert-msc51-cpp <cert/msc51-cpp>`,
@@ -578,12 +579,12 @@ Check aliases
578579
:doc:`cppcoreguidelines-non-private-member-variables-in-classes <cppcoreguidelines/non-private-member-variables-in-classes>`, :doc:`misc-non-private-member-variables-in-classes <misc/non-private-member-variables-in-classes>`,
579580
:doc:`cppcoreguidelines-use-default-member-init <cppcoreguidelines/use-default-member-init>`, :doc:`modernize-use-default-member-init <modernize/use-default-member-init>`, "Yes"
580581
:doc:`fuchsia-header-anon-namespaces <fuchsia/header-anon-namespaces>`, :doc:`google-build-namespaces <google/build-namespaces>`,
581-
:doc:`google-readability-braces-around-statements <google/readability-braces-around-statements>`, :doc:`readability-braces-around-statements <readability/braces-around-statements>`, "Yes"
582+
:doc:`google-readability-braces-around-statements <google/readability-braces-around-statements>`, :doc:`readability-braces-around-statements <readability/braces-around-statements>`,
582583
:doc:`google-readability-function-size <google/readability-function-size>`, :doc:`readability-function-size <readability/function-size>`,
583584
:doc:`google-readability-namespace-comments <google/readability-namespace-comments>`, :doc:`llvm-namespace-comment <llvm/namespace-comment>`,
584585
:doc:`hicpp-avoid-c-arrays <hicpp/avoid-c-arrays>`, :doc:`modernize-avoid-c-arrays <modernize/avoid-c-arrays>`,
585586
:doc:`hicpp-avoid-goto <hicpp/avoid-goto>`, :doc:`cppcoreguidelines-avoid-goto <cppcoreguidelines/avoid-goto>`,
586-
:doc:`hicpp-braces-around-statements <hicpp/braces-around-statements>`, :doc:`readability-braces-around-statements <readability/braces-around-statements>`, "Yes"
587+
:doc:`hicpp-braces-around-statements <hicpp/braces-around-statements>`, :doc:`readability-braces-around-statements <readability/braces-around-statements>`,
587588
:doc:`hicpp-deprecated-headers <hicpp/deprecated-headers>`, :doc:`modernize-deprecated-headers <modernize/deprecated-headers>`, "Yes"
588589
:doc:`hicpp-explicit-conversions <hicpp/explicit-conversions>`, :doc:`google-explicit-constructor <google/explicit-constructor>`, "Yes"
589590
:doc:`hicpp-function-size <hicpp/function-size>`, :doc:`readability-function-size <readability/function-size>`,
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
.. title:: clang-tidy - readability-constant-operand-order
2+
3+
readability-constant-operand-order
4+
==================================
5+
6+
Warns when a constant appears on the non-preferred side of a supported binary
7+
operator and offers a fix-it to swap operands (and invert the operator for
8+
``<``, ``>``, ``<=``, ``>=``).
9+
10+
Examples
11+
--------
12+
13+
.. code-block:: c++
14+
15+
// Before
16+
if (nullptr == p) { /* ... */ }
17+
if (0 < x) { /* ... */ }
18+
19+
// After
20+
if (p == nullptr) { /* ... */ }
21+
if (x > 0) { /* ... */ }
22+
23+
Options
24+
-------
25+
26+
.. option:: PreferredConstantSide (string)
27+
28+
Either ``Left`` or ``Right``. Default: ``Right``.
29+
30+
.. option:: BinaryOperators (string)
31+
32+
Comma-separated list of operators to check. Default: ``==,!=,<,<=,>,>=``.

0 commit comments

Comments
 (0)