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

>From 3828e8b91273ae8761a801c23ead155bf008d1d3 Mon Sep 17 00:00:00 2001
From: Adrian Vogelsgesang <[email protected]>
Date: Fri, 17 Jul 2026 21:37:31 +0000
Subject: [PATCH 1/7] [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.md            |   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 64f7f7d400550..64b228b7a2a0b 100644
--- a/clang-tools-extra/docs/ReleaseNotes.rst
+++ b/clang-tools-extra/docs/ReleaseNotes.rst
@@ -101,6 +101,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 :doc:`performance-expensive-value-or
   <clang-tidy/checks/performance/expensive-value-or>` check.
 
diff --git a/clang-tools-extra/docs/clang-tidy/checks/list.md 
b/clang-tools-extra/docs/clang-tidy/checks/list.md
index 12d8a48ee8d86..8255f7ddc412d 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/list.md
+++ b/clang-tools-extra/docs/clang-tidy/checks/list.md
@@ -334,6 +334,7 @@ zircon/*
 | {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 702b7f291d174d090cc3a3e7f53d93aeca568c25 Mon Sep 17 00:00:00 2001
From: Adrian Vogelsgesang <[email protected]>
Date: Fri, 17 Jul 2026 23:31:56 +0000
Subject: [PATCH 2/7] 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 64b228b7a2a0b..8aec5f84fe664 100644
--- a/clang-tools-extra/docs/ReleaseNotes.rst
+++ b/clang-tools-extra/docs/ReleaseNotes.rst
@@ -104,8 +104,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 :doc:`performance-expensive-value-or
   <clang-tidy/checks/performance/expensive-value-or>` check.
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 c8c6c34c105842c9b5ace71c6df5536b9dfb7390 Mon Sep 17 00:00:00 2001
From: Adrian Vogelsgesang <[email protected]>
Date: Fri, 17 Jul 2026 23:54:16 +0000
Subject: [PATCH 3/7] 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 d6fccc6100a088148d76d0125b62edaa92779999 Mon Sep 17 00:00:00 2001
From: Adrian Vogelsgesang <[email protected]>
Date: Sat, 18 Jul 2026 08:57:54 +0000
Subject: [PATCH 4/7] [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

>From fe0a889d851660e9aab124cbc49a5fb6dd39d76f Mon Sep 17 00:00:00 2001
From: Adrian Vogelsgesang <[email protected]>
Date: Thu, 23 Jul 2026 21:21:40 +0000
Subject: [PATCH 5/7] [clang-tidy] Address zwuis's second round of review
 comments for modernize-use-to-underlying

- Replace std::optional<StringRef> header member with a plain StringRef that
  uses "" to mean "insert no include", and rename it to 
ReplacementFunctionHeader
  (mirrors MakeSmartPtrCheck).
- Docs: turn the ImpreciseCasts values into a bullet list and use single
  backquotes for the ReplacementFunction option values.
- Tests: move the std::to_underlying stub into the shared
  Inputs/Headers/std/utility and #include <utility> instead; consolidate the
  imprecise-casts RUN suffixes to reduce duplicated CHECK lines.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
---
 .../modernize/UseToUnderlyingCheck.cpp        | 19 ++++++------
 .../modernize/UseToUnderlyingCheck.h          |  2 +-
 .../checks/modernize/use-to-underlying.rst    | 30 ++++++++-----------
 .../checkers/Inputs/Headers/std/utility       |  5 ++++
 .../use-to-underlying-imprecise-casts.cpp     | 30 +++++--------------
 .../checkers/modernize/use-to-underlying.cpp  | 10 ++-----
 6 files changed, 37 insertions(+), 59 deletions(-)

diff --git a/clang-tools-extra/clang-tidy/modernize/UseToUnderlyingCheck.cpp 
b/clang-tools-extra/clang-tidy/modernize/UseToUnderlyingCheck.cpp
index 33bb043f4c8a2..766b220e592f5 100644
--- a/clang-tools-extra/clang-tidy/modernize/UseToUnderlyingCheck.cpp
+++ b/clang-tools-extra/clang-tidy/modernize/UseToUnderlyingCheck.cpp
@@ -42,17 +42,17 @@ UseToUnderlyingCheck::UseToUnderlyingCheck(StringRef Name,
       ImpreciseCasts(Options.get("ImpreciseCasts", ImpreciseCastsKind::Warn)),
       ReplacementFunction(
           Options.get("ReplacementFunction", "std::to_underlying")),
+      ReplacementFunctionHeader(Options.get("ReplacementFunctionHeader", "")),
       IncludeInserter(Options.getLocalOrGlobal("IncludeStyle",
                                                utils::IncludeSorter::IS_LLVM),
-                      areDiagsSelfContained()),
-      MaybeHeaderToInclude(Options.get("ReplacementFunctionHeader")) {
+                      areDiagsSelfContained()) {
   if (ReplacementFunction == "std::to_underlying") {
-    if (!MaybeHeaderToInclude)
-      MaybeHeaderToInclude = "<utility>";
-    else if (*MaybeHeaderToInclude != "<utility>")
+    if (ReplacementFunctionHeader.empty())
+      ReplacementFunctionHeader = "<utility>";
+    else if (ReplacementFunctionHeader != "<utility>")
       configurationDiag("'std::to_underlying' is declared in '<utility>', but "
                         "'ReplacementFunctionHeader' is set to '%0'")
-          << *MaybeHeaderToInclude;
+          << ReplacementFunctionHeader;
   }
 }
 
@@ -76,8 +76,7 @@ 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);
+  Options.store(Opts, "ReplacementFunctionHeader", ReplacementFunctionHeader);
 }
 
 void UseToUnderlyingCheck::registerMatchers(MatchFinder *Finder) {
@@ -149,11 +148,11 @@ void UseToUnderlyingCheck::check(const 
MatchFinder::MatchResult &Result) {
     Diag << tooling::fixit::createReplacement(*Operand, Call);
   }
 
-  if (MaybeHeaderToInclude)
+  if (!ReplacementFunctionHeader.empty())
     Diag << IncludeInserter.createIncludeInsertion(
         Result.SourceManager->getFileID(
             Result.SourceManager->getExpansionLoc(Cast->getBeginLoc())),
-        *MaybeHeaderToInclude);
+        ReplacementFunctionHeader);
 }
 
 } // namespace clang::tidy::modernize
diff --git a/clang-tools-extra/clang-tidy/modernize/UseToUnderlyingCheck.h 
b/clang-tools-extra/clang-tidy/modernize/UseToUnderlyingCheck.h
index 990c6e2219d85..fd07da51ee6d3 100644
--- a/clang-tools-extra/clang-tidy/modernize/UseToUnderlyingCheck.h
+++ b/clang-tools-extra/clang-tidy/modernize/UseToUnderlyingCheck.h
@@ -51,8 +51,8 @@ class UseToUnderlyingCheck : public ClangTidyCheck {
 private:
   const ImpreciseCastsKind ImpreciseCasts;
   const StringRef ReplacementFunction;
+  StringRef ReplacementFunctionHeader;
   utils::IncludeInserter IncludeInserter;
-  std::optional<StringRef> MaybeHeaderToInclude;
 };
 
 } // namespace clang::tidy::modernize
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 3c8742d30ecd6..5f6f8b8d0dd41 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
@@ -59,17 +59,14 @@ Options
    underlying type) are handled. Precise casts are always diagnosed and fixed
    regardless of this option. Possible values:
 
-   `Ignore`
-     Do not diagnose imprecise casts.
+   - `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.
+   - `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:
+   - `PreserveType`: Wrap the operand in a call to the replacement function,
+     keeping the original destination type:
 
      .. code-block:: c++
 
@@ -77,11 +74,10 @@ Options
        // becomes:
        long l = 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:
+   - `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++
 
@@ -92,9 +88,9 @@ Options
 .. 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;
+   `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
diff --git 
a/clang-tools-extra/test/clang-tidy/checkers/Inputs/Headers/std/utility 
b/clang-tools-extra/test/clang-tidy/checkers/Inputs/Headers/std/utility
index 4c72e941dc825..144def3ae8226 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/Inputs/Headers/std/utility
+++ b/clang-tools-extra/test/clang-tidy/checkers/Inputs/Headers/std/utility
@@ -55,6 +55,11 @@ pair<T1, T2> make_pair(T1 x, T2 y) {
   return pair<T1, T2>(x, y);
 }
 
+template <typename T>
+constexpr __underlying_type(T) to_underlying(T value) noexcept {
+  return static_cast<__underlying_type(T)>(value);
+}
+
 } // namespace std
 
 #endif // _UTILITY_
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
index 88f1ed7a29d06..e9b77cc01056d 100644
--- 
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
@@ -1,26 +1,17 @@
-// RUN: %check_clang_tidy -check-suffixes=IGNORE -std=c++23-or-later %s \
+// RUN: %check_clang_tidy -check-suffixes=COMMON -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: %check_clang_tidy -check-suffixes=COMMON,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: %check_clang_tidy -check-suffixes=COMMON,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: %check_clang_tidy -check-suffixes=COMMON,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
+#include <utility>
 
 enum class E : int { A, B };
 
@@ -28,14 +19,8 @@ enum class E : int { A, B };
 // 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);
+  // CHECK-MESSAGES-COMMON: :[[@LINE-1]]:10: warning: use 'std::to_underlying' 
to convert a scoped enumeration to its underlying type 
[modernize-use-to-underlying]
+  // CHECK-FIXES-COMMON: return std::to_underlying(e);
 }
 
 // An imprecise cast:
@@ -48,7 +33,6 @@ long imprecise(E 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
index 1bc28f371785e..d7be675518942 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
@@ -1,14 +1,9 @@
 // RUN: %check_clang_tidy -std=c++23-or-later %s modernize-use-to-underlying %t
 
+// The <utility> header for std::to_underlying is not yet included, so the 
check
+// must insert it.
 // 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
@@ -54,7 +49,6 @@ void negatives(ColorInt c, Unscoped u) {
   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.

>From 0364924a31d940f0bb6fd853ba1a09040945586f Mon Sep 17 00:00:00 2001
From: Adrian Vogelsgesang <[email protected]>
Date: Fri, 24 Jul 2026 20:23:09 +0000
Subject: [PATCH 6/7] [clang-tidy] Convert modernize-use-to-underlying docs to
 Markdown

Follow the ongoing reST-to-Markdown documentation migration (llvm#201242) and
EugeneZelenko's review request. Rewrite the check documentation in MyST
Markdown: `{title}` block, `#`/`##` headings, fenced `cpp` code blocks,
`{option}` directives (with nested code blocks using a 4-backtick outer fence),
and `{option}` cross-references. list.rst already references the doc without an
extension, so no change is needed there.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
---
 .../checks/modernize/use-to-underlying.md     | 113 ++++++++++++++++++
 .../checks/modernize/use-to-underlying.rst    | 107 -----------------
 2 files changed, 113 insertions(+), 107 deletions(-)
 create mode 100644 
clang-tools-extra/docs/clang-tidy/checks/modernize/use-to-underlying.md
 delete mode 100644 
clang-tools-extra/docs/clang-tidy/checks/modernize/use-to-underlying.rst

diff --git 
a/clang-tools-extra/docs/clang-tidy/checks/modernize/use-to-underlying.md 
b/clang-tools-extra/docs/clang-tidy/checks/modernize/use-to-underlying.md
new file mode 100644
index 0000000000000..606266f327e42
--- /dev/null
+++ b/clang-tools-extra/docs/clang-tidy/checks/modernize/use-to-underlying.md
@@ -0,0 +1,113 @@
+```{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.
+
+Example:
+
+```cpp
+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. A cast is *imprecise* when the destination type is an integer
+type other than the underlying type (a different width or signedness).
+
+
+```cpp
+enum class E : int {};
+
+// precise cast
+int i = static_cast<int>(E{});
+// imprecise cast
+unsigned j = static_cast<unsigned>(E{});
+```
+
+Precise casts are always rewritten.
+
+Imprecise casts perform an additional integer conversion, so there is no
+single correct rewrite. Rewrites of imprecise casts are 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:
+
+  ```cpp
+  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 **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:
+
+  ```cpp
+  long l = static_cast<long>(E{});
+  // becomes:
+  long l = 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 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`.
+```
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
deleted file mode 100644
index 5f6f8b8d0dd41..0000000000000
--- a/clang-tools-extra/docs/clang-tidy/checks/modernize/use-to-underlying.rst
+++ /dev/null
@@ -1,107 +0,0 @@
-.. 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:
-  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
-``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:
-       long l = 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:
-       long l = 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 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`.

>From fe4c79cb66a61137a5466952f61f640236319f8f Mon Sep 17 00:00:00 2001
From: Adrian Vogelsgesang <[email protected]>
Date: Thu, 6 Aug 2026 21:32:50 +0000
Subject: [PATCH 7/7] Address comments

---
 .../modernize/UseToUnderlyingCheck.cpp        | 10 ++-----
 .../checks/modernize/use-to-underlying.md     | 27 ++++++++++---------
 .../checkers/modernize/use-to-underlying.cpp  |  1 -
 3 files changed, 16 insertions(+), 22 deletions(-)

diff --git a/clang-tools-extra/clang-tidy/modernize/UseToUnderlyingCheck.cpp 
b/clang-tools-extra/clang-tidy/modernize/UseToUnderlyingCheck.cpp
index 766b220e592f5..5d6664bb30ff9 100644
--- a/clang-tools-extra/clang-tidy/modernize/UseToUnderlyingCheck.cpp
+++ b/clang-tools-extra/clang-tidy/modernize/UseToUnderlyingCheck.cpp
@@ -113,7 +113,6 @@ void UseToUnderlyingCheck::check(const 
MatchFinder::MatchResult &Result) {
   if (DestType->isBooleanType() && !IsPrecise)
     return;
 
-  // Ignore imprecise casts if asked to do so.
   if (!IsPrecise && ImpreciseCasts == ImpreciseCastsKind::Ignore)
     return;
 
@@ -122,7 +121,6 @@ void UseToUnderlyingCheck::check(const 
MatchFinder::MatchResult &Result) {
                    "underlying type")
               << ReplacementFunction;
 
-  // In Warn mode an imprecise cast is diagnosed without a fix-it
   if (!IsPrecise && ImpreciseCasts == ImpreciseCastsKind::Warn)
     return;
 
@@ -137,14 +135,10 @@ void UseToUnderlyingCheck::check(const 
MatchFinder::MatchResult &Result) {
   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.
+    // `static_cast<long>(e)` -> `to_underlying(e)`
     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))``.
+    // `static_cast<long>(e)` -> `static_cast<long>(to_underlying(e))`
     Diag << tooling::fixit::createReplacement(*Operand, Call);
   }
 
diff --git 
a/clang-tools-extra/docs/clang-tidy/checks/modernize/use-to-underlying.md 
b/clang-tools-extra/docs/clang-tidy/checks/modernize/use-to-underlying.md
index 606266f327e42..25eb0a7df60a0 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/modernize/use-to-underlying.md
+++ b/clang-tools-extra/docs/clang-tidy/checks/modernize/use-to-underlying.md
@@ -63,9 +63,9 @@ 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.
+- `Warn`: 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:
@@ -86,24 +86,25 @@ regardless of this option. Possible values:
   // becomes:
   long l = std::to_underlying(E{});
   ```
+
+Default is `Warn`.
 ````
 
 ```{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.
+The fully qualified name of the function used in the replacement. 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. Default is `std::to_underlying`.
 ```
 
 ```{option} ReplacementFunctionHeader
 
-The header to include when the replacement function is used. Defaults to
-`<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.
+The header to include when the replacement function is used. When the value is
+enclosed in angle brackets the include directive uses angle brackets, otherwise
+it uses quotes. Default is `<utility>` when {option}`ReplacementFunction` is 
set
+to `std::to_underlying`, and otherwise empty (no include is added).
 ```
 
 ```{option} IncludeStyle
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 d7be675518942..ade399c949507 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
@@ -43,7 +43,6 @@ void imprecise(ColorInt c) {
   // 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

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

Reply via email to