https://github.com/hjanuschka updated https://github.com/llvm/llvm-project/pull/182065
>From 8b2507e9d06878e73df7be7361af4cf2147f46d0 Mon Sep 17 00:00:00 2001 From: Helmut Januschka <[email protected]> Date: Wed, 18 Feb 2026 18:03:47 +0100 Subject: [PATCH] [clang-tidy] Add modernize-use-algorithm check Add new clang-tidy check that finds range-based for loops implementing common algorithm patterns and suggests replacing them with standard library algorithm calls. Currently detects two patterns: - any_of: loop that returns true on a condition, falls through to return false - none_of: loop that returns false on a condition, falls through to return true For example: ```cpp // Before for (const auto &X : V) { if (X < 0) return true; } return false; // After return std::any_of(V.begin(), V.end(), [](const auto &X) { return X < 0; }); ``` The check automatically adds `#include <algorithm>` when needed. --- .../clang-tidy/modernize/CMakeLists.txt | 1 + .../modernize/ModernizeTidyModule.cpp | 2 + .../modernize/UseAlgorithmCheck.cpp | 229 ++++++++++++++++++ .../clang-tidy/modernize/UseAlgorithmCheck.h | 53 ++++ clang-tools-extra/docs/ReleaseNotes.rst | 6 + .../docs/clang-tidy/checks/list.rst | 1 + .../checks/modernize/use-algorithm.rst | 51 ++++ .../checkers/modernize/use-algorithm.cpp | 148 +++++++++++ 8 files changed, 491 insertions(+) create mode 100644 clang-tools-extra/clang-tidy/modernize/UseAlgorithmCheck.cpp create mode 100644 clang-tools-extra/clang-tidy/modernize/UseAlgorithmCheck.h create mode 100644 clang-tools-extra/docs/clang-tidy/checks/modernize/use-algorithm.rst create mode 100644 clang-tools-extra/test/clang-tidy/checkers/modernize/use-algorithm.cpp diff --git a/clang-tools-extra/clang-tidy/modernize/CMakeLists.txt b/clang-tools-extra/clang-tidy/modernize/CMakeLists.txt index cc4cc7a02b594..bfc797a66b450 100644 --- a/clang-tools-extra/clang-tidy/modernize/CMakeLists.txt +++ b/clang-tools-extra/clang-tidy/modernize/CMakeLists.txt @@ -31,6 +31,7 @@ add_clang_library(clangTidyModernizeModule STATIC ShrinkToFitCheck.cpp TypeTraitsCheck.cpp UnaryStaticAssertCheck.cpp + UseAlgorithmCheck.cpp UseAutoCheck.cpp UseBoolLiteralsCheck.cpp UseConstraintsCheck.cpp diff --git a/clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp b/clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp index fcb860d8c5298..f023271802c89 100644 --- a/clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp +++ b/clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp @@ -31,6 +31,7 @@ #include "ShrinkToFitCheck.h" #include "TypeTraitsCheck.h" #include "UnaryStaticAssertCheck.h" +#include "UseAlgorithmCheck.h" #include "UseAutoCheck.h" #include "UseBoolLiteralsCheck.h" #include "UseConstraintsCheck.h" @@ -116,6 +117,7 @@ class ModernizeModule : public ClangTidyModule { CheckFactories.registerCheck<TypeTraitsCheck>("modernize-type-traits"); CheckFactories.registerCheck<UnaryStaticAssertCheck>( "modernize-unary-static-assert"); + CheckFactories.registerCheck<UseAlgorithmCheck>("modernize-use-algorithm"); CheckFactories.registerCheck<UseAutoCheck>("modernize-use-auto"); CheckFactories.registerCheck<UseBoolLiteralsCheck>( "modernize-use-bool-literals"); diff --git a/clang-tools-extra/clang-tidy/modernize/UseAlgorithmCheck.cpp b/clang-tools-extra/clang-tidy/modernize/UseAlgorithmCheck.cpp new file mode 100644 index 0000000000000..2eb51c42e0c70 --- /dev/null +++ b/clang-tools-extra/clang-tidy/modernize/UseAlgorithmCheck.cpp @@ -0,0 +1,229 @@ +//===----------------------------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "UseAlgorithmCheck.h" +#include "clang/AST/ASTContext.h" +#include "clang/ASTMatchers/ASTMatchFinder.h" +#include "clang/Lex/Lexer.h" + +using namespace clang::ast_matchers; + +namespace clang::tidy::modernize { + +UseAlgorithmCheck::UseAlgorithmCheck(StringRef Name, ClangTidyContext *Context) + : ClangTidyCheck(Name, Context), + Inserter(Options.getLocalOrGlobal("IncludeStyle", + utils::IncludeSorter::IS_LLVM), + areDiagsSelfContained()) {} + +void UseAlgorithmCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) { + Options.store(Opts, "IncludeStyle", Inserter.getStyle()); +} + +void UseAlgorithmCheck::registerPPCallbacks(const SourceManager &SM, + Preprocessor *PP, + Preprocessor *ModuleExpanderPP) { + Inserter.registerPreprocessor(PP); +} + +/// Get the source text for an expression, without implicit casts/parens. +static StringRef getExprText(const Expr *E, const SourceManager &SM, + const LangOptions &LangOpts) { + return Lexer::getSourceText( + CharSourceRange::getTokenRange(E->getSourceRange()), SM, LangOpts); +} + +/// Check if a return statement returns a bool literal with the given value. +static bool returnsBoolLiteral(const ReturnStmt *Ret, bool Value) { + if (!Ret || !Ret->getRetValue()) + return false; + const auto *BoolLit = + dyn_cast<CXXBoolLiteralExpr>(Ret->getRetValue()->IgnoreImplicit()); + return BoolLit && BoolLit->getValue() == Value; +} + +/// Try to extract a simple condition from an if statement body that is just +/// "return true;" or "return false;". Returns the condition expression if the +/// if body is a single return of a bool literal. +static const Expr *getIfConditionForReturn(const IfStmt *If, + bool ExpectedReturnValue) { + if (!If || If->getElse() || If->getInit() || If->getConditionVariable()) + return nullptr; + + // Body must be "return true;" or "return false;". + const ReturnStmt *Ret = nullptr; + if (const auto *Body = dyn_cast<CompoundStmt>(If->getThen())) { + if (Body->size() != 1) + return nullptr; + Ret = dyn_cast<ReturnStmt>(Body->body_front()); + } else { + Ret = dyn_cast<ReturnStmt>(If->getThen()); + } + + if (!returnsBoolLiteral(Ret, ExpectedReturnValue)) + return nullptr; + + return If->getCond(); +} + +/// Find the statement immediately following \p Target in the same compound +/// statement. Returns nullptr if not found or not in a compound statement. +static const Stmt *getNextStmt(const Stmt *Target, ASTContext &Context) { + for (const auto &Parent : Context.getParents(*Target)) { + const auto *Compound = Parent.get<CompoundStmt>(); + if (!Compound) + continue; + bool FoundTarget = false; + for (const auto *Child : Compound->body()) { + if (FoundTarget) + return Child; + if (Child == Target) + FoundTarget = true; + } + } + return nullptr; +} + +void UseAlgorithmCheck::registerMatchers(MatchFinder *Finder) { + // Match range-based for loops whose body is a single if statement with + // a return of a bool literal and no else branch. + Finder->addMatcher( + cxxForRangeStmt( + hasBody(anyOf( + // Body is { if (cond) return true/false; } + compoundStmt(statementCountIs(1), has(ifStmt().bind("if"))), + // Body is: if (cond) return true/false; + ifStmt().bind("if"))), + hasRangeInit(expr().bind("container")), + hasLoopVariable(varDecl().bind("loopVar"))) + .bind("loop"), + this); +} + +void UseAlgorithmCheck::check(const MatchFinder::MatchResult &Result) { + const auto *Loop = Result.Nodes.getNodeAs<CXXForRangeStmt>("loop"); + const auto *If = Result.Nodes.getNodeAs<IfStmt>("if"); + const auto *Container = Result.Nodes.getNodeAs<Expr>("container"); + const auto *LoopVar = Result.Nodes.getNodeAs<VarDecl>("loopVar"); + + if (!Loop || !If || !Container || !LoopVar) + return; + + // Skip macros. + if (Loop->getBeginLoc().isMacroID()) + return; + + const auto &SM = *Result.SourceManager; + const auto &LangOpts = Result.Context->getLangOpts(); + + // Determine the pattern: + // any_of: if (cond) return true; ... return false; + // none_of: if (cond) return false; ... return true; + // (all_of is none_of with negated condition, but we emit none_of for + // clarity since the user's condition maps directly.) + struct PatternInfo { + StringRef AlgorithmName; + bool IfReturns; // What the if-body returns. + bool TrailingReturns; // What the trailing return must be. + bool NegateCondition; + }; + + static constexpr PatternInfo Patterns[] = { + {"std::any_of", true, false, false}, + {"std::none_of", false, true, false}, + }; + + const Expr *Condition = nullptr; + const PatternInfo *Matched = nullptr; + + for (const auto &P : Patterns) { + Condition = getIfConditionForReturn(If, P.IfReturns); + if (!Condition) + continue; + + // Check the trailing return statement. + const auto *NextStmt = getNextStmt(Loop, *Result.Context); + const auto *TrailingRet = dyn_cast_or_null<ReturnStmt>(NextStmt); + if (!returnsBoolLiteral(TrailingRet, P.TrailingReturns)) + continue; + + Matched = &P; + break; + } + + if (!Matched) + return; + + // Get source text for the components. + const StringRef ContainerText = getExprText(Container, SM, LangOpts); + const StringRef ConditionText = getExprText(Condition, SM, LangOpts); + const StringRef LoopVarName = LoopVar->getName(); + + if (ContainerText.empty() || ConditionText.empty() || LoopVarName.empty()) + return; + + // Extract the full declaration text (e.g. "const auto &x") from the + // for-range loop, then strip the variable name to get just the type part. + // This preserves qualifiers like const, volatile, and reference markers. + std::string VarTypeText; + { + // Get text from the type start to right before the variable name. + const SourceLocation TypeStart = LoopVar->getBeginLoc(); + const SourceLocation NameStart = LoopVar->getLocation(); + if (TypeStart.isValid() && NameStart.isValid()) { + VarTypeText = + Lexer::getSourceText( + CharSourceRange::getCharRange(TypeStart, NameStart), SM, LangOpts) + .rtrim() + .str(); + } + if (VarTypeText.empty()) + VarTypeText = LoopVar->getType().getAsString(); + } + + // Build replacement: return std::any_of(c.begin(), c.end(), + // [](const auto &x) { return cond; }); + std::string Replacement; + llvm::raw_string_ostream OS(Replacement); + // If the type ends with & or *, don't add a space before the name. + const StringRef Sep = (!VarTypeText.empty() && (VarTypeText.back() == '&' || + VarTypeText.back() == '*')) + ? "" + : " "; + OS << "return " << Matched->AlgorithmName << "(" << ContainerText + << ".begin(), " << ContainerText << ".end(), [](" << VarTypeText << Sep + << LoopVarName << ") { return " << ConditionText << "; });"; + + // Calculate the range to replace: from the for loop to the trailing return + // (including its semicolon). + const auto *TrailingRet = + dyn_cast<ReturnStmt>(getNextStmt(Loop, *Result.Context)); + if (!TrailingRet) + return; + + // Find the semicolon after the trailing return. + SourceLocation TrailingEnd = TrailingRet->getEndLoc(); + std::optional<Token> SemiTok = + Lexer::findNextToken(TrailingEnd, SM, LangOpts); + SourceLocation ReplaceEnd = + (SemiTok && SemiTok->is(tok::semi)) ? SemiTok->getEndLoc() : TrailingEnd; + + SourceRange ReplaceRange(Loop->getBeginLoc(), ReplaceEnd); + + auto Diag = diag(Loop->getForLoc(), "this loop can be replaced with '%0'") + << Matched->AlgorithmName + << FixItHint::CreateReplacement( + CharSourceRange::getCharRange(ReplaceRange), Replacement); + + // Add #include <algorithm> if needed. + if (auto IncludeFixit = Inserter.createIncludeInsertion( + SM.getFileID(Loop->getBeginLoc()), "<algorithm>")) + Diag << *IncludeFixit; +} + +} // namespace clang::tidy::modernize diff --git a/clang-tools-extra/clang-tidy/modernize/UseAlgorithmCheck.h b/clang-tools-extra/clang-tidy/modernize/UseAlgorithmCheck.h new file mode 100644 index 0000000000000..de833334e7368 --- /dev/null +++ b/clang-tools-extra/clang-tidy/modernize/UseAlgorithmCheck.h @@ -0,0 +1,53 @@ +//===----------------------------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MODERNIZE_USEALGORITHMCHECK_H +#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MODERNIZE_USEALGORITHMCHECK_H + +#include "../ClangTidyCheck.h" +#include "../utils/IncludeInserter.h" + +namespace clang::tidy::modernize { + +/// Finds range-based for loops that can be replaced with standard algorithms. +/// +/// Currently detects loops that implement any_of, all_of, or none_of patterns: +/// \code +/// for (const auto &X : Container) { +/// if (Condition) +/// return true; +/// } +/// return false; +/// \endcode +/// Can be replaced with: +/// \code +/// return std::any_of(Container.begin(), Container.end(), +/// [](const auto &X) { return Condition; }); +/// \endcode +/// +/// For the user-facing documentation see: +/// https://clang.llvm.org/extra/clang-tidy/checks/modernize/use-algorithm.html +class UseAlgorithmCheck : public ClangTidyCheck { +public: + UseAlgorithmCheck(StringRef Name, ClangTidyContext *Context); + bool isLanguageVersionSupported(const LangOptions &LangOpts) const override { + return LangOpts.CPlusPlus11; + } + void registerMatchers(ast_matchers::MatchFinder *Finder) override; + void registerPPCallbacks(const SourceManager &SM, Preprocessor *PP, + Preprocessor *ModuleExpanderPP) override; + void check(const ast_matchers::MatchFinder::MatchResult &Result) override; + void storeOptions(ClangTidyOptions::OptionMap &Opts) override; + +private: + utils::IncludeInserter Inserter; +}; + +} // namespace clang::tidy::modernize + +#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MODERNIZE_USEALGORITHMCHECK_H diff --git a/clang-tools-extra/docs/ReleaseNotes.rst b/clang-tools-extra/docs/ReleaseNotes.rst index 68bab88146241..3527f1f230b22 100644 --- a/clang-tools-extra/docs/ReleaseNotes.rst +++ b/clang-tools-extra/docs/ReleaseNotes.rst @@ -121,6 +121,12 @@ New checks ``llvm::to_vector(llvm::make_filter_range(...))`` that can be replaced with ``llvm::map_to_vector`` and ``llvm::filter_to_vector``. +- New :doc:`modernize-use-algorithm + <clang-tidy/checks/modernize/use-algorithm>` check. + + Finds range-based for loops that implement ``any_of`` or ``none_of`` + patterns and suggests replacing them with standard algorithm calls. + - New :doc:`modernize-use-string-view <clang-tidy/checks/modernize/use-string-view>` check. diff --git a/clang-tools-extra/docs/clang-tidy/checks/list.rst b/clang-tools-extra/docs/clang-tidy/checks/list.rst index c475870ed7b31..3335bd089cf90 100644 --- a/clang-tools-extra/docs/clang-tidy/checks/list.rst +++ b/clang-tools-extra/docs/clang-tidy/checks/list.rst @@ -311,6 +311,7 @@ Clang-Tidy Checks :doc:`modernize-shrink-to-fit <modernize/shrink-to-fit>`, "Yes" :doc:`modernize-type-traits <modernize/type-traits>`, "Yes" :doc:`modernize-unary-static-assert <modernize/unary-static-assert>`, "Yes" + :doc:`modernize-use-algorithm <modernize/use-algorithm>`, "Yes" :doc:`modernize-use-auto <modernize/use-auto>`, "Yes" :doc:`modernize-use-bool-literals <modernize/use-bool-literals>`, "Yes" :doc:`modernize-use-constraints <modernize/use-constraints>`, "Yes" diff --git a/clang-tools-extra/docs/clang-tidy/checks/modernize/use-algorithm.rst b/clang-tools-extra/docs/clang-tidy/checks/modernize/use-algorithm.rst new file mode 100644 index 0000000000000..7b0899b80b6b3 --- /dev/null +++ b/clang-tools-extra/docs/clang-tidy/checks/modernize/use-algorithm.rst @@ -0,0 +1,51 @@ +.. title:: clang-tidy - modernize-use-algorithm + +modernize-use-algorithm +======================= + +Finds range-based for loops that implement common algorithm patterns +and suggests replacing them with standard library algorithm calls. + +Currently detects two patterns: + +``std::any_of`` + A loop that returns ``true`` when a condition is met and falls + through to ``return false``. + +``std::none_of`` + A loop that returns ``false`` when a condition is met and falls + through to ``return true``. + +.. code-block:: c++ + + // Before + bool hasNegative(const std::vector<int> &V) { + for (const auto &X : V) { + if (X < 0) + return true; + } + return false; + } + + // After + bool hasNegative(const std::vector<int> &V) { + return std::any_of(V.begin(), V.end(), + [](const auto &X) { return X < 0; }); + } + +The check will not flag a loop if: + +- the loop body contains more than a single ``if`` statement, +- the ``if`` has an ``else`` branch or an init-statement, +- the ``if`` body is not a single ``return true``/``return false``, +- there is no matching trailing return statement immediately after + the loop. + +Options +------- + +.. option:: IncludeStyle + + A string specifying which include-style is used, ``llvm`` or + ``google``. Default is ``llvm``. This option can also be set + globally. diff --git a/clang-tools-extra/test/clang-tidy/checkers/modernize/use-algorithm.cpp b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-algorithm.cpp new file mode 100644 index 0000000000000..bbd497106c07b --- /dev/null +++ b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-algorithm.cpp @@ -0,0 +1,148 @@ +// RUN: %check_clang_tidy -std=c++14-or-later %s modernize-use-algorithm %t + +namespace std { +template <typename T> +class vector { +public: + using iterator = T *; + using const_iterator = const T *; + vector(); + iterator begin(); + iterator end(); + const_iterator begin() const; + const_iterator end() const; +}; +} // namespace std + +// Positive: any_of pattern with braces around the return. +bool anyOfBraces(const std::vector<int> &V) { + for (const auto &X : V) { + // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: this loop can be replaced with 'std::any_of' [modernize-use-algorithm] + if (X < 0) { + return true; + } + } + return false; + // CHECK-FIXES: return std::any_of(V.begin(), V.end(), [](const auto &X) { return X < 0; }); +} + +// Positive: any_of pattern without braces. +bool anyOfNoBraces(const std::vector<int> &V) { + for (const auto &X : V) + // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: this loop can be replaced with 'std::any_of' [modernize-use-algorithm] + if (X > 10) + return true; + return false; + // CHECK-FIXES: return std::any_of(V.begin(), V.end(), [](const auto &X) { return X > 10; }); +} + +// Positive: none_of pattern. +bool noneOfPattern(const std::vector<int> &V) { + for (const auto &X : V) { + // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: this loop can be replaced with 'std::none_of' [modernize-use-algorithm] + if (X == 0) + return false; + } + return true; + // CHECK-FIXES: return std::none_of(V.begin(), V.end(), [](const auto &X) { return X == 0; }); +} + +// Positive: any_of with compound condition. +bool anyOfCompound(const std::vector<int> &V) { + for (const auto &X : V) { + // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: this loop can be replaced with 'std::any_of' [modernize-use-algorithm] + if (X > 0 && X < 100) + return true; + } + return false; + // CHECK-FIXES: return std::any_of(V.begin(), V.end(), [](const auto &X) { return X > 0 && X < 100; }); +} + +// Positive: explicit type instead of auto. +bool explicitType(const std::vector<int> &V) { + for (const int &X : V) { + // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: this loop can be replaced with 'std::any_of' [modernize-use-algorithm] + if (X < 0) + return true; + } + return false; + // CHECK-FIXES: return std::any_of(V.begin(), V.end(), [](const int &X) { return X < 0; }); +} + +// Positive: by-value loop variable. +bool byValue(const std::vector<int> &V) { + for (int X : V) { + // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: this loop can be replaced with 'std::any_of' [modernize-use-algorithm] + if (X < 0) + return true; + } + return false; + // CHECK-FIXES: return std::any_of(V.begin(), V.end(), [](int X) { return X < 0; }); +} + +// Negative: loop body has more than just the if. +bool extraStatements(const std::vector<int> &V) { + for (const auto &X : V) { + int Y = X + 1; + if (Y < 0) + return true; + } + return false; +} + +// Negative: if has an else branch. +bool withElse(const std::vector<int> &V) { + for (const auto &X : V) { + if (X < 0) + return true; + else + continue; + } + return false; +} + +// Negative: trailing return does not match (both return true). +bool bothTrue(const std::vector<int> &V) { + for (const auto &X : V) { + if (X < 0) + return true; + } + return true; +} + +// Negative: trailing return does not match (both return false). +bool bothFalse(const std::vector<int> &V) { + for (const auto &X : V) { + if (X < 0) + return false; + } + return false; +} + +// Negative: no trailing return after the loop. +bool noTrailingReturn(const std::vector<int> &V) { + for (const auto &X : V) { + if (X < 0) + return true; + } + int Y = 0; + return Y == 0; +} + +// Negative: loop body returns a non-bool-literal. +int returnsNonBool(const std::vector<int> &V) { + for (const auto &X : V) { + if (X < 0) + return X; + } + return 0; +} + +// Negative: if has a condition variable. +bool withCondVar(const std::vector<int> &V) { + for (const auto &X : V) { + if (int Y = X; Y < 0) + return true; + } + return false; +} _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
