[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
Sun Jul 19 13:05:47 PDT 2026
https://github.com/lucasly-ba updated https://github.com/llvm/llvm-project/pull/210554
>From 1c89fd4734b7b6e53305b28273558115e5ca29f1 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 | 85 +++++++++++++++++++
.../clang-tidy/modernize/UseAsConstCheck.h | 43 ++++++++++
clang-tools-extra/docs/ReleaseNotes.rst | 7 ++
.../docs/clang-tidy/checks/list.rst | 1 +
.../checks/modernize/use-as-const.rst | 29 +++++++
.../checkers/modernize/use-as-const.cpp | 78 +++++++++++++++++
8 files changed, 247 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.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..d19f13226e4d6
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/modernize/UseAsConstCheck.cpp
@@ -0,0 +1,85 @@
+//===----------------------------------------------------------------------===//
+//
+// 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 {
+
+UseAsConstCheck::UseAsConstCheck(StringRef Name, ClangTidyContext *Context)
+ : ClangTidyCheck(Name, Context),
+ Inserter(Options.getLocalOrGlobal("IncludeStyle",
+ utils::IncludeSorter::IS_LLVM),
+ areDiagsSelfContained()) {}
+
+void UseAsConstCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
+ Options.store(Opts, "IncludeStyle", Inserter.getStyle());
+}
+
+void UseAsConstCheck::registerMatchers(MatchFinder *Finder) {
+ // Match a static_cast to a const-qualified type; check() narrows it down.
+ Finder->addMatcher(
+ cxxStaticCastExpr(hasType(qualType(isConstQualified()))).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");
+ // A cast written in a macro cannot be safely rewritten.
+ if (Cast->getBeginLoc().isMacroID() || Cast->getEndLoc().isMacroID())
+ return;
+
+ // Only handle a cast written as 'const T &'.
+ const auto *RefType = Cast->getTypeAsWritten()->getAs<LValueReferenceType>();
+ if (!RefType)
+ return;
+ QualType Pointee = RefType->getPointeeType();
+ if (!Pointee.isConstQualified())
+ return;
+
+ const Expr *Sub = Cast->getSubExprAsWritten();
+ // In a template 'const T &' may collapse to a reference that adds no const,
+ // where std::as_const is not equivalent.
+ if (Sub->isTypeDependent() || Pointee->isDependentType())
+ return;
+ if (!Sub->isLValue())
+ return;
+
+ // Rewrite only when const is the sole difference; adding const to the
+ // sub-expression type keeps any volatile.
+ const ASTContext &Ctx = *Result.Context;
+ QualType SubType = Sub->getType();
+ if (SubType.isConstQualified() ||
+ !Ctx.hasSameType(Pointee, SubType.withConst()))
+ return;
+
+ const SourceManager &SM = *Result.SourceManager;
+ StringRef SubText = Lexer::getSourceText(
+ CharSourceRange::getTokenRange(Sub->getSourceRange()), SM, getLangOpts());
+ if (SubText.empty())
+ return;
+
+ diag(Cast->getBeginLoc(),
+ "use 'std::as_const' instead of 'static_cast' to add 'const'")
+ << 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..145875df06cac
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/modernize/UseAsConstCheck.h
@@ -0,0 +1,43 @@
+//===----------------------------------------------------------------------===//
+//
+// 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;
+};
+
+} // 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..bd75a49c7b431
--- /dev/null
+++ b/clang-tools-extra/docs/clang-tidy/checks/modernize/use-as-const.rst
@@ -0,0 +1,29 @@
+.. 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.
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..5c32cc0956267
--- /dev/null
+++ b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-as-const.cpp
@@ -0,0 +1,78 @@
+// RUN: %check_clang_tidy -std=c++17 %s modernize-use-as-const %t
+
+// CHECK-FIXES: #include <utility>
+
+struct S {};
+struct Derived : S {};
+void use(const S &);
+
+typedef S SAlias;
+
+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]));
+}
+
+// The operand type may be spelled through a typedef.
+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));
+}
+
+// A concrete cast inside a template still fires, and an instantiation must not
+// report it 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>;
+
+void negatives(const S cobj, Derived d, S obj) {
+ // Already const: there is nothing to add.
+ use(static_cast<const S &>(cobj));
+ // Derived-to-base changes the type, not just adds const.
+ use(static_cast<const S &>(d));
+ // Not a cast to a const reference.
+ S copy = static_cast<S>(obj);
+ (void)copy;
+}
+
+// A cast written inside a macro is left untouched: the fix would be unreliable.
+#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