diff --git a/clang-tools-extra/clang-tidy/modernize/CMakeLists.txt b/clang-tools-extra/clang-tidy/modernize/CMakeLists.txt index 2c5c44db587fe..d8500fd2595b0 100644 --- a/clang-tools-extra/clang-tidy/modernize/CMakeLists.txt +++ b/clang-tools-extra/clang-tidy/modernize/CMakeLists.txt @@ -47,6 +47,7 @@ add_clang_library(clangTidyModernizeModule STATIC UseRangesCheck.cpp UseScopedLockCheck.cpp UseStartsEndsWithCheck.cpp + UseStaticLambdaCheck.cpp UseStdBitCheck.cpp UseStdFormatCheck.cpp UseStdNumbersCheck.cpp diff --git a/clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp b/clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp index cc13da7535bcb..7471608b2172d 100644 --- a/clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp +++ b/clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp @@ -47,6 +47,7 @@ #include "UseRangesCheck.h" #include "UseScopedLockCheck.h" #include "UseStartsEndsWithCheck.h" +#include "UseStaticLambdaCheck.h" #include "UseStdBitCheck.h" #include "UseStdFormatCheck.h" #include "UseStdNumbersCheck.h" @@ -97,6 +98,8 @@ class ModernizeModule : public ClangTidyModule { "modernize-use-scoped-lock"); CheckFactories.registerCheck( "modernize-use-starts-ends-with"); + CheckFactories.registerCheck( + "modernize-use-static-lambda"); CheckFactories.registerCheck("modernize-use-std-bit"); CheckFactories.registerCheck("modernize-use-std-format"); CheckFactories.registerCheck( diff --git a/clang-tools-extra/clang-tidy/modernize/UseStaticLambdaCheck.cpp b/clang-tools-extra/clang-tidy/modernize/UseStaticLambdaCheck.cpp new file mode 100644 index 0000000000000..a93d98e436a30 --- /dev/null +++ b/clang-tools-extra/clang-tidy/modernize/UseStaticLambdaCheck.cpp @@ -0,0 +1,121 @@ +//===----------------------------------------------------------------------===// +// +// 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 "UseStaticLambdaCheck.h" +#include "clang/AST/ExprCXX.h" +#include "clang/AST/LambdaCapture.h" +#include "clang/AST/TypeLoc.h" +#include "clang/ASTMatchers/ASTMatchFinder.h" +#include "clang/ASTMatchers/ASTMatchers.h" +#include "clang/Basic/SourceLocation.h" +#include "clang/Lex/Lexer.h" + +using namespace clang::ast_matchers; + +namespace clang::tidy::modernize { + +namespace { +AST_MATCHER(LambdaExpr, hasNoCaptureDefault) { + return Node.getCaptureDefault() == LCD_None; +} +AST_MATCHER(LambdaExpr, callOperatorIsStatic) { + return Node.getCallOperator()->isStatic(); +} +AST_MATCHER(LambdaExpr, callOperatorIsConst) { + return Node.getCallOperator()->isConst(); +} +} // namespace + +void UseStaticLambdaCheck::registerMatchers(MatchFinder *Finder) { + Finder->addMatcher( + lambdaExpr(hasNoCaptureDefault(), unless(hasAnyCapture(lambdaCapture())), + unless(callOperatorIsStatic()), callOperatorIsConst()) + .bind("lambda"), + this); +} + +void UseStaticLambdaCheck::check(const MatchFinder::MatchResult &Result) { + const auto *Lambda = Result.Nodes.getNodeAs("lambda"); + assert(Lambda && "lambda should be bound by the matcher"); + + const SourceLocation LambdaLoc = Lambda->getBeginLoc(); + if (LambdaLoc.isInvalid() || LambdaLoc.isMacroID()) + return; + + const CXXMethodDecl *CallOp = Lambda->getCallOperator(); + const SourceManager &SM = *Result.SourceManager; + const LangOptions &LangOpts = getLangOpts(); + + SourceLocation InsertLoc; + StringRef InsertStr; + + if (Lambda->hasExplicitParameters()) { + const TypeSourceInfo *TSI = CallOp->getTypeSourceInfo(); + if (!TSI) + return; + auto FTL = TSI->getTypeLoc().IgnoreParens().getAs(); + if (!FTL) + return; + const SourceLocation RParenLoc = FTL.getRParenLoc(); + if (RParenLoc.isInvalid()) + return; + InsertLoc = Lexer::getLocForEndOfToken(RParenLoc, 0, SM, LangOpts); + InsertStr = " static"; + } else { + SourceLocation ScanStart = Lambda->getIntroducerRange().getEnd(); + if (Lambda->isGenericLambda()) { + if (const TemplateParameterList *TPL = Lambda->getTemplateParameterList()) { + // Skip past the template requires-clause if present, otherwise past '>'. + if (const Expr *Req = TPL->getRequiresClause()) + ScanStart = Req->getEndLoc(); + else + ScanStart = TPL->getRAngleLoc(); + } + } + ScanStart = Lexer::getLocForEndOfToken(ScanStart, 0, SM, LangOpts); + if (ScanStart.isInvalid()) + return; + + // Scan forward, tracking '[' / ']' depth to skip [[attr]] blocks. + SourceLocation CurLoc = ScanStart; + int Depth = 0; + while (true) { + Token RawTok; + if (Lexer::getRawToken(CurLoc, RawTok, SM, LangOpts, + /*IgnoreWhiteSpace=*/true)) + break; + if (RawTok.is(tok::l_square)) { + ++Depth; + } else if (RawTok.is(tok::r_square)) { + if (--Depth < 0) + break; // malformed source + if (Depth == 0) { + // Finished consuming one [[...]] block; keep scanning. + CurLoc = Lexer::getLocForEndOfToken(RawTok.getLocation(), 0, SM, + LangOpts); + continue; + } + } else if (Depth == 0) { + // Outside any attribute block: this is the insertion point. + InsertLoc = RawTok.getLocation(); + break; + } + CurLoc = + Lexer::getLocForEndOfToken(RawTok.getLocation(), 0, SM, LangOpts); + } + InsertStr = "static "; + } + + if (InsertLoc.isInvalid()) + return; + + diag(LambdaLoc, "lambda with empty capture list can be marked 'static'") + << FixItHint::CreateInsertion(InsertLoc, InsertStr); +} + +} // namespace clang::tidy::modernize diff --git a/clang-tools-extra/clang-tidy/modernize/UseStaticLambdaCheck.h b/clang-tools-extra/clang-tidy/modernize/UseStaticLambdaCheck.h new file mode 100644 index 0000000000000..65c723394c967 --- /dev/null +++ b/clang-tools-extra/clang-tidy/modernize/UseStaticLambdaCheck.h @@ -0,0 +1,40 @@ +//===----------------------------------------------------------------------===// +// +// 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_USESTATICLAMBDACHECK_H +#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MODERNIZE_USESTATICLAMBDACHECK_H + +#include "../ClangTidyCheck.h" + +namespace clang::tidy::modernize { + +/// Finds non-capturing lambdas that can be marked ``static`` (C++23). +/// +/// Marking a non-capturing lambda ``static`` turns ``operator()`` into a +/// static member function, making it clear the lambda has no dependency on +/// any closure state. +/// +/// For the user-facing documentation see: +/// https://clang.llvm.org/extra/clang-tidy/checks/modernize/use-static-lambda.html +class UseStaticLambdaCheck : public ClangTidyCheck { +public: + UseStaticLambdaCheck(StringRef Name, ClangTidyContext *Context) + : ClangTidyCheck(Name, Context) {} + bool isLanguageVersionSupported(const LangOptions &LangOpts) const override { + return LangOpts.CPlusPlus23; + } + std::optional getCheckTraversalKind() const override { + return TK_IgnoreUnlessSpelledInSource; + } + void registerMatchers(ast_matchers::MatchFinder *Finder) override; + void check(const ast_matchers::MatchFinder::MatchResult &Result) override; +}; + +} // namespace clang::tidy::modernize + +#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MODERNIZE_USESTATICLAMBDACHECK_H diff --git a/clang-tools-extra/docs/ReleaseNotes.rst b/clang-tools-extra/docs/ReleaseNotes.rst index acf000908acb2..0c3370c0ba3cb 100644 --- a/clang-tools-extra/docs/ReleaseNotes.rst +++ b/clang-tools-extra/docs/ReleaseNotes.rst @@ -145,6 +145,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-static-lambda + ` check. + + Finds lambdas with an empty capture list (``[]``) that are not marked + ``static`` and suggests adding the specifier (C++23). + - New :doc:`modernize-use-std-bit ` check. diff --git a/clang-tools-extra/docs/clang-tidy/checks/list.rst b/clang-tools-extra/docs/clang-tidy/checks/list.rst index 053ce6f0779d9..ac4d9b7f6a033 100644 --- a/clang-tools-extra/docs/clang-tidy/checks/list.rst +++ b/clang-tools-extra/docs/clang-tidy/checks/list.rst @@ -326,6 +326,7 @@ Clang-Tidy Checks :doc:`modernize-use-ranges `, "Yes" :doc:`modernize-use-scoped-lock `, "Yes" :doc:`modernize-use-starts-ends-with `, "Yes" + :doc:`modernize-use-static-lambda `, "Yes" :doc:`modernize-use-std-bit `, "Yes" :doc:`modernize-use-std-format `, "Yes" :doc:`modernize-use-std-numbers `, "Yes" diff --git a/clang-tools-extra/docs/clang-tidy/checks/modernize/use-static-lambda.rst b/clang-tools-extra/docs/clang-tidy/checks/modernize/use-static-lambda.rst new file mode 100644 index 0000000000000..bdbfff6a2754f --- /dev/null +++ b/clang-tools-extra/docs/clang-tidy/checks/modernize/use-static-lambda.rst @@ -0,0 +1,29 @@ +.. title:: clang-tidy - modernize-use-static-lambda + +modernize-use-static-lambda +=========================== + +Finds lambdas with an empty capture list (``[]``) that are not already marked +``static`` and suggests adding the ``static`` specifier (introduced in C++23). + +Marking a non-capturing lambda ``static`` turns ``operator()`` into a static +member function, making it clear that the lambda has no dependency on any +closure state. + +Example +------- + +.. code-block:: c++ + + auto square = [](int x) { return x * x; }; + + auto answer = [] { return 42; }; + +transforms to: + +.. code-block:: c++ + + auto square = [](int x) static { return x * x; }; + + auto answer = [] static { return 42; }; + diff --git a/clang-tools-extra/test/clang-tidy/checkers/modernize/use-static-lambda-cxx20.cpp b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-static-lambda-cxx20.cpp new file mode 100644 index 0000000000000..9b72d7644abe0 --- /dev/null +++ b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-static-lambda-cxx20.cpp @@ -0,0 +1,10 @@ +// RUN: %check_clang_tidy -std=c++20 %s modernize-use-static-lambda %t + +// The check is gated on C++23; no warnings should be emitted in C++20. + +void noWarnings() { + auto f1 = [](int x) { return x * x; }; + auto f2 = [] { return 42; }; + (void)f1; + (void)f2; +} diff --git a/clang-tools-extra/test/clang-tidy/checkers/modernize/use-static-lambda.cpp b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-static-lambda.cpp new file mode 100644 index 0000000000000..16663406e06c7 --- /dev/null +++ b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-static-lambda.cpp @@ -0,0 +1,191 @@ +// RUN: %check_clang_tidy -std=c++23-or-later %s modernize-use-static-lambda %t + +void basicCases() { + auto f1 = [](int x) { return x * x; }; + // CHECK-MESSAGES: :[[@LINE-1]]:13: warning: lambda with empty capture list can be marked 'static' [modernize-use-static-lambda] + // CHECK-FIXES: auto f1 = [](int x) static { return x * x; }; + + auto f2 = [] { return 42; }; + // CHECK-MESSAGES: :[[@LINE-1]]:13: warning: lambda with empty capture list can be marked 'static' [modernize-use-static-lambda] + // CHECK-FIXES: auto f2 = [] static { return 42; }; + + auto f3 = [](int x) -> int { return x; }; + // CHECK-MESSAGES: :[[@LINE-1]]:13: warning: lambda with empty capture list can be marked 'static' [modernize-use-static-lambda] + // CHECK-FIXES: auto f3 = [](int x) static -> int { return x; }; + + auto f4 = []() noexcept { return 1; }; + // CHECK-MESSAGES: :[[@LINE-1]]:13: warning: lambda with empty capture list can be marked 'static' [modernize-use-static-lambda] + // CHECK-FIXES: auto f4 = []() static noexcept { return 1; }; +} + +void alreadyStatic() { + auto f = [](int x) static { return x * x; }; + (void)f; +} + +void capturingLambdas() { + int x = 5; + auto f1 = [x]() { return x; }; + (void)f1; + + auto f2 = [&x]() { return x; }; + (void)f2; + + // Capture-default counts as a capture even if nothing is actually captured. + auto f3 = [=]() { return 0; }; + (void)f3; + + auto f4 = [&]() { return 0; }; + (void)f4; +} + +void mutableLambda() { + auto g = []() mutable { return 0; }; + (void)g; +} + +#define MAKE_LAMBDA [](int x) { return x; } + +void macroLambda() { + auto f = MAKE_LAMBDA; + (void)f; +} + +void constexprLambda() { + auto f = []() constexpr { return 42; }; + // CHECK-MESSAGES: :[[@LINE-1]]:12: warning: lambda with empty capture list can be marked 'static' [modernize-use-static-lambda] + // CHECK-FIXES: auto f = []() static constexpr { return 42; }; +} + +void constevalLambda() { + auto L = []() consteval { return 1; }; + // CHECK-MESSAGES: :[[@LINE-1]]:12: warning: lambda with empty capture list can be marked 'static' [modernize-use-static-lambda] + // CHECK-FIXES: auto L = []() static consteval { return 1; }; + (void)L; +} + +void combinedSpecifiers() { + auto L = []() noexcept -> int { return 1; }; + // CHECK-MESSAGES: :[[@LINE-1]]:12: warning: lambda with empty capture list can be marked 'static' [modernize-use-static-lambda] + // CHECK-FIXES: auto L = []() static noexcept -> int { return 1; }; + (void)L; +} + +void genericLambda() { + auto L = [](auto X) { return X; }; + // CHECK-MESSAGES: :[[@LINE-1]]:12: warning: lambda with empty capture list can be marked 'static' [modernize-use-static-lambda] + // CHECK-FIXES: auto L = [](auto X) static { return X; }; + (void)L; +} + +void explicitTemplateParameterList() { + auto L = [](T X) { return X; }; + // CHECK-MESSAGES: :[[@LINE-1]]:12: warning: lambda with empty capture list can be marked 'static' [modernize-use-static-lambda] + // CHECK-FIXES: auto L = [](T X) static { return X; }; + (void)L; +} + +void withRequiresClause() { + // trailing requires-clause after the parameter list + auto L1 = [](T X) requires (sizeof(T) > 0) { return X; }; + // CHECK-MESSAGES: :[[@LINE-1]]:13: warning: lambda with empty capture list can be marked 'static' [modernize-use-static-lambda] + // CHECK-FIXES: auto L1 = [](T X) static requires (sizeof(T) > 0) { return X; }; + (void)L1; + + // requires-clause on the template params, before '()' + auto L2 = [] requires (sizeof(T) > 0) (T X) { return X; }; + // CHECK-MESSAGES: :[[@LINE-1]]:13: warning: lambda with empty capture list can be marked 'static' [modernize-use-static-lambda] + // CHECK-FIXES: auto L2 = [] requires (sizeof(T) > 0) (T X) static { return X; }; + (void)L2; +} + +void withFrontAttribute() { + // [[nodiscard]] sits between '<>' and '()' + auto L = [] [[nodiscard]] (T X) { return X; }; + // CHECK-MESSAGES: :[[@LINE-1]]:12: warning: lambda with empty capture list can be marked 'static' [modernize-use-static-lambda] + // CHECK-FIXES: auto L = [] {{\[\[nodiscard\]\]}} (T X) static { return X; }; + (void)L; +} + +void withoutParameterList() { + // no '()' — template params only + auto L = [] { return T{}; }; + // CHECK-MESSAGES: :[[@LINE-1]]:12: warning: lambda with empty capture list can be marked 'static' [modernize-use-static-lambda] + // CHECK-FIXES: auto L = [] static { return T{}; }; + (void)L; +} + +// No '()' — every lambda-specifier kind that may appear without explicit params. +void noParamSpecifiers() { + // noexcept without '()' + auto L1 = [] noexcept { return 1; }; + // CHECK-MESSAGES: :[[@LINE-1]]:13: warning: lambda with empty capture list can be marked 'static' [modernize-use-static-lambda] + // CHECK-FIXES: auto L1 = [] static noexcept { return 1; }; + (void)L1; + + // trailing return type without '()' + auto L2 = [] -> int { return 1; }; + // CHECK-MESSAGES: :[[@LINE-1]]:13: warning: lambda with empty capture list can be marked 'static' [modernize-use-static-lambda] + // CHECK-FIXES: auto L2 = [] static -> int { return 1; }; + (void)L2; + + // constexpr without '()' + auto L3 = [] constexpr { return 1; }; + // CHECK-MESSAGES: :[[@LINE-1]]:13: warning: lambda with empty capture list can be marked 'static' [modernize-use-static-lambda] + // CHECK-FIXES: auto L3 = [] static constexpr { return 1; }; + (void)L3; + + // noexcept + trailing return type without '()' + auto L4 = [] noexcept -> int { return 1; }; + // CHECK-MESSAGES: :[[@LINE-1]]:13: warning: lambda with empty capture list can be marked 'static' [modernize-use-static-lambda] + // CHECK-FIXES: auto L4 = [] static noexcept -> int { return 1; }; + (void)L4; +} + +void combinations() { + // no '()' with a requires-clause + auto L1 = [] requires (sizeof(T) > 0) { return T{}; }; + // CHECK-MESSAGES: :[[@LINE-1]]:13: warning: lambda with empty capture list can be marked 'static' [modernize-use-static-lambda] + // CHECK-FIXES: auto L1 = [] requires (sizeof(T) > 0) static { return T{}; }; + (void)L1; + + // attribute, params, and requires-clause all at once + auto L2 = [] [[nodiscard]] (T X) requires (sizeof(T) > 0) { return X; }; + // CHECK-MESSAGES: :[[@LINE-1]]:13: warning: lambda with empty capture list can be marked 'static' [modernize-use-static-lambda] + // CHECK-FIXES: auto L2 = [] {{\[\[nodiscard\]\]}} (T X) static requires (sizeof(T) > 0) { return X; }; + (void)L2; + + // attribute with no '()' + auto L3 = [] [[nodiscard]] { return T{}; }; + // CHECK-MESSAGES: :[[@LINE-1]]:13: warning: lambda with empty capture list can be marked 'static' [modernize-use-static-lambda] + // CHECK-FIXES: auto L3 = [] {{\[\[nodiscard\]\]}} static { return T{}; }; + (void)L3; + + // no '()' with trailing return type and template params + auto L4 = [] -> T { return T{}; }; + // CHECK-MESSAGES: :[[@LINE-1]]:13: warning: lambda with empty capture list can be marked 'static' [modernize-use-static-lambda] + // CHECK-FIXES: auto L4 = [] static -> T { return T{}; }; + (void)L4; +} + +// The check must not produce duplicate diagnostics for template instantiations. +template +void templated(T Value) { + auto L = [](T X) { return X; }; + // CHECK-MESSAGES: :[[@LINE-1]]:12: warning: lambda with empty capture list can be marked 'static' [modernize-use-static-lambda] + // CHECK-FIXES: auto L = [](T X) static { return X; }; + (void)L; +} + +void instantiateTemplates() { + templated(1); + templated(2); +} + +// A lambda that is already static via a macro expansion must not be diagnosed. +#define STATIC static + +void staticFromMacro() { + auto L = []() STATIC { return 1; }; + (void)L; +}