https://github.com/vogelsgesang updated 
https://github.com/llvm/llvm-project/pull/210459

>From 55dd3d3785c550bbebf727849c77359d854ff0d9 Mon Sep 17 00:00:00 2001
From: Adrian Vogelsgesang <[email protected]>
Date: Fri, 17 Jul 2026 21:37:31 +0000
Subject: [PATCH 1/4] [clang-tidy] Add modernize-use-to-underlying check

Add a new check that flags casts from a scoped enumeration to an integer
type and replaces them with a call to std::to_underlying (C++23).

Addresses #71543.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
---
 .../clang-tidy/modernize/CMakeLists.txt       |   1 +
 .../modernize/ModernizeTidyModule.cpp         |   3 +
 .../modernize/UseToUnderlyingCheck.cpp        | 163 ++++++++++++++++++
 .../modernize/UseToUnderlyingCheck.h          |  60 +++++++
 clang-tools-extra/docs/ReleaseNotes.rst       |   6 +
 .../docs/clang-tidy/checks/list.rst           |   1 +
 .../checks/modernize/use-to-underlying.rst    | 105 +++++++++++
 .../use-to-underlying-custom-function.cpp     |  17 ++
 .../use-to-underlying-imprecise-casts.cpp     |  55 ++++++
 .../checkers/modernize/use-to-underlying.cpp  | 108 ++++++++++++
 10 files changed, 519 insertions(+)
 create mode 100644 
clang-tools-extra/clang-tidy/modernize/UseToUnderlyingCheck.cpp
 create mode 100644 
clang-tools-extra/clang-tidy/modernize/UseToUnderlyingCheck.h
 create mode 100644 
clang-tools-extra/docs/clang-tidy/checks/modernize/use-to-underlying.rst
 create mode 100644 
clang-tools-extra/test/clang-tidy/checkers/modernize/use-to-underlying-custom-function.cpp
 create mode 100644 
clang-tools-extra/test/clang-tidy/checkers/modernize/use-to-underlying-imprecise-casts.cpp
 create mode 100644 
clang-tools-extra/test/clang-tidy/checkers/modernize/use-to-underlying.cpp

