Skip to content

Commit c13e271

Browse files
authored
Add clang-tidy check to suggest replacement of conditional statement with std::min/std::max (#77816)
This pull request fixes #64914 where author suggests adding a readability check to propose the replacement of conditional statements with std::min/std::max for improved code readability. Additionally, reference is made to PyLint's similar checks: [consider-using-min-builtin](https://pylint.pycqa.org/en/latest/user_guide/messages/refactor/consider-using-min-builtin.html) and [consider-using-max-builtin](https://pylint.pycqa.org/en/latest/user_guide/messages/refactor/consider-using-max-builtin.html)
1 parent 5ce2f73 commit c13e271

File tree

8 files changed

+524
-0
lines changed

8 files changed

+524
-0
lines changed

clang-tools-extra/clang-tidy/readability/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ add_clang_library(clangTidyReadabilityModule
5555
UniqueptrDeleteReleaseCheck.cpp
5656
UppercaseLiteralSuffixCheck.cpp
5757
UseAnyOfAllOfCheck.cpp
58+
UseStdMinMaxCheck.cpp
5859

5960
LINK_LIBS
6061
clangTidy

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

+3
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
#include "UniqueptrDeleteReleaseCheck.h"
5959
#include "UppercaseLiteralSuffixCheck.h"
6060
#include "UseAnyOfAllOfCheck.h"
61+
#include "UseStdMinMaxCheck.h"
6162

6263
namespace clang::tidy {
6364
namespace readability {
@@ -163,6 +164,8 @@ class ReadabilityModule : public ClangTidyModule {
163164
"readability-uppercase-literal-suffix");
164165
CheckFactories.registerCheck<UseAnyOfAllOfCheck>(
165166
"readability-use-anyofallof");
167+
CheckFactories.registerCheck<UseStdMinMaxCheck>(
168+
"readability-use-std-min-max");
166169
}
167170
};
168171

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
//===--- UseStdMinMaxCheck.cpp - clang-tidy -------------------------------===//
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 "UseStdMinMaxCheck.h"
10+
#include "../utils/ASTUtils.h"
11+
#include "clang/AST/ASTContext.h"
12+
#include "clang/ASTMatchers/ASTMatchFinder.h"
13+
#include "clang/Lex/Preprocessor.h"
14+
15+
using namespace clang::ast_matchers;
16+
17+
namespace clang::tidy::readability {
18+
19+
namespace {
20+
21+
// Ignore if statements that are inside macros.
22+
AST_MATCHER(IfStmt, isIfInMacro) {
23+
return Node.getIfLoc().isMacroID() || Node.getEndLoc().isMacroID();
24+
}
25+
26+
} // namespace
27+
28+
static const llvm::StringRef AlgorithmHeader("<algorithm>");
29+
30+
static bool minCondition(const BinaryOperator::Opcode Op, const Expr *CondLhs,
31+
const Expr *CondRhs, const Expr *AssignLhs,
32+
const Expr *AssignRhs, const ASTContext &Context) {
33+
if ((Op == BO_LT || Op == BO_LE) &&
34+
(tidy::utils::areStatementsIdentical(CondLhs, AssignRhs, Context) &&
35+
tidy::utils::areStatementsIdentical(CondRhs, AssignLhs, Context)))
36+
return true;
37+
38+
if ((Op == BO_GT || Op == BO_GE) &&
39+
(tidy::utils::areStatementsIdentical(CondLhs, AssignLhs, Context) &&
40+
tidy::utils::areStatementsIdentical(CondRhs, AssignRhs, Context)))
41+
return true;
42+
43+
return false;
44+
}
45+
46+
static bool maxCondition(const BinaryOperator::Opcode Op, const Expr *CondLhs,
47+
const Expr *CondRhs, const Expr *AssignLhs,
48+
const Expr *AssignRhs, const ASTContext &Context) {
49+
if ((Op == BO_LT || Op == BO_LE) &&
50+
(tidy::utils::areStatementsIdentical(CondLhs, AssignLhs, Context) &&
51+
tidy::utils::areStatementsIdentical(CondRhs, AssignRhs, Context)))
52+
return true;
53+
54+
if ((Op == BO_GT || Op == BO_GE) &&
55+
(tidy::utils::areStatementsIdentical(CondLhs, AssignRhs, Context) &&
56+
tidy::utils::areStatementsIdentical(CondRhs, AssignLhs, Context)))
57+
return true;
58+
59+
return false;
60+
}
61+
62+
QualType getNonTemplateAlias(QualType QT) {
63+
while (true) {
64+
// cast to a TypedefType
65+
if (const TypedefType *TT = dyn_cast<TypedefType>(QT)) {
66+
// check if the typedef is a template and if it is dependent
67+
if (!TT->getDecl()->getDescribedTemplate() &&
68+
!TT->getDecl()->getDeclContext()->isDependentContext())
69+
return QT;
70+
QT = TT->getDecl()->getUnderlyingType();
71+
}
72+
// cast to elaborated type
73+
else if (const ElaboratedType *ET = dyn_cast<ElaboratedType>(QT)) {
74+
QT = ET->getNamedType();
75+
} else {
76+
break;
77+
}
78+
}
79+
return QT;
80+
}
81+
82+
static std::string createReplacement(const Expr *CondLhs, const Expr *CondRhs,
83+
const Expr *AssignLhs,
84+
const SourceManager &Source,
85+
const LangOptions &LO,
86+
StringRef FunctionName,
87+
const BinaryOperator *BO) {
88+
const llvm::StringRef CondLhsStr = Lexer::getSourceText(
89+
Source.getExpansionRange(CondLhs->getSourceRange()), Source, LO);
90+
const llvm::StringRef CondRhsStr = Lexer::getSourceText(
91+
Source.getExpansionRange(CondRhs->getSourceRange()), Source, LO);
92+
const llvm::StringRef AssignLhsStr = Lexer::getSourceText(
93+
Source.getExpansionRange(AssignLhs->getSourceRange()), Source, LO);
94+
95+
clang::QualType GlobalImplicitCastType;
96+
clang::QualType LhsType = CondLhs->getType()
97+
.getCanonicalType()
98+
.getNonReferenceType()
99+
.getUnqualifiedType();
100+
clang::QualType RhsType = CondRhs->getType()
101+
.getCanonicalType()
102+
.getNonReferenceType()
103+
.getUnqualifiedType();
104+
if (LhsType != RhsType) {
105+
GlobalImplicitCastType = getNonTemplateAlias(BO->getLHS()->getType());
106+
}
107+
108+
return (AssignLhsStr + " = " + FunctionName +
109+
(!GlobalImplicitCastType.isNull()
110+
? "<" + GlobalImplicitCastType.getAsString() + ">("
111+
: "(") +
112+
CondLhsStr + ", " + CondRhsStr + ");")
113+
.str();
114+
}
115+
116+
UseStdMinMaxCheck::UseStdMinMaxCheck(StringRef Name, ClangTidyContext *Context)
117+
: ClangTidyCheck(Name, Context),
118+
IncludeInserter(Options.getLocalOrGlobal("IncludeStyle",
119+
utils::IncludeSorter::IS_LLVM),
120+
areDiagsSelfContained()) {}
121+
122+
void UseStdMinMaxCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
123+
Options.store(Opts, "IncludeStyle", IncludeInserter.getStyle());
124+
}
125+
126+
void UseStdMinMaxCheck::registerMatchers(MatchFinder *Finder) {
127+
auto AssignOperator =
128+
binaryOperator(hasOperatorName("="),
129+
hasLHS(expr(unless(isTypeDependent())).bind("AssignLhs")),
130+
hasRHS(expr(unless(isTypeDependent())).bind("AssignRhs")));
131+
auto BinaryOperator =
132+
binaryOperator(hasAnyOperatorName("<", ">", "<=", ">="),
133+
hasLHS(expr(unless(isTypeDependent())).bind("CondLhs")),
134+
hasRHS(expr(unless(isTypeDependent())).bind("CondRhs")))
135+
.bind("binaryOp");
136+
Finder->addMatcher(
137+
ifStmt(stmt().bind("if"), unless(isIfInMacro()),
138+
unless(hasElse(stmt())), // Ensure `if` has no `else`
139+
hasCondition(BinaryOperator),
140+
hasThen(
141+
anyOf(stmt(AssignOperator),
142+
compoundStmt(statementCountIs(1), has(AssignOperator)))),
143+
hasParent(stmt(unless(ifStmt(hasElse(
144+
equalsBoundNode("if"))))))), // Ensure `if` has no `else if`
145+
this);
146+
}
147+
148+
void UseStdMinMaxCheck::registerPPCallbacks(const SourceManager &SM,
149+
Preprocessor *PP,
150+
Preprocessor *ModuleExpanderPP) {
151+
IncludeInserter.registerPreprocessor(PP);
152+
}
153+
154+
void UseStdMinMaxCheck::check(const MatchFinder::MatchResult &Result) {
155+
const auto *If = Result.Nodes.getNodeAs<IfStmt>("if");
156+
const clang::LangOptions &LO = Result.Context->getLangOpts();
157+
const auto *CondLhs = Result.Nodes.getNodeAs<Expr>("CondLhs");
158+
const auto *CondRhs = Result.Nodes.getNodeAs<Expr>("CondRhs");
159+
const auto *AssignLhs = Result.Nodes.getNodeAs<Expr>("AssignLhs");
160+
const auto *AssignRhs = Result.Nodes.getNodeAs<Expr>("AssignRhs");
161+
const auto *BinaryOp = Result.Nodes.getNodeAs<BinaryOperator>("binaryOp");
162+
const clang::BinaryOperatorKind BinaryOpcode = BinaryOp->getOpcode();
163+
const SourceLocation IfLocation = If->getIfLoc();
164+
const SourceLocation ThenLocation = If->getEndLoc();
165+
166+
auto ReplaceAndDiagnose = [&](const llvm::StringRef FunctionName) {
167+
const SourceManager &Source = *Result.SourceManager;
168+
diag(IfLocation, "use `%0` instead of `%1`")
169+
<< FunctionName << BinaryOp->getOpcodeStr()
170+
<< FixItHint::CreateReplacement(
171+
SourceRange(IfLocation, Lexer::getLocForEndOfToken(
172+
ThenLocation, 0, Source, LO)),
173+
createReplacement(CondLhs, CondRhs, AssignLhs, Source, LO,
174+
FunctionName, BinaryOp))
175+
<< IncludeInserter.createIncludeInsertion(
176+
Source.getFileID(If->getBeginLoc()), AlgorithmHeader);
177+
};
178+
179+
if (minCondition(BinaryOpcode, CondLhs, CondRhs, AssignLhs, AssignRhs,
180+
(*Result.Context))) {
181+
ReplaceAndDiagnose("std::min");
182+
} else if (maxCondition(BinaryOpcode, CondLhs, CondRhs, AssignLhs, AssignRhs,
183+
(*Result.Context))) {
184+
ReplaceAndDiagnose("std::max");
185+
}
186+
}
187+
188+
} // namespace clang::tidy::readability
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//===--- UseStdMinMaxCheck.h - clang-tidy -----------------------*- C++ -*-===//
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_USESTDMINMAXCHECK_H
10+
#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_READABILITY_USESTDMINMAXCHECK_H
11+
12+
#include "../ClangTidyCheck.h"
13+
#include "../utils/IncludeInserter.h"
14+
15+
namespace clang::tidy::readability {
16+
17+
/// Replaces certain conditional statements with equivalent calls to
18+
/// ``std::min`` or ``std::max``.
19+
/// For the user-facing documentation see:
20+
/// http://clang.llvm.org/extra/clang-tidy/checks/readability/UseStdMinMax.html
21+
class UseStdMinMaxCheck : public ClangTidyCheck {
22+
public:
23+
UseStdMinMaxCheck(StringRef Name, ClangTidyContext *Context);
24+
bool isLanguageVersionSupported(const LangOptions &LangOpts) const override {
25+
return LangOpts.CPlusPlus;
26+
}
27+
void registerPPCallbacks(const SourceManager &SM, Preprocessor *PP,
28+
Preprocessor *ModuleExpanderPP) override;
29+
void storeOptions(ClangTidyOptions::OptionMap &Opts) override;
30+
void registerMatchers(ast_matchers::MatchFinder *Finder) override;
31+
void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
32+
std::optional<TraversalKind> getCheckTraversalKind() const override {
33+
return TK_IgnoreUnlessSpelledInSource;
34+
}
35+
36+
private:
37+
utils::IncludeInserter IncludeInserter;
38+
};
39+
40+
} // namespace clang::tidy::readability
41+
42+
#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_READABILITY_USESTDMINMAXCHECK_H

clang-tools-extra/docs/ReleaseNotes.rst

+6
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,12 @@ Improvements to clang-tidy
100100
New checks
101101
^^^^^^^^^^
102102

103+
- New :doc:`readability-use-std-min-max
104+
<clang-tidy/checks/readability/use-std-min-max>` check.
105+
106+
Replaces certain conditional statements with equivalent calls to
107+
``std::min`` or ``std::max``.
108+
103109
New check aliases
104110
^^^^^^^^^^^^^^^^^
105111

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

+1
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,7 @@ Clang-Tidy Checks
385385
:doc:`readability-uniqueptr-delete-release <readability/uniqueptr-delete-release>`, "Yes"
386386
:doc:`readability-uppercase-literal-suffix <readability/uppercase-literal-suffix>`, "Yes"
387387
:doc:`readability-use-anyofallof <readability/use-anyofallof>`,
388+
:doc:`readability-use-std-min-max <readability/use-std-min-max>`, "Yes"
388389
:doc:`zircon-temporary-objects <zircon/temporary-objects>`,
389390

390391

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
.. title:: clang-tidy - readability-use-std-min-max
2+
3+
readability-use-std-min-max
4+
===========================
5+
6+
Replaces certain conditional statements with equivalent calls to
7+
``std::min`` or ``std::max``.
8+
Note: This may impact performance in critical code due to potential
9+
additional stores compared to the original if statement.
10+
11+
Before:
12+
13+
.. code-block:: c++
14+
15+
void foo() {
16+
int a = 2, b = 3;
17+
if (a < b)
18+
a = b;
19+
}
20+
21+
22+
After:
23+
24+
.. code-block:: c++
25+
26+
void foo() {
27+
int a = 2, b = 3;
28+
a = std::max(a, b);
29+
}

0 commit comments

Comments
 (0)