[clang-tools-extra] [clang-tidy] Add `modernize-use-as-const` check (PR #210554)
Lucas Ly Ba via cfe-commits
cfe-commits at lists.llvm.org
Tue Jul 28 18:55:49 PDT 2026
https://github.com/lucasly-ba updated https://github.com/llvm/llvm-project/pull/210554
>From 960ad52a6bcac10fe0f41295202d23b676cf359b Mon Sep 17 00:00:00 2001
From: Lucas Ly Ba <hi at lucaslyba.com>
Date: Sun, 19 Jul 2026 00:10:25 +0200
Subject: [PATCH] [clang-tidy] Add `modernize-use-as-const` check
Replaces a static_cast that only adds const to an lvalue with a call to
std::as_const (C++17), which states the intent more clearly and cannot
accidentally change the referenced type. The fix also inserts the <utility>
include.
Fixes #189665
---
.../clang-tidy/modernize/CMakeLists.txt | 1 +
.../modernize/ModernizeTidyModule.cpp | 3 +
.../clang-tidy/modernize/UseAsConstCheck.cpp | 101 ++++++++++++++++++
.../clang-tidy/modernize/UseAsConstCheck.h | 44 ++++++++
clang-tools-extra/docs/ReleaseNotes.rst | 7 ++
.../docs/clang-tidy/checks/list.rst | 1 +
.../checks/modernize/use-as-const.rst | 44 ++++++++
.../modernize/use-as-const-ignore-macros.cpp | 22 ++++
.../checkers/modernize/use-as-const.cpp | 99 +++++++++++++++++
9 files changed, 322 insertions(+)
create mode 100644 clang-tools-extra/clang-tidy/modernize/UseAsConstCheck.cpp
create mode 100644 clang-tools-extra/clang-tidy/modernize/UseAsConstCheck.h
create mode 100644 clang-tools-extra/docs/clang-tidy/checks/modernize/use-as-const.rst
create mode 100644 clang-tools-extra/test/clang-tidy/checkers/modernize/use-as-const-ignore-macros.cpp
create mode 100644 clang-tools-extra/test/clang-tidy/checkers/modernize/use-as-const.cpp
diff --git a/clang-tools-extra/clang-tidy/modernize/CMakeLists.txt b/clang-tools-extra/clang-tidy/modernize/CMakeLists.txt
index 2c5c44db587fe..78e9c02b16255 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
+ UseAsConstCheck.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 cc13da7535bcb..abeff9e72e117 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 "UseAsConstCheck.h"
#include "UseAutoCheck.h"
#include "UseBoolLiteralsCheck.h"
#include "UseConstraintsCheck.h"
@@ -88,6 +89,8 @@ class ModernizeModule : public ClangTidyModule {
CheckFactories.registerCheck<MinMaxUseInitializerListCheck>(
"modernize-min-max-use-initializer-list");
CheckFactories.registerCheck<PassByValueCheck>("modernize-pass-by-value");
+ CheckFactories.registerCheck<UseAsConstCheck>(
+ "modernize-use-as-const");
CheckFactories.registerCheck<UseDesignatedInitializersCheck>(
"modernize-use-designated-initializers");
CheckFactories.registerCheck<UseIntegerSignComparisonCheck>(
diff --git a/clang-tools-extra/clang-tidy/modernize/UseAsConstCheck.cpp b/clang-tools-extra/clang-tidy/modernize/UseAsConstCheck.cpp
new file mode 100644
index 0000000000000..61582abd87ba7
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/modernize/UseAsConstCheck.cpp
@@ -0,0 +1,101 @@
+//===----------------------------------------------------------------------===//
+//
+// 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 "UseAsConstCheck.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 {
+
+namespace {
+
+AST_MATCHER(Expr, isLValueExpr) { return Node.isLValue(); }
+
+AST_MATCHER_P(ExplicitCastExpr, hasSourceExpressionAsWritten,
+ ast_matchers::internal::Matcher<Expr>, InnerMatcher) {
+ const Expr *Sub = Node.getSubExprAsWritten();
+ return Sub != nullptr && InnerMatcher.matches(*Sub, Finder, Builder);
+}
+
+AST_MATCHER(ExplicitCastExpr, addsOnlyConst) {
+ const auto *Ref = Node.getTypeAsWritten()->getAs<LValueReferenceType>();
+ if (Ref == nullptr)
+ return false;
+ const Expr *Sub = Node.getSubExprAsWritten();
+ return Sub != nullptr &&
+ Finder->getASTContext().hasSameType(Ref->getPointeeType(),
+ Sub->getType().withConst());
+}
+
+} // namespace
+
+UseAsConstCheck::UseAsConstCheck(StringRef Name, ClangTidyContext *Context)
+ : ClangTidyCheck(Name, Context),
+ Inserter(Options.getLocalOrGlobal("IncludeStyle",
+ utils::IncludeSorter::IS_LLVM),
+ areDiagsSelfContained()),
+ IgnoreMacros(Options.getLocalOrGlobal("IgnoreMacros", true)) {}
+
+void UseAsConstCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
+ Options.store(Opts, "IncludeStyle", Inserter.getStyle());
+ Options.store(Opts, "IgnoreMacros", IgnoreMacros);
+}
+
+void UseAsConstCheck::registerMatchers(MatchFinder *Finder) {
+ Finder->addMatcher(
+ cxxStaticCastExpr(
+ unless(isTypeDependent()),
+ hasDestinationType(qualType(hasCanonicalType(
+ lValueReferenceType(pointee(qualType(isConstQualified())))))),
+ hasSourceExpressionAsWritten(
+ expr(unless(isTypeDependent()), isLValueExpr(),
+ unless(hasType(qualType(isConstQualified()))))
+ .bind("sub")),
+ addsOnlyConst())
+ .bind("cast"),
+ this);
+}
+
+void UseAsConstCheck::registerPPCallbacks(const SourceManager &SM,
+ Preprocessor *PP,
+ Preprocessor *ModuleExpanderPP) {
+ Inserter.registerPreprocessor(PP);
+}
+
+void UseAsConstCheck::check(const MatchFinder::MatchResult &Result) {
+ const auto *Cast = Result.Nodes.getNodeAs<CXXStaticCastExpr>("cast");
+ const auto *Sub = Result.Nodes.getNodeAs<Expr>("sub");
+
+ const bool InMacro =
+ Cast->getBeginLoc().isMacroID() || Cast->getEndLoc().isMacroID();
+ if (InMacro && IgnoreMacros)
+ return;
+
+ auto Diag =
+ diag(Cast->getBeginLoc(),
+ "use 'std::as_const' instead of 'static_cast' to add 'const'");
+
+ if (InMacro)
+ return;
+
+ const SourceManager &SM = *Result.SourceManager;
+ StringRef SubText = Lexer::getSourceText(
+ CharSourceRange::getTokenRange(Sub->getSourceRange()), SM, getLangOpts());
+ if (SubText.empty())
+ return;
+
+ Diag << FixItHint::CreateReplacement(Cast->getSourceRange(),
+ ("std::as_const(" + SubText + ")").str())
+ << Inserter.createIncludeInsertion(SM.getFileID(Cast->getBeginLoc()),
+ "<utility>");
+}
+
+} // namespace clang::tidy::modernize
diff --git a/clang-tools-extra/clang-tidy/modernize/UseAsConstCheck.h b/clang-tools-extra/clang-tidy/modernize/UseAsConstCheck.h
new file mode 100644
index 0000000000000..ff18936a07b84
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/modernize/UseAsConstCheck.h
@@ -0,0 +1,44 @@
+//===----------------------------------------------------------------------===//
+//
+// 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_USEASCONSTCHECK_H
+#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MODERNIZE_USEASCONSTCHECK_H
+
+#include "../ClangTidyCheck.h"
+#include "../utils/IncludeInserter.h"
+
+namespace clang::tidy::modernize {
+
+/// Suggests ``std::as_const`` for a ``static_cast`` that only adds ``const`` to
+/// an lvalue, e.g. ``static_cast<const T &>(x)`` becomes ``std::as_const(x)``.
+///
+/// For the user-facing documentation see:
+/// https://clang.llvm.org/extra/clang-tidy/checks/modernize/use-as-const.html
+class UseAsConstCheck : public ClangTidyCheck {
+public:
+ UseAsConstCheck(StringRef Name, ClangTidyContext *Context);
+ void storeOptions(ClangTidyOptions::OptionMap &Opts) override;
+ 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;
+ bool isLanguageVersionSupported(const LangOptions &LangOpts) const override {
+ return LangOpts.CPlusPlus17;
+ }
+ std::optional<TraversalKind> getCheckTraversalKind() const override {
+ return TK_IgnoreUnlessSpelledInSource;
+ }
+
+private:
+ utils::IncludeInserter Inserter;
+ const bool IgnoreMacros;
+};
+
+} // namespace clang::tidy::modernize
+
+#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MODERNIZE_USEASCONSTCHECK_H
diff --git a/clang-tools-extra/docs/ReleaseNotes.rst b/clang-tools-extra/docs/ReleaseNotes.rst
index 69c3bcf67b8db..63988a8d22950 100644
--- a/clang-tools-extra/docs/ReleaseNotes.rst
+++ b/clang-tools-extra/docs/ReleaseNotes.rst
@@ -97,6 +97,13 @@ Improvements to clang-tidy
New checks
^^^^^^^^^^
+- New :doc:`modernize-use-as-const
+ <clang-tidy/checks/modernize/use-as-const>` check.
+
+ Replaces a ``static_cast`` that only adds ``const`` to an lvalue with a call
+ to ``std::as_const`` (available since C++17), which states the intent more
+ clearly and cannot accidentally change the referenced type.
+
New check aliases
^^^^^^^^^^^^^^^^^
diff --git a/clang-tools-extra/docs/clang-tidy/checks/list.rst b/clang-tools-extra/docs/clang-tidy/checks/list.rst
index 2a44dc78fbc89..24613445f3205 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/list.rst
+++ b/clang-tools-extra/docs/clang-tidy/checks/list.rst
@@ -312,6 +312,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-as-const <modernize/use-as-const>`, "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-as-const.rst b/clang-tools-extra/docs/clang-tidy/checks/modernize/use-as-const.rst
new file mode 100644
index 0000000000000..5ff53b766f7aa
--- /dev/null
+++ b/clang-tools-extra/docs/clang-tidy/checks/modernize/use-as-const.rst
@@ -0,0 +1,44 @@
+.. title:: clang-tidy - modernize-use-as-const
+
+modernize-use-as-const
+======================
+
+Replaces a ``static_cast`` that only adds ``const`` to an lvalue with a call to
+``std::as_const`` (available since C++17), which states the intent more clearly
+and cannot accidentally change the referenced type.
+
+.. code-block:: c++
+
+ void use(const std::string &);
+
+ void f(std::string s) {
+ use(static_cast<const std::string &>(s));
+ }
+
+becomes
+
+.. code-block:: c++
+
+ void use(const std::string &);
+
+ void f(std::string s) {
+ use(std::as_const(s));
+ }
+
+Casts of an already ``const`` operand, and casts that change the type rather than
+only adding ``const``, are left untouched. A cast whose operand is an rvalue is
+also left untouched, because ``std::as_const`` is deleted for rvalues.
+
+Options
+-------
+
+.. option:: IgnoreMacros
+
+ If set to `true`, the check will not give warnings inside macros. Default
+ is `true`. A cast inside a macro is reported but not fixed, since the
+ replacement would rewrite the macro body for every expansion.
+
+.. option:: IncludeStyle
+
+ A string specifying which include-style is used, `llvm` or `google`. Default
+ is `llvm`.
diff --git a/clang-tools-extra/test/clang-tidy/checkers/modernize/use-as-const-ignore-macros.cpp b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-as-const-ignore-macros.cpp
new file mode 100644
index 0000000000000..0337a8b35b369
--- /dev/null
+++ b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-as-const-ignore-macros.cpp
@@ -0,0 +1,22 @@
+// RUN: %check_clang_tidy -std=c++17-or-later %s modernize-use-as-const %t -- \
+// RUN: -config="{CheckOptions: {modernize-use-as-const.IgnoreMacros: false}}"
+
+// CHECK-FIXES: #include <utility>
+
+struct S {};
+void use(const S &);
+
+#define TO_CONST(x) static_cast<const S &>(x)
+
+// Reported at the expansion, but not fixed.
+void in_macro(S obj) {
+ use(TO_CONST(obj));
+ // CHECK-MESSAGES: :[[@LINE-1]]:7: warning: use 'std::as_const' instead of 'static_cast' to add 'const' [modernize-use-as-const]
+ // CHECK-FIXES: use(TO_CONST(obj));
+}
+
+void outside_macro(S obj) {
+ use(static_cast<const S &>(obj));
+ // CHECK-MESSAGES: :[[@LINE-1]]:7: warning: use 'std::as_const' instead of 'static_cast' to add 'const' [modernize-use-as-const]
+ // CHECK-FIXES: use(std::as_const(obj));
+}
diff --git a/clang-tools-extra/test/clang-tidy/checkers/modernize/use-as-const.cpp b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-as-const.cpp
new file mode 100644
index 0000000000000..94f8389a08332
--- /dev/null
+++ b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-as-const.cpp
@@ -0,0 +1,99 @@
+// RUN: %check_clang_tidy -std=c++17-or-later %s modernize-use-as-const %t
+
+// CHECK-FIXES: #include <utility>
+
+struct S {};
+struct Derived : S {};
+void use(const S &);
+void use_cv(const volatile S &);
+
+typedef S SAlias;
+using ConstRef = const S &;
+
+void basic(S obj) {
+ use(static_cast<const S &>(obj));
+ // CHECK-MESSAGES: :[[@LINE-1]]:7: warning: use 'std::as_const' instead of 'static_cast' to add 'const' [modernize-use-as-const]
+ // CHECK-FIXES: use(std::as_const(obj));
+}
+
+struct Wrap {
+ S m;
+};
+
+void other_lvalues(Wrap w, S *p, S arr[1]) {
+ use(static_cast<const S &>(w.m));
+ // CHECK-MESSAGES: :[[@LINE-1]]:7: warning: use 'std::as_const' instead of 'static_cast' to add 'const' [modernize-use-as-const]
+ // CHECK-FIXES: use(std::as_const(w.m));
+ use(static_cast<const S &>(*p));
+ // CHECK-MESSAGES: :[[@LINE-1]]:7: warning: use 'std::as_const' instead of 'static_cast' to add 'const' [modernize-use-as-const]
+ // CHECK-FIXES: use(std::as_const(*p));
+ use(static_cast<const S &>(arr[0]));
+ // CHECK-MESSAGES: :[[@LINE-1]]:7: warning: use 'std::as_const' instead of 'static_cast' to add 'const' [modernize-use-as-const]
+ // CHECK-FIXES: use(std::as_const(arr[0]));
+}
+
+void via_typedef(SAlias obj) {
+ use(static_cast<const S &>(obj));
+ // CHECK-MESSAGES: :[[@LINE-1]]:7: warning: use 'std::as_const' instead of 'static_cast' to add 'const' [modernize-use-as-const]
+ // CHECK-FIXES: use(std::as_const(obj));
+}
+
+void destination_typedef(S obj) {
+ use(static_cast<ConstRef>(obj));
+ // CHECK-MESSAGES: :[[@LINE-1]]:7: warning: use 'std::as_const' instead of 'static_cast' to add 'const' [modernize-use-as-const]
+ // CHECK-FIXES: use(std::as_const(obj));
+}
+
+void keeps_volatile(volatile S vobj) {
+ use_cv(static_cast<const volatile S &>(vobj));
+ // CHECK-MESSAGES: :[[@LINE-1]]:10: warning: use 'std::as_const' instead of 'static_cast' to add 'const' [modernize-use-as-const]
+ // CHECK-FIXES: use_cv(std::as_const(vobj));
+}
+
+// The instantiation must not report the cast a second time.
+template <typename T>
+struct Holder {
+ const S &get(S &s) {
+ return static_cast<const S &>(s);
+ // CHECK-MESSAGES: :[[@LINE-1]]:12: warning: use 'std::as_const' instead of 'static_cast' to add 'const' [modernize-use-as-const]
+ // CHECK-FIXES: return std::as_const(s);
+ }
+};
+template struct Holder<int>;
+
+S make();
+
+// std::as_const is deleted for rvalues, even though binding one to 'const S &'
+// is well-formed.
+void rvalue_negatives(S obj) {
+ use(static_cast<const S &>(S{}));
+ use(static_cast<const S &>(make()));
+ use(static_cast<const S &>(static_cast<S &&>(obj)));
+}
+
+void negatives(const S cobj, Derived d, S obj) {
+ use(static_cast<const S &>(cobj));
+ use(static_cast<const S &>(d));
+ S copy = static_cast<S>(obj);
+ (void)copy;
+ S &&r = static_cast<S &&>(obj);
+ (void)r;
+}
+
+// See use-as-const-ignore-macros.cpp for IgnoreMacros: false.
+#define TO_CONST(x) static_cast<const S &>(x)
+void in_macro(S obj) {
+ use(TO_CONST(obj));
+}
+
+// Dependent casts are skipped: when T is a reference type 'const T &' collapses
+// and adds no const, so std::as_const would not be equivalent.
+template <typename T>
+const T &as_const_tmpl(T &x) {
+ return static_cast<const T &>(x);
+}
+void instantiate() {
+ S s;
+ as_const_tmpl(s);
+ as_const_tmpl<S &>(s);
+}
More information about the cfe-commits
mailing list