diff --git a/clang-tools-extra/clang-tidy/modernize/CMakeLists.txt 
b/clang-tools-extra/clang-tidy/modernize/CMakeLists.txt
index 2c5c44db587fe..23aeb393ad016 100644
--- a/clang-tools-extra/clang-tidy/modernize/CMakeLists.txt
+++ b/clang-tools-extra/clang-tidy/modernize/CMakeLists.txt
@@ -53,6 +53,7 @@ add_clang_library(clangTidyModernizeModule STATIC
   UseStdPrintCheck.cpp
   UseStringViewCheck.cpp
   UseStructuredBindingCheck.cpp
+  UseToUnderlyingCheck.cpp
   UseTrailingReturnTypeCheck.cpp
   UseTransparentFunctorsCheck.cpp
   UseUncaughtExceptionsCheck.cpp
diff --git a/clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp 
b/clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp
index cc13da7535bcb..62676469aefe7 100644
--- a/clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp
+++ b/clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp
@@ -53,6 +53,7 @@
 #include "UseStdPrintCheck.h"
 #include "UseStringViewCheck.h"
 #include "UseStructuredBindingCheck.h"
+#include "UseToUnderlyingCheck.h"
 #include "UseTrailingReturnTypeCheck.h"
 #include "UseTransparentFunctorsCheck.h"
 #include "UseUncaughtExceptionsCheck.h"
@@ -138,6 +139,8 @@ class ModernizeModule : public ClangTidyModule {
         "modernize-use-string-view");
     CheckFactories.registerCheck<UseStructuredBindingCheck>(
         "modernize-use-structured-binding");
+    CheckFactories.registerCheck<UseToUnderlyingCheck>(
+        "modernize-use-to-underlying");
     CheckFactories.registerCheck<UseTrailingReturnTypeCheck>(
         "modernize-use-trailing-return-type");
     CheckFactories.registerCheck<UseTransparentFunctorsCheck>(
diff --git a/clang-tools-extra/clang-tidy/modernize/UseToUnderlyingCheck.cpp 
b/clang-tools-extra/clang-tidy/modernize/UseToUnderlyingCheck.cpp
new file mode 100644
index 0000000000000..c60ea02e316f2
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/modernize/UseToUnderlyingCheck.cpp
@@ -0,0 +1,163 @@
+//===--- UseToUnderlyingCheck.cpp - clang-tidy 
----------------------------===//
+//
+// 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 "UseToUnderlyingCheck.h"
+#include "clang/AST/ASTContext.h"
+#include "clang/ASTMatchers/ASTMatchFinder.h"
+#include "clang/Lex/Lexer.h"
+
+using namespace clang::ast_matchers;
+
+namespace clang::tidy {
+
+template <>
+struct OptionEnumMapping<modernize::UseToUnderlyingCheck::ImpreciseCastsKind> {
+  static llvm::ArrayRef<
+      std::pair<modernize::UseToUnderlyingCheck::ImpreciseCastsKind, 
StringRef>>
+  getEnumMapping() {
+    using ImpreciseCastsKind =
+        modernize::UseToUnderlyingCheck::ImpreciseCastsKind;
+    static constexpr std::pair<ImpreciseCastsKind, StringRef> Mapping[] = {
+        {ImpreciseCastsKind::Ignore, "Ignore"},
+        {ImpreciseCastsKind::Warn, "Warn"},
+        {ImpreciseCastsKind::PreserveType, "PreserveType"},
+        {ImpreciseCastsKind::UseUnderlyingType, "UseUnderlyingType"},
+    };
+    return {Mapping};
+  }
+};
+
+} // namespace clang::tidy
+
+namespace clang::tidy::modernize {
+
+UseToUnderlyingCheck::UseToUnderlyingCheck(StringRef Name,
+                                           ClangTidyContext *Context)
+    : ClangTidyCheck(Name, Context),
+      ImpreciseCasts(Options.get("ImpreciseCasts", ImpreciseCastsKind::Warn)),
+      ReplacementFunction(
+          Options.get("ReplacementFunction", "std::to_underlying")),
+      IncludeInserter(Options.getLocalOrGlobal("IncludeStyle",
+                                               utils::IncludeSorter::IS_LLVM),
+                      areDiagsSelfContained()),
+      MaybeHeaderToInclude(Options.get("ReplacementFunctionHeader")) {
+  if (!MaybeHeaderToInclude && ReplacementFunction == "std::to_underlying")
+    MaybeHeaderToInclude = "<utility>";
+}
+
+bool UseToUnderlyingCheck::isLanguageVersionSupported(
+    const LangOptions &LangOpts) const {
+  // std::to_underlying is a C++23 library facility, but a user-provided
+  // replacement (e.g. llvm::to_underlying) only requires scoped enumerations,
+  // which are available since C++11.
+  if (ReplacementFunction == "std::to_underlying")
+    return LangOpts.CPlusPlus23;
+  return LangOpts.CPlusPlus11;
+}
+
+void UseToUnderlyingCheck::registerPPCallbacks(const SourceManager &SM,
+                                               Preprocessor *PP,
+                                               Preprocessor *ModuleExpanderPP) 
{
+  IncludeInserter.registerPreprocessor(PP);
+}
+
+void UseToUnderlyingCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
+  Options.store(Opts, "ImpreciseCasts", ImpreciseCasts);
+  Options.store(Opts, "ReplacementFunction", ReplacementFunction);
+  Options.store(Opts, "IncludeStyle", IncludeInserter.getStyle());
+  if (MaybeHeaderToInclude)
+    Options.store(Opts, "ReplacementFunctionHeader", *MaybeHeaderToInclude);
+}
+
+void UseToUnderlyingCheck::registerMatchers(MatchFinder *Finder) {
+  // Match an explicit cast (``static_cast``, C-style or functional) whose
+  // operand is an expression of scoped-enumeration type. The destination type
+  // is validated in check() so that the same code path handles precise and
+  // imprecise conversions.
+  Finder->addMatcher(
+      explicitCastExpr(
+          unless(isInTemplateInstantiation()),
+          hasSourceExpression(
+              expr(hasType(hasCanonicalType(enumType(
+                       hasDeclaration(enumDecl(isScoped()).bind("enum"))))))
+                  .bind("operand")))
+          .bind("cast"),
+      this);
+}
+
+void UseToUnderlyingCheck::check(const MatchFinder::MatchResult &Result) {
+  const auto *Cast = Result.Nodes.getNodeAs<ExplicitCastExpr>("cast");
+  const auto *Operand = Result.Nodes.getNodeAs<Expr>("operand");
+  const auto *Enum = Result.Nodes.getNodeAs<EnumDecl>("enum");
+
+  // Ignore dependent contexts we cannot reason about reliably.
+  if (Cast->isValueDependent() || Cast->getType()->isDependentType())
+    return;
+
+  const QualType DestType = Cast->getType().getCanonicalType();
+
+  // We only care about conversions to an integer type. Enum-to-enum casts and
+  // floating-point or pointer destinations are excluded.
+  if (!DestType->isIntegerType() || DestType->isEnumeralType())
+    return;
+
+  const QualType UnderlyingType =
+      Enum->getIntegerType().getCanonicalType().getUnqualifiedType();
+  const QualType DestUnqualified = DestType.getUnqualifiedType();
+  const bool IsPrecise = UnderlyingType == DestUnqualified;
+
+  // A cast to ``bool`` that is not precise (i.e. the underlying type is not
+  // ``bool``) expresses a truthiness test rather than a request for the
+  // underlying value, so it is left untouched.
+  if (DestType->isBooleanType() && !IsPrecise)
+    return;
+
+  // Ignore imprecise casts if asked to do so.
+  if (!IsPrecise && ImpreciseCasts == ImpreciseCastsKind::Ignore)
+    return;
+
+  auto Diag = diag(Cast->getBeginLoc(),
+                   "use '%0' to convert a scoped enumeration to its "
+                   "underlying type")
+              << ReplacementFunction;
+
+  // In Warn mode an imprecise cast is diagnosed without a fix-it
+  if (!IsPrecise && ImpreciseCasts == ImpreciseCastsKind::Warn)
+    return;
+
+  const StringRef OperandText = Lexer::getSourceText(
+      CharSourceRange::getTokenRange(Operand->getSourceRange()),
+      *Result.SourceManager, getLangOpts());
+  if (OperandText.empty())
+    return;
+
+  const std::string Call =
+      (ReplacementFunction + "(" + OperandText + ")").str();
+
+  const bool ReplaceWholeCast =
+      IsPrecise || ImpreciseCasts == ImpreciseCastsKind::UseUnderlyingType;
+  if (ReplaceWholeCast) {
+    // Replace the whole cast: ``static_cast<int>(e)`` -> ``to_underlying(e)``.
+    // For an imprecise cast this changes the resulting type to the underlying
+    // type.
+    Diag << FixItHint::CreateReplacement(Cast->getSourceRange(), Call);
+  } else {
+    // Preserve the intended (wider or differently-signed) destination type by
+    // wrapping only the operand: ``static_cast<long>(e)`` ->
+    // ``static_cast<long>(to_underlying(e))``.
+    Diag << FixItHint::CreateReplacement(Operand->getSourceRange(), Call);
+  }
+
+  if (MaybeHeaderToInclude)
+    Diag << IncludeInserter.createIncludeInsertion(
+        Result.SourceManager->getFileID(
+            Result.SourceManager->getExpansionLoc(Cast->getBeginLoc())),
+        *MaybeHeaderToInclude);
+}
+
+} // namespace clang::tidy::modernize
diff --git a/clang-tools-extra/clang-tidy/modernize/UseToUnderlyingCheck.h 
b/clang-tools-extra/clang-tidy/modernize/UseToUnderlyingCheck.h
new file mode 100644
index 0000000000000..1692978af4ddd
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/modernize/UseToUnderlyingCheck.h
@@ -0,0 +1,60 @@
+//===--- UseToUnderlyingCheck.h - clang-tidy --------------------*- C++ 
-*-===//
+//
+// 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_USETOUNDERLYINGCHECK_H
+#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MODERNIZE_USETOUNDERLYINGCHECK_H
+
+#include "../ClangTidyCheck.h"
+#include "../utils/IncludeInserter.h"
+#include <optional>
+
+namespace clang::tidy::modernize {
+
+/// Finds casts from a scoped enumeration to its underlying integer type and
+/// replaces them with a call to ``std::to_underlying`` (C++23).
+///
+/// For the user-facing documentation see:
+/// 
https://clang.llvm.org/extra/clang-tidy/checks/modernize/use-to-underlying.html
+class UseToUnderlyingCheck : public ClangTidyCheck {
+public:
+  /// How to treat an imprecise cast, i.e. a cast whose destination type is an
+  /// integer type other than the enumeration's underlying type.
+  enum class ImpreciseCastsKind {
+    /// Do not diagnose imprecise casts.
+    Ignore,
+    /// Diagnose imprecise casts but do not offer a fix-it.
+    Warn,
+    /// Wrap the operand in a call to the replacement function, preserving the
+    /// original destination type.
+    PreserveType,
+    /// Replace the whole cast with a call to the replacement function, 
changing
+    /// the resulting type to the underlying type.
+    UseUnderlyingType,
+  };
+
+  UseToUnderlyingCheck(StringRef Name, ClangTidyContext *Context);
+  bool isLanguageVersionSupported(const LangOptions &LangOpts) const override;
+  void registerPPCallbacks(const SourceManager &SM, Preprocessor *PP,
+                           Preprocessor *ModuleExpanderPP) override;
+  void storeOptions(ClangTidyOptions::OptionMap &Opts) override;
+  void registerMatchers(ast_matchers::MatchFinder *Finder) override;
+  void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
+  std::optional<TraversalKind> getCheckTraversalKind() const override {
+    return TK_IgnoreUnlessSpelledInSource;
+  }
+
+private:
+  const ImpreciseCastsKind ImpreciseCasts;
+  const StringRef ReplacementFunction;
+  utils::IncludeInserter IncludeInserter;
+  std::optional<StringRef> MaybeHeaderToInclude;
+};
+
+} // namespace clang::tidy::modernize
+
+#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MODERNIZE_USETOUNDERLYINGCHECK_H
diff --git a/clang-tools-extra/docs/ReleaseNotes.rst 
b/clang-tools-extra/docs/ReleaseNotes.rst
index 69c3bcf67b8db..6e5ceb3fe96f4 100644
--- a/clang-tools-extra/docs/ReleaseNotes.rst
+++ b/clang-tools-extra/docs/ReleaseNotes.rst
@@ -97,6 +97,12 @@ Improvements to clang-tidy
 New checks
 ^^^^^^^^^^
 
+- New :doc:`modernize-use-to-underlying
+  <clang-tidy/checks/modernize/use-to-underlying>` check.
+
+  Finds casts from a scoped enumeration to an integer type and replaces them
+  with a call to ``std::to_underlying``.
+
 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..d954e0c1cc38b 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/list.rst
+++ b/clang-tools-extra/docs/clang-tidy/checks/list.rst
@@ -334,6 +334,7 @@ Clang-Tidy Checks
    :doc:`modernize-use-std-print <modernize/use-std-print>`, "Yes"
    :doc:`modernize-use-string-view <modernize/use-string-view>`, "Yes"
    :doc:`modernize-use-structured-binding <modernize/use-structured-binding>`, 
"Yes"
+   :doc:`modernize-use-to-underlying <modernize/use-to-underlying>`, "Yes"
    :doc:`modernize-use-trailing-return-type 
<modernize/use-trailing-return-type>`, "Yes"
    :doc:`modernize-use-transparent-functors 
<modernize/use-transparent-functors>`, "Yes"
    :doc:`modernize-use-uncaught-exceptions 
<modernize/use-uncaught-exceptions>`, "Yes"
diff --git 
a/clang-tools-extra/docs/clang-tidy/checks/modernize/use-to-underlying.rst 
b/clang-tools-extra/docs/clang-tidy/checks/modernize/use-to-underlying.rst
new file mode 100644
index 0000000000000..e7d1d8e3519bc
--- /dev/null
+++ b/clang-tools-extra/docs/clang-tidy/checks/modernize/use-to-underlying.rst
@@ -0,0 +1,105 @@
+.. title:: clang-tidy - modernize-use-to-underlying
+
+modernize-use-to-underlying
+===========================
+
+Finds casts from a scoped enumeration (``enum class``) to an integer type and
+replaces them with a call to ``std::to_underlying`` (introduced in C++23).
+
+Converting a scoped enumeration to a hard-coded integer type is error-prone: if
+the enumeration's underlying type is later changed, every such cast silently
+becomes a narrowing, widening or sign-changing conversion. 
``std::to_underlying``
+always yields exactly the underlying type and keeps the code correct.
+
+.. code-block:: c++
+
+  enum class Color : unsigned char { Red, Green, Blue };
+
+  void f(Color c) {
+    // Before:
+    auto value = static_cast<unsigned char>(c);
+    // After:
+    auto value = std::to_underlying(c);
+  }
+
+The check matches ``static_cast``, C-style casts and functional-style casts.
+
+Precise and imprecise casts
+---------------------------
+
+A cast is *precise* when its destination type is exactly the underlying type of
+the enumeration. In that case the whole cast is replaced:
+
+.. code-block:: c++
+
+  enum class E : int {};
+
+  int i = static_cast<int>(E{});      // becomes: std::to_underlying(E{})
+
+A cast is *imprecise* when the destination type is an integer type other than
+the underlying type (a different width or signedness), for example
+``static_cast<long>`` or ``static_cast<unsigned>`` on an ``enum : int``. Such a
+cast performs an additional integer conversion, so there is no single correct
+rewrite; how imprecise casts are handled is controlled by the
+:option:`ImpreciseCasts` option.
+
+A cast to a non-integer type (floating point, pointer, another enumeration) is
+never flagged. A cast to ``bool`` is only flagged when ``bool`` is the exact
+underlying type of the enumeration; otherwise it is treated as a truthiness
+test and left untouched.
+
+Options
+-------
+
+.. option:: ImpreciseCasts
+
+   Controls how imprecise casts (whose destination type differs from the
+   underlying type) are handled. Precise casts are always diagnosed and fixed
+   regardless of this option. Possible values:
+
+   ``Ignore``
+     Do not diagnose imprecise casts.
+
+   ``Warn`` *(default)*
+     Diagnose imprecise casts but do not offer a fix-it. Neither automatic
+     rewrite is applied because both change the meaning of the code in ways
+     that may not be intended.
+
+   ``PreserveType``
+     Wrap the operand in a call to the replacement function, keeping the
+     original destination type:
+
+     .. code-block:: c++
+
+       long l = static_cast<long>(E{});  // becomes: 
static_cast<long>(std::to_underlying(E{}))
+
+   ``UseUnderlyingType``
+     Replace the whole cast with a call to the replacement function. This
+     **changes the type** of the expression from the destination type to the
+     underlying type, so use it only when the wider or differently-signed
+     destination type was itself unintended:
+
+     .. code-block:: c++
+
+       long l = static_cast<long>(E{});  // becomes: std::to_underlying(E{})
+
+.. option:: ReplacementFunction
+
+   The fully qualified name of the function used in the replacement. Defaults 
to
+   ``std::to_underlying``. Set this to use a hand-rolled equivalent (for 
example
+   ``llvm::to_underlying``) when targeting a language standard before C++23. 
When
+   the value is ``std::to_underlying``, the check only runs in C++23 or later;
+   with any other value it runs from C++11 onwards.
+
+.. option:: ReplacementFunctionHeader
+
+   The header to include when the replacement function is used. Defaults to
+   ``<utility>`` when :option:`ReplacementFunction` is ``std::to_underlying``,
+   and is otherwise empty (no include is added). When the value is enclosed in
+   angle brackets the include directive uses angle brackets, otherwise it uses
+   quotes.
+
+.. 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-to-underlying-custom-function.cpp
 
b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-to-underlying-custom-function.cpp
new file mode 100644
index 0000000000000..95fb760b27ae0
--- /dev/null
+++ 
b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-to-underlying-custom-function.cpp
@@ -0,0 +1,17 @@
+// RUN: %check_clang_tidy -std=c++11-or-later %s modernize-use-to-underlying 
%t \
+// RUN:   -- -config="{CheckOptions: { \
+// RUN:        modernize-use-to-underlying.ReplacementFunction: 
'llvm::to_underlying', \
+// RUN:        modernize-use-to-underlying.ReplacementFunctionHeader: 
'llvm/ADT/STLExtras.h'}}"
+
+// With a user-provided replacement function the check runs before C++23 and
+// uses the configured name and header.
+
+// CHECK-FIXES: #include "llvm/ADT/STLExtras.h"
+
+enum class E : int { A, B };
+
+int convert(E e) {
+  return static_cast<int>(e);
+  // CHECK-MESSAGES: :[[@LINE-1]]:10: warning: use 'llvm::to_underlying' to 
convert a scoped enumeration to its underlying type 
[modernize-use-to-underlying]
+  // CHECK-FIXES: return llvm::to_underlying(e);
+}
diff --git 
a/clang-tools-extra/test/clang-tidy/checkers/modernize/use-to-underlying-imprecise-casts.cpp
 
