[clang-tools-extra] [clang-tidy] Add modernize-use-static-lambda check (PR #196893)
Aayush Shrivastava via cfe-commits
cfe-commits at lists.llvm.org
Mon May 11 02:16:43 PDT 2026
https://github.com/iamaayushrivastava updated https://github.com/llvm/llvm-project/pull/196893
>From 764b08cef88222650bc71f6978de72bff3f95c8a Mon Sep 17 00:00:00 2001
From: Aayush Shrivastava <iamaayushrivastava at gmail.com>
Date: Mon, 11 May 2026 12:51:48 +0530
Subject: [PATCH 1/4] [clang-tidy] Add modernize-use-static-lambda check
---
.../clang-tidy/modernize/CMakeLists.txt | 1 +
.../modernize/ModernizeTidyModule.cpp | 3 +
.../modernize/UseStaticLambdaCheck.cpp | 77 ++++++++++++++++++
.../modernize/UseStaticLambdaCheck.h | 38 +++++++++
.../docs/clang-tidy/checks/list.rst | 1 +
.../checks/modernize/use-static-lambda.rst | 39 +++++++++
.../modernize/use-static-lambda-cxx20.cpp | 10 +++
.../checkers/modernize/use-static-lambda.cpp | 79 +++++++++++++++++++
8 files changed, 248 insertions(+)
create mode 100644 clang-tools-extra/clang-tidy/modernize/UseStaticLambdaCheck.cpp
create mode 100644 clang-tools-extra/clang-tidy/modernize/UseStaticLambdaCheck.h
create mode 100644 clang-tools-extra/docs/clang-tidy/checks/modernize/use-static-lambda.rst
create mode 100644 clang-tools-extra/test/clang-tidy/checkers/modernize/use-static-lambda-cxx20.cpp
create mode 100644 clang-tools-extra/test/clang-tidy/checkers/modernize/use-static-lambda.cpp
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..dfb675f850d24 100644
--- a/clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp
+++ b/clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp
@@ -51,6 +51,7 @@
#include "UseStdFormatCheck.h"
#include "UseStdNumbersCheck.h"
#include "UseStdPrintCheck.h"
+#include "UseStaticLambdaCheck.h"
#include "UseStringViewCheck.h"
#include "UseStructuredBindingCheck.h"
#include "UseTrailingReturnTypeCheck.h"
@@ -97,6 +98,8 @@ class ModernizeModule : public ClangTidyModule {
"modernize-use-scoped-lock");
CheckFactories.registerCheck<UseStartsEndsWithCheck>(
"modernize-use-starts-ends-with");
+ CheckFactories.registerCheck<UseStaticLambdaCheck>(
+ "modernize-use-static-lambda");
CheckFactories.registerCheck<UseStdBitCheck>("modernize-use-std-bit");
CheckFactories.registerCheck<UseStdFormatCheck>("modernize-use-std-format");
CheckFactories.registerCheck<UseStdNumbersCheck>(
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..25331baddee80
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/modernize/UseStaticLambdaCheck.cpp
@@ -0,0 +1,77 @@
+#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 {
+
+void UseStaticLambdaCheck::registerMatchers(MatchFinder *Finder) {
+ // Match lambdas that have no captures and no capture-default.
+ Finder->addMatcher(
+ lambdaExpr(unless(hasAnyCapture(lambdaCapture()))).bind("lambda"), this);
+}
+
+void UseStaticLambdaCheck::check(const MatchFinder::MatchResult &Result) {
+ const auto *Lambda = Result.Nodes.getNodeAs<LambdaExpr>("lambda");
+ if (!Lambda)
+ return;
+
+ // Reject lambdas with a capture-default (e.g. [=] or [&] with no actual
+ // captures): they are not truly capture-free.
+ if (Lambda->getCaptureDefault() != LCD_None)
+ return;
+
+ const CXXMethodDecl *CallOp = Lambda->getCallOperator();
+
+ // Already static
+ if (CallOp->isStatic())
+ return;
+
+ // `static` and `mutable` are mutually exclusive lambda-specifiers (C++23).
+ // A mutable lambda has a non-const call operator that is not already static.
+ if (!CallOp->isConst())
+ return;
+
+ const SourceLocation LambdaLoc = Lambda->getBeginLoc();
+ if (LambdaLoc.isInvalid() || LambdaLoc.isMacroID())
+ return;
+
+ const SourceManager &SM = *Result.SourceManager;
+ const LangOptions &LangOpts = getLangOpts();
+
+ if (Lambda->hasExplicitParameters()) {
+ // Lambda has an explicit parameter list: [...](params) { ... }.
+ // Insert static right after the closing ).
+ const TypeSourceInfo *TSI = CallOp->getTypeSourceInfo();
+ if (!TSI)
+ return;
+ auto FTL = TSI->getTypeLoc().IgnoreParens().getAs<FunctionTypeLoc>();
+ if (!FTL)
+ return;
+ const SourceLocation RParenLoc = FTL.getRParenLoc();
+ if (RParenLoc.isInvalid())
+ return;
+ const SourceLocation InsertLoc =
+ Lexer::getLocForEndOfToken(RParenLoc, 0, SM, LangOpts);
+ diag(LambdaLoc, "lambda with empty capture list can be marked static")
+ << FixItHint::CreateInsertion(InsertLoc, " static");
+ } else {
+ // Lambda has no explicit parameter list: [...] { ... }.
+ // We must add () as well since specifiers require a parameter list.
+ const SourceLocation IntroEnd = Lambda->getIntroducerRange().getEnd();
+ if (IntroEnd.isInvalid())
+ return;
+ const SourceLocation InsertLoc =
+ Lexer::getLocForEndOfToken(IntroEnd, 0, SM, LangOpts);
+ diag(LambdaLoc, "lambda with empty capture list can be marked 'static'")
+ << FixItHint::CreateInsertion(InsertLoc, "() static");
+ }
+}
+
+} // 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..abb5d6360668b
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/modernize/UseStaticLambdaCheck.h
@@ -0,0 +1,38 @@
+//===----------------------------------------------------------------------===//
+//
+// 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).
+///
+/// A lambda with an empty capture list (``[]``) has no dependency on any
+/// enclosing state. Marking it ``static`` communicates this clearly and
+/// may allow the compiler to skip generating an implicit conversion-to-
+/// function-pointer operator.
+///
+/// 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) {}
+ void registerMatchers(ast_matchers::MatchFinder *Finder) override;
+ void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
+ bool isLanguageVersionSupported(const LangOptions &LangOpts) const override {
+ return LangOpts.CPlusPlus23;
+ }
+};
+
+} // namespace clang::tidy::modernize
+
+#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MODERNIZE_USESTATICLAMBDACHECK_H
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 <modernize/use-ranges>`, "Yes"
:doc:`modernize-use-scoped-lock <modernize/use-scoped-lock>`, "Yes"
:doc:`modernize-use-starts-ends-with <modernize/use-starts-ends-with>`, "Yes"
+ :doc:`modernize-use-static-lambda <modernize/use-static-lambda>`, "Yes"
:doc:`modernize-use-std-bit <modernize/use-std-bit>`, "Yes"
:doc:`modernize-use-std-format <modernize/use-std-format>`, "Yes"
:doc:`modernize-use-std-numbers <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..5ff2721aa735d
--- /dev/null
+++ b/clang-tools-extra/docs/clang-tidy/checks/modernize/use-static-lambda.rst
@@ -0,0 +1,39 @@
+.. 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).
+
+A non-capturing lambda does not depend on any enclosing state. Marking it
+``static`` makes that property explicit in the source and allows the compiler
+to omit the implicit conversion-to-function-pointer member.
+
+This check requires C++23 or later because ``static`` as a lambda-specifier
+was introduced in C++23 (P2564R3).
+
+.. note::
+ The ``static`` and ``mutable`` lambda-specifiers are mutually exclusive.
+ Mutable lambdas are not diagnosed even when their capture list is empty.
+
+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; };
+
+Note that when the original lambda has no explicit parameter list, ``()`` is
+inserted along with ``static``, because a parameter list is required whenever
+lambda-specifiers are present.
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..11dad0629b630
--- /dev/null
+++ b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-static-lambda.cpp
@@ -0,0 +1,79 @@
+// RUN: %check_clang_tidy -std=c++23-or-later %s modernize-use-static-lambda %t
+
+// Basic cases
+
+void basicCases() {
+ // Lambda with explicit params, no capture.
+ 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; };
+
+ // Lambda with no params, no capture.
+ 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; };
+
+ // Lambda with trailing return type.
+ 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; };
+
+ // Lambda with noexcept.
+ 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; };
+}
+
+// Already static: no warning
+
+void alreadyStatic() {
+ auto f = [](int x) static { return x * x; };
+ (void)f;
+}
+
+// Capturing lambdas: no warning
+
+void capturingLambdas() {
+ int x = 5;
+
+ // Explicit capture by value.
+ auto f1 = [x]() { return x; };
+ (void)f1;
+
+ // Explicit capture by reference.
+ auto f2 = [&x]() { return x; };
+ (void)f2;
+
+ // Capture-default by value (even if nothing actually captured).
+ auto f3 = [=]() { return 0; };
+ (void)f3;
+
+ // Capture-default by reference (even if nothing actually captured).
+ auto f4 = [&]() { return 0; };
+ (void)f4;
+}
+
+// Mutable lambda: no warning (static and mutable are mutually exclusive)
+
+void mutableLambda() {
+ // Mutable with no capture is still excluded.
+ auto g = []() mutable { return 0; };
+ (void)g;
+}
+
+// Macro: no warning
+
+#define MAKE_LAMBDA [](int x) { return x; }
+
+void macroLambda() {
+ auto f = MAKE_LAMBDA;
+ (void)f;
+}
+
+// constexpr lambda: warning (static + constexpr is valid in C++23)
+
+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; };
+}
>From 38d3fe801804587e5f0ad88838f76319179641c9 Mon Sep 17 00:00:00 2001
From: Aayush Shrivastava <iamaayushrivastava at gmail.com>
Date: Mon, 11 May 2026 13:11:27 +0530
Subject: [PATCH 2/4] Add license comments
---
.../clang-tidy/modernize/UseStaticLambdaCheck.cpp | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/clang-tools-extra/clang-tidy/modernize/UseStaticLambdaCheck.cpp b/clang-tools-extra/clang-tidy/modernize/UseStaticLambdaCheck.cpp
index 25331baddee80..24665882005b1 100644
--- a/clang-tools-extra/clang-tidy/modernize/UseStaticLambdaCheck.cpp
+++ b/clang-tools-extra/clang-tidy/modernize/UseStaticLambdaCheck.cpp
@@ -1,3 +1,11 @@
+//===----------------------------------------------------------------------===//
+//
+// 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"
@@ -74,4 +82,4 @@ void UseStaticLambdaCheck::check(const MatchFinder::MatchResult &Result) {
}
}
-} // namespace clang::tidy::modernize
+} // namespace clang::tidy::modernize
\ No newline at end of file
>From 4369134a2035fe7db8d8a6013de148ed26d75f9d Mon Sep 17 00:00:00 2001
From: Aayush Shrivastava <iamaayushrivastava at gmail.com>
Date: Mon, 11 May 2026 13:21:46 +0530
Subject: [PATCH 3/4] [clang-tidy] Add release notes and fix include order for
modernize-use-static-lambda
---
.../clang-tidy/modernize/ModernizeTidyModule.cpp | 2 +-
clang-tools-extra/docs/ReleaseNotes.rst | 6 ++++++
2 files changed, 7 insertions(+), 1 deletion(-)
diff --git a/clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp b/clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp
index dfb675f850d24..7471608b2172d 100644
--- a/clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp
+++ b/clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp
@@ -47,11 +47,11 @@
#include "UseRangesCheck.h"
#include "UseScopedLockCheck.h"
#include "UseStartsEndsWithCheck.h"
+#include "UseStaticLambdaCheck.h"
#include "UseStdBitCheck.h"
#include "UseStdFormatCheck.h"
#include "UseStdNumbersCheck.h"
#include "UseStdPrintCheck.h"
-#include "UseStaticLambdaCheck.h"
#include "UseStringViewCheck.h"
#include "UseStructuredBindingCheck.h"
#include "UseTrailingReturnTypeCheck.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
+ <clang-tidy/checks/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
<clang-tidy/checks/modernize/use-std-bit>` check.
>From ba74e6b0056a7e594439d860615f4a058880775b Mon Sep 17 00:00:00 2001
From: Aayush Shrivastava <iamaayushrivastava at gmail.com>
Date: Mon, 11 May 2026 14:45:29 +0530
Subject: [PATCH 4/4] Fix inconsistent diagnostic string in
modernize-use-static-lambda
---
clang-tools-extra/clang-tidy/modernize/UseStaticLambdaCheck.cpp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/clang-tools-extra/clang-tidy/modernize/UseStaticLambdaCheck.cpp b/clang-tools-extra/clang-tidy/modernize/UseStaticLambdaCheck.cpp
index 24665882005b1..fdcdee06464ee 100644
--- a/clang-tools-extra/clang-tidy/modernize/UseStaticLambdaCheck.cpp
+++ b/clang-tools-extra/clang-tidy/modernize/UseStaticLambdaCheck.cpp
@@ -67,7 +67,7 @@ void UseStaticLambdaCheck::check(const MatchFinder::MatchResult &Result) {
return;
const SourceLocation InsertLoc =
Lexer::getLocForEndOfToken(RParenLoc, 0, SM, LangOpts);
- diag(LambdaLoc, "lambda with empty capture list can be marked static")
+ diag(LambdaLoc, "lambda with empty capture list can be marked 'static'")
<< FixItHint::CreateInsertion(InsertLoc, " static");
} else {
// Lambda has no explicit parameter list: [...] { ... }.
More information about the cfe-commits
mailing list