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