b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-to-underlying-imprecise-casts.cpp
new file mode 100644
index 0000000000000..88f1ed7a29d06
--- /dev/null
+++ 
b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-to-underlying-imprecise-casts.cpp
@@ -0,0 +1,55 @@
+// RUN: %check_clang_tidy -check-suffixes=IGNORE -std=c++23-or-later %s \
+// RUN:   modernize-use-to-underlying %t -- \
+// RUN:   -config="{CheckOptions: {modernize-use-to-underlying.ImpreciseCasts: 
Ignore}}"
+// RUN: %check_clang_tidy -check-suffixes=WARN -std=c++23-or-later %s \
+// RUN:   modernize-use-to-underlying %t -- \
+// RUN:   -config="{CheckOptions: {modernize-use-to-underlying.ImpreciseCasts: 
Warn}}"
+// RUN: %check_clang_tidy -check-suffixes=PRESERVE -std=c++23-or-later %s \
+// RUN:   modernize-use-to-underlying %t -- \
+// RUN:   -config="{CheckOptions: {modernize-use-to-underlying.ImpreciseCasts: 
PreserveType}}"
+// RUN: %check_clang_tidy -check-suffixes=UNDERLYING -std=c++23-or-later %s \
+// RUN:   modernize-use-to-underlying %t -- \
+// RUN:   -config="{CheckOptions: {modernize-use-to-underlying.ImpreciseCasts: 
UseUnderlyingType}}"
+
+// CHECK-FIXES-WARN: #include <utility>
+// CHECK-FIXES-PRESERVE: #include <utility>
+// CHECK-FIXES-UNDERLYING: #include <utility>
+
+namespace std {
+template <typename T>
+constexpr __underlying_type(T) to_underlying(T value) noexcept {
+  return static_cast<__underlying_type(T)>(value);
+}
+} // namespace std
+
+enum class E : int { A, B };
+
+// A precise cast is always diagnosed and fully replaced, regardless of the
+// ImpreciseCasts option.
+int precise(E e) {
+  return static_cast<int>(e);
+  // CHECK-MESSAGES-IGNORE: :[[@LINE-1]]:10: warning: use 'std::to_underlying' 
to convert a scoped enumeration to its underlying type 
[modernize-use-to-underlying]
+  // CHECK-MESSAGES-WARN: :[[@LINE-2]]:10: warning: use 'std::to_underlying' 
to convert a scoped enumeration to its underlying type 
[modernize-use-to-underlying]
+  // CHECK-MESSAGES-PRESERVE: :[[@LINE-3]]:10: warning: use 
'std::to_underlying' to convert a scoped enumeration to its underlying type 
[modernize-use-to-underlying]
+  // CHECK-MESSAGES-UNDERLYING: :[[@LINE-4]]:10: warning: use 
'std::to_underlying' to convert a scoped enumeration to its underlying type 
[modernize-use-to-underlying]
+  // CHECK-FIXES-IGNORE: return std::to_underlying(e);
+  // CHECK-FIXES-WARN: return std::to_underlying(e);
+  // CHECK-FIXES-PRESERVE: return std::to_underlying(e);
+  // CHECK-FIXES-UNDERLYING: return std::to_underlying(e);
+}
+
+// An imprecise cast:
+//  - Ignore:            no diagnostic, no fix.
+//  - Warn:              warn but offer no fix-it.
+//  - PreserveType:      warn and wrap the operand, keeping the destination 
type.
+//  - UseUnderlyingType: warn and replace the whole cast, changing the type.
+long imprecise(E e) {
+  return static_cast<long>(e);
+  // CHECK-MESSAGES-WARN: :[[@LINE-1]]:10: warning: use 'std::to_underlying' 
to convert a scoped enumeration to its underlying type 
[modernize-use-to-underlying]
+  // CHECK-MESSAGES-PRESERVE: :[[@LINE-2]]:10: warning: use 
'std::to_underlying' to convert a scoped enumeration to its underlying type 
[modernize-use-to-underlying]
+  // CHECK-MESSAGES-UNDERLYING: :[[@LINE-3]]:10: warning: use 
'std::to_underlying' to convert a scoped enumeration to its underlying type 
[modernize-use-to-underlying]
+  // CHECK-FIXES-IGNORE: return static_cast<long>(e);
+  // CHECK-FIXES-WARN: return static_cast<long>(e);
+  // CHECK-FIXES-PRESERVE: return static_cast<long>(std::to_underlying(e));
+  // CHECK-FIXES-UNDERLYING: return std::to_underlying(e);
+}
diff --git 
a/clang-tools-extra/test/clang-tidy/checkers/modernize/use-to-underlying.cpp 
b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-to-underlying.cpp
new file mode 100644
index 0000000000000..a1ee85fcb1e53
--- /dev/null
+++ b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-to-underlying.cpp
@@ -0,0 +1,108 @@
+// RUN: %check_clang_tidy -std=c++23-or-later %s modernize-use-to-underlying %t
+
+// CHECK-FIXES: #include <utility>
+
+namespace std {
+template <typename T>
+constexpr __underlying_type(T) to_underlying(T value) noexcept {
+  return static_cast<__underlying_type(T)>(value);
+}
+} // namespace std
+
+enum class ColorInt : int { Red, Green, Blue };
+enum class ByteEnum : unsigned char { A, B };
+enum class DefaultEnum { X, Y }; // underlying type is int
+enum class OtherEnum : int {};
+enum Unscoped : int { U0, U1 }; // not a scoped enum
+
+// A precise cast (destination type equals the underlying type) is always
+// diagnosed and the whole cast is replaced. static_cast, C-style and
+// functional-style casts are all matched.
+void precise(ColorInt c, ByteEnum b, DefaultEnum d) {
+  int A = static_cast<int>(c);
+  // CHECK-MESSAGES: :[[@LINE-1]]:11: warning: use 'std::to_underlying' to 
convert a scoped enumeration to its underlying type 
[modernize-use-to-underlying]
+  // CHECK-FIXES: int A = std::to_underlying(c);
+  unsigned char B = static_cast<unsigned char>(b);
+  // CHECK-MESSAGES: :[[@LINE-1]]:21: warning: use 'std::to_underlying' to 
convert a scoped enumeration to its underlying type 
[modernize-use-to-underlying]
+  // CHECK-FIXES: unsigned char B = std::to_underlying(b);
+  int C = static_cast<int>(d);
+  // CHECK-MESSAGES: :[[@LINE-1]]:11: warning: use 'std::to_underlying' to 
convert a scoped enumeration to its underlying type 
[modernize-use-to-underlying]
+  // CHECK-FIXES: int C = std::to_underlying(d);
+  int D = (int)c;
+  // CHECK-MESSAGES: :[[@LINE-1]]:11: warning: use 'std::to_underlying' to 
convert a scoped enumeration to its underlying type 
[modernize-use-to-underlying]
+  // CHECK-FIXES: int D = std::to_underlying(c);
+  int E = int(c);
+  // CHECK-MESSAGES: :[[@LINE-1]]:11: warning: use 'std::to_underlying' to 
convert a scoped enumeration to its underlying type 
[modernize-use-to-underlying]
+  // CHECK-FIXES: int E = std::to_underlying(c);
+}
+
+// With the default ImpreciseCasts=Warn, an imprecise cast (destination type
+// differs from the underlying type in width or signedness) is diagnosed but no
+// fix-it is applied.
+void imprecise(ColorInt c) {
+  long W = static_cast<long>(c);
+  // CHECK-MESSAGES: :[[@LINE-1]]:12: warning: use 'std::to_underlying' to 
convert a scoped enumeration to its underlying type 
[modernize-use-to-underlying]
+  // CHECK-FIXES: long W = static_cast<long>(c);
+  unsigned S = static_cast<unsigned>(c);
+  // CHECK-MESSAGES: :[[@LINE-1]]:16: warning: use 'std::to_underlying' to 
convert a scoped enumeration to its underlying type 
[modernize-use-to-underlying]
+  // CHECK-FIXES: unsigned S = static_cast<unsigned>(c);
+}
+
+// Cases that must never be flagged.
+void negatives(ColorInt c, Unscoped u) {
+  bool Truthy = static_cast<bool>(c);      // truthiness test, not underlying
+  double Dbl = static_cast<double>(c);     // non-integer destination
+  OtherEnum Enum = static_cast<OtherEnum>(c); // enum-to-enum
+  int Un = static_cast<int>(u);            // unscoped enumeration
+  int Ok = std::to_underlying(c);          // already correct
+}
+
+// When bool is the exact underlying type, the cast is precise and flagged.
+enum class BoolEnum : bool { No, Yes };
+bool precise_bool(BoolEnum b) {
+  return static_cast<bool>(b);
+  // CHECK-MESSAGES: :[[@LINE-1]]:10: warning: use 'std::to_underlying' to 
convert a scoped enumeration to its underlying type 
[modernize-use-to-underlying]
+  // CHECK-FIXES: return std::to_underlying(b);
+}
+
+// The operand can be an arbitrary expression, which is preserved verbatim.
+int operand_expression(ColorInt a, bool cond) {
+  return static_cast<int>(cond ? a : ColorInt::Red);
+  // CHECK-MESSAGES: :[[@LINE-1]]:10: warning: use 'std::to_underlying' to 
convert a scoped enumeration to its underlying type 
[modernize-use-to-underlying]
+  // CHECK-FIXES: return std::to_underlying(cond ? a : ColorInt::Red);
+}
+
+// A typedef of a scoped enum is seen through (canonical type is the enum).
+using ColorAlias = ColorInt;
+int typedef_enum(ColorAlias c) {
+  return static_cast<int>(c);
+  // CHECK-MESSAGES: :[[@LINE-1]]:10: warning: use 'std::to_underlying' to 
convert a scoped enumeration to its underlying type 
[modernize-use-to-underlying]
+  // CHECK-FIXES: return std::to_underlying(c);
+}
+
+// The check should not fire on an uninstantiated template.
+template <typename E>
+int template_cast(E e) {
+  return static_cast<int>(e);
+}
+
+void use_template(ColorInt c) { template_cast(c); }
+
+// Casts involving macros. The fix is applied whenever the operand's spelling
+// can be recovered: at the invocation of a function-like macro whose body is
+// the whole cast, when only the destination type comes from a macro, and when
+// the operand itself is a macro.
+#define CAST_TO_INT(x) static_cast<int>(x)
+#define DEST_TYPE int
+#define RED_VALUE ColorInt::Red
+void macros(ColorInt c) {
+  int A = CAST_TO_INT(c);
+  // CHECK-MESSAGES: :[[@LINE-1]]:11: warning: use 'std::to_underlying' to 
convert a scoped enumeration to its underlying type 
[modernize-use-to-underlying]
+  // CHECK-FIXES: int A = std::to_underlying(c);
+  int B = static_cast<DEST_TYPE>(c);
+  // CHECK-MESSAGES: :[[@LINE-1]]:11: warning: use 'std::to_underlying' to 
convert a scoped enumeration to its underlying type 
[modernize-use-to-underlying]
+  // CHECK-FIXES: int B = std::to_underlying(c);
+  int C = static_cast<int>(RED_VALUE);
+  // CHECK-MESSAGES: :[[@LINE-1]]:11: warning: use 'std::to_underlying' to 
convert a scoped enumeration to its underlying type 
[modernize-use-to-underlying]
+  // CHECK-FIXES: int C = std::to_underlying(RED_VALUE);
+}

>From 90f55a6551dbfe85b99cdfd12c7ef64247c47b96 Mon Sep 17 00:00:00 2001
From: Adrian Vogelsgesang <[email protected]>
Date: Fri, 17 Jul 2026 23:31:56 +0000
Subject: [PATCH 2/4] Apply EugeneZelenko's review feedback

- Use single backticks for option values (Ignore, Warn, PreserveType,
  UseUnderlyingType, llvm, google) and for the ReplacementFunction value,
  matching the convention used by other clang-tidy option docs.
- Synchronize the Release Notes entry with the first sentence of the
  check documentation.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
---
 clang-tools-extra/docs/ReleaseNotes.rst            |  4 ++--
 .../checks/modernize/use-to-underlying.rst         | 14 +++++++-------
 2 files changed, 9 insertions(+), 9 deletions(-)

diff --git a/clang-tools-extra/docs/ReleaseNotes.rst 
b/clang-tools-extra/docs/ReleaseNotes.rst
index 6e5ceb3fe96f4..2d923aa567a91 100644
--- a/clang-tools-extra/docs/ReleaseNotes.rst
+++ b/clang-tools-extra/docs/ReleaseNotes.rst
@@ -100,8 +100,8 @@ New checks
 - New :doc:`modernize-use-to-underlying
   <clang-tidy/checks/modernize/use-to-underlying>` check.
 
-  Finds casts from a scoped enumeration to an integer type and replaces them
-  with a call to ``std::to_underlying``.
+  Finds casts from a scoped enumeration (``enum class``) to an integer type and
+  replaces them with a call to ``std::to_underlying`` (introduced in C++23).
 
 New check aliases
 ^^^^^^^^^^^^^^^^^
diff --git 
a/clang-tools-extra/docs/clang-tidy/checks/modernize/use-to-underlying.rst 
b/clang-tools-extra/docs/clang-tidy/checks/modernize/use-to-underlying.rst
index e7d1d8e3519bc..b723343cfb19e 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/modernize/use-to-underlying.rst
+++ b/clang-tools-extra/docs/clang-tidy/checks/modernize/use-to-underlying.rst
@@ -57,15 +57,15 @@ Options
    underlying type) are handled. Precise casts are always diagnosed and fixed
    regardless of this option. Possible values:
 
-   ``Ignore``
+   `Ignore`
      Do not diagnose imprecise casts.
 
-   ``Warn`` *(default)*
+   `Warn` *(default)*
      Diagnose imprecise casts but do not offer a fix-it. Neither automatic
      rewrite is applied because both change the meaning of the code in ways
      that may not be intended.
 
-   ``PreserveType``
+   `PreserveType`
      Wrap the operand in a call to the replacement function, keeping the
      original destination type:
 
@@ -73,7 +73,7 @@ Options
 
        long l = static_cast<long>(E{});  // becomes: 
static_cast<long>(std::to_underlying(E{}))
 
-   ``UseUnderlyingType``
+   `UseUnderlyingType`
      Replace the whole cast with a call to the replacement function. This
      **changes the type** of the expression from the destination type to the
      underlying type, so use it only when the wider or differently-signed
@@ -94,12 +94,12 @@ Options
 .. option:: ReplacementFunctionHeader
 
    The header to include when the replacement function is used. Defaults to
-   ``<utility>`` when :option:`ReplacementFunction` is ``std::to_underlying``,
+   ``<utility>`` when :option:`ReplacementFunction` is set to 
`std::to_underlying`,
    and is otherwise empty (no include is added). When the value is enclosed in
    angle brackets the include directive uses angle brackets, otherwise it uses
    quotes.
 
 .. option:: IncludeStyle
 
-   A string specifying which include-style is used, ``llvm`` or ``google``.
-   Default is ``llvm``.
+   A string specifying which include-style is used, `llvm` or `google`.
+   Default is `llvm`.

>From 86c242e27a15526f5965ba333de087e7c7456904 Mon Sep 17 00:00:00 2001
From: Adrian Vogelsgesang <[email protected]>
Date: Fri, 17 Jul 2026 23:54:16 +0000
Subject: [PATCH 3/4] Improve before/after examples in
 modernize-use-to-underlying docs

Split the inline "// becomes:" examples onto separate lines so the before
and after code are both readable instead of crammed into a trailing comment.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
---
 .../checks/modernize/use-to-underlying.rst           | 12 +++++++++---
 1 file changed, 9 insertions(+), 3 deletions(-)

diff --git 
a/clang-tools-extra/docs/clang-tidy/checks/modernize/use-to-underlying.rst 
b/clang-tools-extra/docs/clang-tidy/checks/modernize/use-to-underlying.rst
index b723343cfb19e..3c8742d30ecd6 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/modernize/use-to-underlying.rst
+++ b/clang-tools-extra/docs/clang-tidy/checks/modernize/use-to-underlying.rst
@@ -34,7 +34,9 @@ the enumeration. In that case the whole cast is replaced:
 
   enum class E : int {};
 
-  int i = static_cast<int>(E{});      // becomes: std::to_underlying(E{})
+  int i = static_cast<int>(E{});
+  // becomes:
+  int i = std::to_underlying(E{});
 
 A cast is *imprecise* when the destination type is an integer type other than
 the underlying type (a different width or signedness), for example
@@ -71,7 +73,9 @@ Options
 
      .. code-block:: c++
 
-       long l = static_cast<long>(E{});  // becomes: 
static_cast<long>(std::to_underlying(E{}))
+       long l = static_cast<long>(E{});
+       // becomes:
+       long l = static_cast<long>(std::to_underlying(E{}));
 
    `UseUnderlyingType`
      Replace the whole cast with a call to the replacement function. This
@@ -81,7 +85,9 @@ Options
 
      .. code-block:: c++
 
-       long l = static_cast<long>(E{});  // becomes: std::to_underlying(E{})
+       long l = static_cast<long>(E{});
+       // becomes:
+       long l = std::to_underlying(E{});
 
 .. option:: ReplacementFunction
 

>From f6ce0a0a7595c5bf6c5c0538c6b1ae53c9b97801 Mon Sep 17 00:00:00 2001
From: Adrian Vogelsgesang <[email protected]>
Date: Sat, 18 Jul 2026 08:57:54 +0000
Subject: [PATCH 4/4] [clang-tidy] Address zwuis's review comments for
 modernize-use-to-underlying

- Restore the standard plain file-header comment in the .cpp and .h files
- Drop the redundant unless(isInTemplateInstantiation()) matcher and the
  value-dependence guard; both are unnecessary under
  TK_IgnoreUnlessSpelledInSource.
- Move the integer / non-enum destination-type filtering into the AST
  matcher via hasDestinationType.
- Use clang/Tooling/FixIt.h helpers (tooling::fixit::getText and
  createReplacement) instead of Lexer::getSourceText and
  FixItHint::CreateReplacement.
- Emit a configuration diagnostic when ReplacementFunction is
  std::to_underlying but ReplacementFunctionHeader is not <utility>, and
  cover it with a test.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
---
 .../modernize/UseToUnderlyingCheck.cpp        | 44 +++++++++----------
 .../modernize/UseToUnderlyingCheck.h          |  2 +-
 .../use-to-underlying-invalid-config.cpp      | 19 ++++++++
 .../checkers/modernize/use-to-underlying.cpp  | 43 +++++++++++++++---
 4 files changed, 78 insertions(+), 30 deletions(-)
 create mode 100644 
clang-tools-extra/test/clang-tidy/checkers/modernize/use-to-underlying-invalid-config.cpp

diff --git a/clang-tools-extra/clang-tidy/modernize/UseToUnderlyingCheck.cpp 
b/clang-tools-extra/clang-tidy/modernize/UseToUnderlyingCheck.cpp
index c60ea02e316f2..33bb043f4c8a2 100644
--- a/clang-tools-extra/clang-tidy/modernize/UseToUnderlyingCheck.cpp
+++ b/clang-tools-extra/clang-tidy/modernize/UseToUnderlyingCheck.cpp
@@ -1,4 +1,4 @@
-//===--- UseToUnderlyingCheck.cpp - clang-tidy 
----------------------------===//
+//===----------------------------------------------------------------------===//
 //
 // Part of the LLVM Project, under the Apache License v2.0 with LLVM 
Exceptions.
 // See https://llvm.org/LICENSE.txt for license information.
@@ -9,7 +9,7 @@
 #include "UseToUnderlyingCheck.h"
 #include "clang/AST/ASTContext.h"
 #include "clang/ASTMatchers/ASTMatchFinder.h"
-#include "clang/Lex/Lexer.h"
+#include "clang/Tooling/FixIt.h"
 
 using namespace clang::ast_matchers;
 
@@ -46,8 +46,14 @@ UseToUnderlyingCheck::UseToUnderlyingCheck(StringRef Name,
                                                utils::IncludeSorter::IS_LLVM),
                       areDiagsSelfContained()),
       MaybeHeaderToInclude(Options.get("ReplacementFunctionHeader")) {
-  if (!MaybeHeaderToInclude && ReplacementFunction == "std::to_underlying")
-    MaybeHeaderToInclude = "<utility>";
+  if (ReplacementFunction == "std::to_underlying") {
+    if (!MaybeHeaderToInclude)
+      MaybeHeaderToInclude = "<utility>";
+    else if (*MaybeHeaderToInclude != "<utility>")
+      configurationDiag("'std::to_underlying' is declared in '<utility>', but "
+                        "'ReplacementFunctionHeader' is set to '%0'")
+          << *MaybeHeaderToInclude;
+  }
 }
 
 bool UseToUnderlyingCheck::isLanguageVersionSupported(
@@ -75,13 +81,14 @@ void 
UseToUnderlyingCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
 }
 
 void UseToUnderlyingCheck::registerMatchers(MatchFinder *Finder) {
-  // Match an explicit cast (``static_cast``, C-style or functional) whose
-  // operand is an expression of scoped-enumeration type. The destination type
-  // is validated in check() so that the same code path handles precise and
-  // imprecise conversions.
+  // Match an explicit cast (``static_cast``, C-style or functional) from a
+  // scoped enumeration to an integer type. Enum-to-enum casts and casts to a
+  // floating-point or pointer type are excluded here; whether the conversion 
is
+  // precise or imprecise is decided in check().
   Finder->addMatcher(
       explicitCastExpr(
-          unless(isInTemplateInstantiation()),
+          hasDestinationType(
+              qualType(isInteger(), unless(hasCanonicalType(enumType())))),
           hasSourceExpression(
               expr(hasType(hasCanonicalType(enumType(
                        hasDeclaration(enumDecl(isScoped()).bind("enum"))))))
@@ -95,17 +102,7 @@ void UseToUnderlyingCheck::check(const 
MatchFinder::MatchResult &Result) {
   const auto *Operand = Result.Nodes.getNodeAs<Expr>("operand");
   const auto *Enum = Result.Nodes.getNodeAs<EnumDecl>("enum");
 
-  // Ignore dependent contexts we cannot reason about reliably.
-  if (Cast->isValueDependent() || Cast->getType()->isDependentType())
-    return;
-
   const QualType DestType = Cast->getType().getCanonicalType();
-
-  // We only care about conversions to an integer type. Enum-to-enum casts and
-  // floating-point or pointer destinations are excluded.
-  if (!DestType->isIntegerType() || DestType->isEnumeralType())
-    return;
-
   const QualType UnderlyingType =
       Enum->getIntegerType().getCanonicalType().getUnqualifiedType();
   const QualType DestUnqualified = DestType.getUnqualifiedType();
@@ -130,9 +127,8 @@ void UseToUnderlyingCheck::check(const 
MatchFinder::MatchResult &Result) {
   if (!IsPrecise && ImpreciseCasts == ImpreciseCastsKind::Warn)
     return;
 
-  const StringRef OperandText = Lexer::getSourceText(
-      CharSourceRange::getTokenRange(Operand->getSourceRange()),
-      *Result.SourceManager, getLangOpts());
+  const StringRef OperandText =
+      tooling::fixit::getText(*Operand, *Result.Context);
   if (OperandText.empty())
     return;
 
@@ -145,12 +141,12 @@ void UseToUnderlyingCheck::check(const 
MatchFinder::MatchResult &Result) {
     // Replace the whole cast: ``static_cast<int>(e)`` -> ``to_underlying(e)``.
     // For an imprecise cast this changes the resulting type to the underlying
     // type.
-    Diag << FixItHint::CreateReplacement(Cast->getSourceRange(), Call);
+    Diag << tooling::fixit::createReplacement(*Cast, Call);
   } else {
     // Preserve the intended (wider or differently-signed) destination type by
     // wrapping only the operand: ``static_cast<long>(e)`` ->
     // ``static_cast<long>(to_underlying(e))``.
-    Diag << FixItHint::CreateReplacement(Operand->getSourceRange(), Call);
+    Diag << tooling::fixit::createReplacement(*Operand, Call);
   }
 
   if (MaybeHeaderToInclude)
diff --git a/clang-tools-extra/clang-tidy/modernize/UseToUnderlyingCheck.h 
b/clang-tools-extra/clang-tidy/modernize/UseToUnderlyingCheck.h
index 1692978af4ddd..990c6e2219d85 100644
--- a/clang-tools-extra/clang-tidy/modernize/UseToUnderlyingCheck.h
+++ b/clang-tools-extra/clang-tidy/modernize/UseToUnderlyingCheck.h
@@ -1,4 +1,4 @@
-//===--- UseToUnderlyingCheck.h - clang-tidy --------------------*- C++ 
-*-===//
+//===----------------------------------------------------------------------===//
 //
 // Part of the LLVM Project, under the Apache License v2.0 with LLVM 
Exceptions.
 // See https://llvm.org/LICENSE.txt for license information.
diff --git 
a/clang-tools-extra/test/clang-tidy/checkers/modernize/use-to-underlying-invalid-config.cpp
 
b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-to-underlying-invalid-config.cpp
new file mode 100644
index 0000000000000..41c1a2aed3e3f
--- /dev/null
+++ 
b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-to-underlying-invalid-config.cpp
@@ -0,0 +1,19 @@
+// RUN: %check_clang_tidy -std=c++23-or-later %s modernize-use-to-underlying 
%t -- \
+// RUN:   -config="{CheckOptions: { \
+// RUN:       modernize-use-to-underlying.ReplacementFunction: 
'std::to_underlying', \
+// RUN:       modernize-use-to-underlying.ReplacementFunctionHeader: 
'<wrong>'}}"
+
+// 'std::to_underlying' is declared in '<utility>', so configuring a different
+// header is a misconfiguration and is reported. The check nevertheless 
proceeds
+// and inserts the configured header.
+
+// CHECK-MESSAGES: warning: 'std::to_underlying' is declared in '<utility>', 
but 'ReplacementFunctionHeader' is set to '<wrong>' [clang-tidy-config]
+// CHECK-FIXES: #include <wrong>
+
+enum class E : int { A, B };
+
+int convert(E e) {
+  return static_cast<int>(e);
+  // CHECK-MESSAGES: :[[@LINE-1]]:10: warning: use 'std::to_underlying' to 
convert a scoped enumeration to its underlying type 
[modernize-use-to-underlying]
+  // CHECK-FIXES: return std::to_underlying(e);
+}
diff --git 
a/clang-tools-extra/test/clang-tidy/checkers/modernize/use-to-underlying.cpp 
b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-to-underlying.cpp
index a1ee85fcb1e53..1bc28f371785e 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/modernize/use-to-underlying.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-to-underlying.cpp
@@ -80,13 +80,46 @@ int typedef_enum(ColorAlias c) {
   // CHECK-FIXES: return std::to_underlying(c);
 }
 
-// The check should not fire on an uninstantiated template.
-template <typename E>
-int template_cast(E e) {
-  return static_cast<int>(e);
+// A cast whose operand has a dependent (template parameter) type is not a
+// scoped enumeration in the template pattern, so it is never flagged.
+template <typename T>
+int dependent_operand(T t) {
+  return static_cast<int>(t);
+}
+
+// A cast whose destination type is dependent is not a known integer type, so 
it
+// is never flagged either.
+template <typename T>
+T dependent_destination(ColorInt c) {
+  return static_cast<T>(c);
+}
+
+// A non-dependent cast inside a template is flagged and fixed on the template
+// pattern itself, even when the template is never instantiated and the cast
+// sits in a value-dependent context.
+template <int N>
+int nondependent_in_uninstantiated_template(ColorInt c) {
+  return static_cast<int>(c) + N;
+  // CHECK-MESSAGES: :[[@LINE-1]]:10: warning: use 'std::to_underlying' to 
convert a scoped enumeration to its underlying type 
[modernize-use-to-underlying]
+  // CHECK-FIXES: return std::to_underlying(c) + N;
 }
 
-void use_template(ColorInt c) { template_cast(c); }
+// A non-dependent cast that is instantiated multiple times is still flagged 
and
+// fixed exactly once (on the pattern), not once per instantiation.
+template <typename T>
+int nondependent_in_instantiated_template(ColorInt c) {
+  return static_cast<int>(c);
+  // CHECK-MESSAGES: :[[@LINE-1]]:10: warning: use 'std::to_underlying' to 
convert a scoped enumeration to its underlying type 
[modernize-use-to-underlying]
+  // CHECK-FIXES: return std::to_underlying(c);
+}
+
+void use_templates(ColorInt c) {
+  dependent_operand(c);
+  dependent_destination<int>(c);
+  dependent_destination<long>(c);
+  nondependent_in_instantiated_template<int>(c);
+  nondependent_in_instantiated_template<char>(c);
+}
 
 // Casts involving macros. The fix is applied whenever the operand's spelling
 // can be recovered: at the invocation of a function-like macro whose body is

_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to