[clang-tools-extra] [clang-tidy] Add `modernize-use-bit-cast` check (PR #189962)

Daniil Dudkin via cfe-commits cfe-commits at lists.llvm.org
Sun May 3 13:11:44 PDT 2026


https://github.com/unterumarmung updated https://github.com/llvm/llvm-project/pull/189962

>From 19985d20365bfd330a864be3989f37d92b1fcb50 Mon Sep 17 00:00:00 2001
From: Daniil Dudkin <unterumarmung at yandex.ru>
Date: Wed, 1 Apr 2026 16:37:47 +0300
Subject: [PATCH 01/14] [clang-tidy] Add `modernize-use-bit-cast` check

---
 .../clang-tidy/modernize/CMakeLists.txt       |   1 +
 .../modernize/ModernizeTidyModule.cpp         |   2 +
 .../clang-tidy/modernize/UseBitCastCheck.cpp  | 305 ++++++++++++++++++
 .../clang-tidy/modernize/UseBitCastCheck.h    |  44 +++
 clang-tools-extra/docs/ReleaseNotes.rst       |   6 +
 .../docs/clang-tidy/checks/list.rst           |   1 +
 .../checks/modernize/use-bit-cast.rst         |  64 ++++
 .../checkers/modernize/use-bit-cast.cpp       | 291 +++++++++++++++++
 8 files changed, 714 insertions(+)
 create mode 100644 clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp
 create mode 100644 clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.h
 create mode 100644 clang-tools-extra/docs/clang-tidy/checks/modernize/use-bit-cast.rst
 create mode 100644 clang-tools-extra/test/clang-tidy/checkers/modernize/use-bit-cast.cpp

diff --git a/clang-tools-extra/clang-tidy/modernize/CMakeLists.txt b/clang-tools-extra/clang-tidy/modernize/CMakeLists.txt
index 2c5c44db587fe..728a0b21613fd 100644
--- a/clang-tools-extra/clang-tidy/modernize/CMakeLists.txt
+++ b/clang-tools-extra/clang-tidy/modernize/CMakeLists.txt
@@ -32,6 +32,7 @@ add_clang_library(clangTidyModernizeModule STATIC
   TypeTraitsCheck.cpp
   UnaryStaticAssertCheck.cpp
   UseAutoCheck.cpp
+  UseBitCastCheck.cpp
   UseBoolLiteralsCheck.cpp
   UseConstraintsCheck.cpp
   UseDefaultMemberInitCheck.cpp
diff --git a/clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp b/clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp
index cc13da7535bcb..6e823a558c299 100644
--- a/clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp
+++ b/clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp
@@ -32,6 +32,7 @@
 #include "TypeTraitsCheck.h"
 #include "UnaryStaticAssertCheck.h"
 #include "UseAutoCheck.h"
+#include "UseBitCastCheck.h"
 #include "UseBoolLiteralsCheck.h"
 #include "UseConstraintsCheck.h"
 #include "UseDefaultMemberInitCheck.h"
@@ -88,6 +89,7 @@ class ModernizeModule : public ClangTidyModule {
     CheckFactories.registerCheck<MinMaxUseInitializerListCheck>(
         "modernize-min-max-use-initializer-list");
     CheckFactories.registerCheck<PassByValueCheck>("modernize-pass-by-value");
+    CheckFactories.registerCheck<UseBitCastCheck>("modernize-use-bit-cast");
     CheckFactories.registerCheck<UseDesignatedInitializersCheck>(
         "modernize-use-designated-initializers");
     CheckFactories.registerCheck<UseIntegerSignComparisonCheck>(
diff --git a/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp b/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp
new file mode 100644
index 0000000000000..bc6173a658890
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp
@@ -0,0 +1,305 @@
+//===----------------------------------------------------------------------===//
+//
+// 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 "UseBitCastCheck.h"
+#include "../utils/Matchers.h"
+#include "clang/AST/ASTContext.h"
+#include "clang/AST/Expr.h"
+#include "clang/AST/ExprCXX.h"
+#include "clang/AST/Type.h"
+#include "clang/ASTMatchers/ASTMatchFinder.h"
+#include "clang/Lex/Lexer.h"
+#include "llvm/ADT/STLExtras.h"
+
+using namespace clang::ast_matchers;
+
+namespace clang::tidy::modernize {
+
+static const Expr *stripMemcpyArgument(const Expr *ExprNode) {
+  ExprNode = ExprNode->IgnoreParenImpCasts();
+  while (const auto *Cast = dyn_cast<ExplicitCastExpr>(ExprNode))
+    ExprNode = Cast->getSubExpr()->IgnoreParenImpCasts();
+  return ExprNode;
+}
+
+static bool isSupportedMemcpyObjectExpr(const Expr *ExprNode) {
+  ExprNode = ExprNode->IgnoreParenImpCasts();
+
+  if (isa<DeclRefExpr>(ExprNode))
+    return true;
+
+  const auto *Member = dyn_cast<MemberExpr>(ExprNode);
+  if (!Member || !isa<FieldDecl>(Member->getMemberDecl()))
+    if (const auto *MemberPointer = dyn_cast<BinaryOperator>(ExprNode))
+      if (MemberPointer->getOpcode() == BO_PtrMemD ||
+          MemberPointer->getOpcode() == BO_PtrMemI)
+        return isSupportedMemcpyObjectExpr(MemberPointer->getLHS());
+
+  return Member && isSupportedMemcpyObjectExpr(Member->getBase());
+}
+
+static const Expr *extractMemcpyObjectExpr(const Expr *ExprNode) {
+  ExprNode = stripMemcpyArgument(ExprNode);
+  const auto *AddressOf = dyn_cast<UnaryOperator>(ExprNode);
+  if (!AddressOf || AddressOf->getOpcode() != UO_AddrOf)
+    return nullptr;
+
+  const Expr *ObjectExpr = AddressOf->getSubExpr()->IgnoreParenImpCasts();
+  return isSupportedMemcpyObjectExpr(ObjectExpr) ? ObjectExpr : nullptr;
+}
+
+static bool isSupportedMemcpyArgType(QualType Type, const ASTContext &Context,
+                                     bool RequireMutable) {
+  if (Type.isNull())
+    return false;
+
+  const QualType CanonicalType = Type.getCanonicalType().getNonReferenceType();
+  if (CanonicalType.isNull() || CanonicalType->isDependentType() ||
+      CanonicalType->isIncompleteType() ||
+      CanonicalType.isVolatileQualified() ||
+      CanonicalType->isAnyPointerType() || CanonicalType->isArrayType() ||
+      CanonicalType->isFunctionType())
+    return false;
+
+  if (RequireMutable) {
+    if (CanonicalType.isConstQualified())
+      return false;
+
+    if (const auto *Record = CanonicalType->getAsCXXRecordDecl())
+      if (!Record->hasSimpleCopyAssignment() &&
+          !Record->hasSimpleMoveAssignment())
+        return false;
+  }
+
+  return Type.getNonReferenceType().isTriviallyCopyableType(Context);
+}
+
+static bool isSameUnqualifiedCanonicalType(QualType LHS, QualType RHS) {
+  return LHS.getCanonicalType().getUnqualifiedType() ==
+         RHS.getCanonicalType().getUnqualifiedType();
+}
+
+static bool isMatchingSizeOfExpression(const Expr *SizeExpr, QualType SrcType,
+                                       QualType DstType,
+                                       const ASTContext &Context) {
+  const auto *UnaryExpr =
+      dyn_cast<UnaryExprOrTypeTraitExpr>(SizeExpr->IgnoreParenImpCasts());
+  if (!UnaryExpr || UnaryExpr->getKind() != UETT_SizeOf ||
+      SizeExpr->getBeginLoc().isMacroID())
+    return false;
+
+  const QualType SizeType = UnaryExpr->getTypeOfArgument();
+  if (SizeType.isNull())
+    return false;
+
+  const QualType SizeCanonical =
+      SizeType.getCanonicalType().getUnqualifiedType();
+  const QualType SrcCanonical = SrcType.getCanonicalType().getUnqualifiedType();
+  const QualType DstCanonical = DstType.getCanonicalType().getUnqualifiedType();
+  if (SizeCanonical != SrcCanonical && SizeCanonical != DstCanonical)
+    return false;
+
+  return Context.getTypeSizeInChars(SrcCanonical) ==
+         Context.getTypeSizeInChars(DstCanonical);
+}
+
+static bool isStatementBody(const Stmt *Current, const Stmt *Parent) {
+  if (const auto *Block = dyn_cast<CompoundStmt>(Parent))
+    return llvm::is_contained(Block->body(), Current);
+
+  if (const auto *If = dyn_cast<IfStmt>(Parent))
+    return If->getThen() == Current || If->getElse() == Current;
+  if (const auto *While = dyn_cast<WhileStmt>(Parent))
+    return While->getBody() == Current;
+  if (const auto *Do = dyn_cast<DoStmt>(Parent))
+    return Do->getBody() == Current;
+  if (const auto *For = dyn_cast<ForStmt>(Parent))
+    return For->getBody() == Current;
+  if (const auto *RangeFor = dyn_cast<CXXForRangeStmt>(Parent))
+    return RangeFor->getBody() == Current;
+  if (const auto *Label = dyn_cast<LabelStmt>(Parent))
+    return Label->getSubStmt() == Current;
+  if (const auto *Case = dyn_cast<SwitchCase>(Parent))
+    return Case->getSubStmt() == Current;
+  if (const auto *Attributed = dyn_cast<AttributedStmt>(Parent))
+    return Attributed->getSubStmt() == Current;
+
+  return false;
+}
+
+namespace {
+
+// These states describe how to spell the replacement when only the memcpy call
+// is replaced. An existing `(void)` cast is preserved by parenthesizing the
+// assignment, while comma/discarded subexpressions need an injected `(void)`.
+enum class MemcpyReplacementForm {
+  None,
+  StatementBody,
+  PreserveOuterVoidCast,
+  InjectVoidCast,
+};
+
+} // namespace
+
+static MemcpyReplacementForm getMemcpyReplacementForm(const Expr *ExprNode,
+                                                      ASTContext &Context) {
+  const Stmt *Current = ExprNode;
+  MemcpyReplacementForm Kind = MemcpyReplacementForm::StatementBody;
+
+  while (true) {
+    auto Parents = Context.getParents(*Current);
+    if (Parents.size() != 1)
+      return MemcpyReplacementForm::None;
+
+    if (const auto *ParentExpr = Parents[0].get<Expr>()) {
+      if (isa<ExprWithCleanups, ImplicitCastExpr, MaterializeTemporaryExpr,
+              CXXBindTemporaryExpr, ParenExpr>(ParentExpr)) {
+        Current = ParentExpr;
+        continue;
+      }
+
+      if (const auto *Cast = dyn_cast<CastExpr>(ParentExpr))
+        if (Cast->getCastKind() == CK_ToVoid)
+          return MemcpyReplacementForm::PreserveOuterVoidCast;
+
+      if (const auto *Comma = dyn_cast<BinaryOperator>(ParentExpr)) {
+        if (Comma->getOpcode() != BO_Comma)
+          return MemcpyReplacementForm::None;
+        if (Comma->getLHS() == Current)
+          return MemcpyReplacementForm::InjectVoidCast;
+        if (Comma->getRHS() == Current) {
+          Current = Comma;
+          Kind = MemcpyReplacementForm::InjectVoidCast;
+          continue;
+        }
+      }
+
+      return MemcpyReplacementForm::None;
+    }
+
+    const auto *ParentStmt = Parents[0].get<Stmt>();
+    if (!ParentStmt || !isStatementBody(Current, ParentStmt))
+      return MemcpyReplacementForm::None;
+    return Kind;
+  }
+}
+
+namespace {
+
+AST_MATCHER(CallExpr, isDiscardedValueContext) {
+  return getMemcpyReplacementForm(&Node, Finder->getASTContext()) !=
+         MemcpyReplacementForm::None;
+}
+
+AST_MATCHER(CallExpr, isBitCastMemcpyCandidate) {
+  if (Node.getNumArgs() != 3 || Node.getBeginLoc().isMacroID())
+    return false;
+
+  const auto *DstExpr = extractMemcpyObjectExpr(Node.getArg(0));
+  const auto *SrcExpr = extractMemcpyObjectExpr(Node.getArg(1));
+  if (!DstExpr || !SrcExpr || DstExpr->getBeginLoc().isMacroID() ||
+      SrcExpr->getBeginLoc().isMacroID())
+    return false;
+
+  const auto &Context = Finder->getASTContext();
+  const QualType DstType = DstExpr->getType().getNonReferenceType();
+  const QualType SrcType = SrcExpr->getType().getNonReferenceType();
+
+  return isSupportedMemcpyArgType(DstType, Context, /*RequireMutable=*/true) &&
+         isSupportedMemcpyArgType(SrcType, Context,
+                                  /*RequireMutable=*/false) &&
+         !isSameUnqualifiedCanonicalType(SrcType, DstType) &&
+         isMatchingSizeOfExpression(Node.getArg(2), SrcType, DstType, Context);
+}
+
+} // namespace
+
+static StringRef getSourceText(const Expr *ExprNode, const SourceManager &SM,
+                               const LangOptions &LangOpts) {
+  return Lexer::getSourceText(
+      CharSourceRange::getTokenRange(ExprNode->getSourceRange()), SM, LangOpts);
+}
+
+UseBitCastCheck::UseBitCastCheck(StringRef Name, ClangTidyContext *Context)
+    : ClangTidyCheck(Name, Context),
+      IncludeInserter(Options.getLocalOrGlobal("IncludeStyle",
+                                               utils::IncludeSorter::IS_LLVM),
+                      areDiagsSelfContained()) {}
+
+void UseBitCastCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
+  Options.store(Opts, "IncludeStyle", IncludeInserter.getStyle());
+}
+
+void UseBitCastCheck::registerPPCallbacks(const SourceManager &SM,
+                                          Preprocessor *PP,
+                                          Preprocessor *ModuleExpanderPP) {
+  IncludeInserter.registerPreprocessor(PP);
+}
+
+void UseBitCastCheck::registerMatchers(MatchFinder *Finder) {
+  Finder->addMatcher(
+      callExpr(callee(functionDecl(hasName("::memcpy"))),
+               isDiscardedValueContext(), unless(isInTemplateInstantiation()),
+               unless(hasAncestor(expr(matchers::hasUnevaluatedContext()))),
+               isBitCastMemcpyCandidate())
+          .bind("memcpy"),
+      this);
+}
+
+void UseBitCastCheck::check(const MatchFinder::MatchResult &Result) {
+  const auto *MemcpyCall = Result.Nodes.getNodeAs<CallExpr>("memcpy");
+  if (!MemcpyCall)
+    return;
+
+  const auto *DstExpr = extractMemcpyObjectExpr(MemcpyCall->getArg(0));
+  const auto *SrcExpr = extractMemcpyObjectExpr(MemcpyCall->getArg(1));
+  if (!DstExpr || !SrcExpr)
+    return;
+
+  const SourceManager &SM = *Result.SourceManager;
+  const LangOptions &LangOpts = getLangOpts();
+  StringRef DstText = getSourceText(DstExpr, SM, LangOpts);
+  StringRef SrcText = getSourceText(SrcExpr, SM, LangOpts);
+  if (DstText.empty() || SrcText.empty())
+    return;
+
+  const MemcpyReplacementForm ReplacementForm =
+      getMemcpyReplacementForm(MemcpyCall, *Result.Context);
+  if (ReplacementForm == MemcpyReplacementForm::None)
+    return;
+
+  const PrintingPolicy Policy(LangOpts);
+  const QualType DstType =
+      DstExpr->getType().getNonReferenceType().getUnqualifiedType();
+  const std::string Assignment = std::string(DstText) + " = std::bit_cast<" +
+                                 DstType.getAsString(Policy) + ">(" +
+                                 std::string(SrcText) + ")";
+  std::string Replacement = Assignment;
+  switch (ReplacementForm) {
+  case MemcpyReplacementForm::StatementBody:
+    break;
+  case MemcpyReplacementForm::PreserveOuterVoidCast:
+    Replacement = "(" + Assignment + ")";
+    break;
+  case MemcpyReplacementForm::InjectVoidCast:
+    Replacement = "(void)(" + Assignment + ")";
+    break;
+  case MemcpyReplacementForm::None:
+    return;
+  }
+
+  const DiagnosticBuilder Diag =
+      diag(MemcpyCall->getBeginLoc(),
+           "use 'std::bit_cast' instead of 'memcpy' for type punning");
+  Diag << FixItHint::CreateReplacement(MemcpyCall->getSourceRange(),
+                                       Replacement);
+  Diag << IncludeInserter.createIncludeInsertion(
+      SM.getFileID(MemcpyCall->getBeginLoc()), "<bit>");
+}
+
+} // namespace clang::tidy::modernize
diff --git a/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.h b/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.h
new file mode 100644
index 0000000000000..c4672a7321d36
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.h
@@ -0,0 +1,44 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MODERNIZE_USEBITCASTCHECK_H
+#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MODERNIZE_USEBITCASTCHECK_H
+
+#include "../ClangTidyCheck.h"
+#include "../utils/IncludeInserter.h"
+
+namespace clang::tidy::modernize {
+
+/// Finds conservative object-to-object ``memcpy`` type punning that can be
+/// expressed as ``std::bit_cast``.
+///
+/// For the user-facing documentation see:
+/// https://clang.llvm.org/extra/clang-tidy/checks/modernize/use-bit-cast.html
+class UseBitCastCheck : public ClangTidyCheck {
+public:
+  UseBitCastCheck(StringRef Name, ClangTidyContext *Context);
+
+  bool isLanguageVersionSupported(const LangOptions &LangOpts) const override {
+    return LangOpts.CPlusPlus20;
+  }
+  void registerMatchers(ast_matchers::MatchFinder *Finder) override;
+  void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
+  void registerPPCallbacks(const SourceManager &SM, Preprocessor *PP,
+                           Preprocessor *ModuleExpanderPP) override;
+  void storeOptions(ClangTidyOptions::OptionMap &Opts) override;
+  std::optional<TraversalKind> getCheckTraversalKind() const override {
+    return TK_IgnoreUnlessSpelledInSource;
+  }
+
+private:
+  utils::IncludeInserter IncludeInserter;
+};
+
+} // namespace clang::tidy::modernize
+
+#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MODERNIZE_USEBITCASTCHECK_H
diff --git a/clang-tools-extra/docs/ReleaseNotes.rst b/clang-tools-extra/docs/ReleaseNotes.rst
index c9749df481bcd..b7d7c8a1459b3 100644
--- a/clang-tools-extra/docs/ReleaseNotes.rst
+++ b/clang-tools-extra/docs/ReleaseNotes.rst
@@ -145,6 +145,12 @@ New checks
   ``llvm::to_vector(llvm::make_filter_range(...))`` that can be replaced with
   ``llvm::map_to_vector`` and ``llvm::filter_to_vector``.
 
+- New :doc:`modernize-use-bit-cast
+  <clang-tidy/checks/modernize/use-bit-cast>` check.
+
+  Finds conservative object-to-object ``memcpy`` type punning that can be
+  rewritten as ``std::bit_cast`` in C++20 and later.
+
 - New :doc:`modernize-use-std-bit
   <clang-tidy/checks/modernize/use-std-bit>` check.
 
diff --git a/clang-tools-extra/docs/clang-tidy/checks/list.rst b/clang-tools-extra/docs/clang-tidy/checks/list.rst
index 053ce6f0779d9..636fe27a347fa 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/list.rst
+++ b/clang-tools-extra/docs/clang-tidy/checks/list.rst
@@ -311,6 +311,7 @@ Clang-Tidy Checks
    :doc:`modernize-type-traits <modernize/type-traits>`, "Yes"
    :doc:`modernize-unary-static-assert <modernize/unary-static-assert>`, "Yes"
    :doc:`modernize-use-auto <modernize/use-auto>`, "Yes"
+   :doc:`modernize-use-bit-cast <modernize/use-bit-cast>`, "Yes"
    :doc:`modernize-use-bool-literals <modernize/use-bool-literals>`, "Yes"
    :doc:`modernize-use-constraints <modernize/use-constraints>`, "Yes"
    :doc:`modernize-use-default-member-init <modernize/use-default-member-init>`, "Yes"
diff --git a/clang-tools-extra/docs/clang-tidy/checks/modernize/use-bit-cast.rst b/clang-tools-extra/docs/clang-tidy/checks/modernize/use-bit-cast.rst
new file mode 100644
index 0000000000000..e847eeb656f49
--- /dev/null
+++ b/clang-tools-extra/docs/clang-tidy/checks/modernize/use-bit-cast.rst
@@ -0,0 +1,64 @@
+.. title:: clang-tidy - modernize-use-bit-cast
+
+modernize-use-bit-cast
+======================
+
+Finds conservative object-to-object ``memcpy`` type punning that can be
+rewritten as ``std::bit_cast`` in C++20 and later.
+
+The check targets the common pattern of copying the full object representation
+of one trivially copyable object into another trivially copyable object of a
+different type:
+
+.. code-block:: c++
+
+  float src = 1.0f;
+  unsigned int dst;
+  std::memcpy(&dst, &src, sizeof(src));
+
+This is rewritten to:
+
+.. code-block:: c++
+
+  float src = 1.0f;
+  unsigned int dst;
+  dst = std::bit_cast<unsigned int>(src);
+
+The fix intentionally replaces only the ``memcpy`` call. It does not fold a
+preceding declaration into ``auto dst = ...`` because doing so can change the
+construction behavior of the destination object.
+
+It only matches direct named source and destination objects, or direct
+field subobjects accessed through ``.``, ``->``, ``.*``, or ``->*``,
+and only when:
+
+* both object types are trivially copyable,
+* neither object type is a pointer, array, function type, or
+  volatile-qualified,
+* the source and destination types differ,
+* the copy size is expressed as ``sizeof(...)`` for either copied type, and
+* the ``memcpy`` call appears in a discarded-value context, such as a statement
+  body, the operand of an explicit ``(void)`` cast, or a comma subexpression
+  whose value is discarded.
+
+The check intentionally does not diagnose:
+
+* pointer punning,
+* array or buffer manipulation,
+* macro expansions,
+* dependent template cases,
+* unevaluated contexts such as ``sizeof(memcpy(...))``,
+* larger expressions where the ``memcpy`` value affects the enclosing
+  expression, such as conditions or operands of unrelated operators,
+* calls where the return value of ``memcpy`` is used, or
+* unrelated overloads such as a user-defined ``memcpy``.
+
+If needed, the fix also inserts ``#include <bit>``.
+
+Options
+-------
+
+.. 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-bit-cast.cpp b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-bit-cast.cpp
new file mode 100644
index 0000000000000..9820bf6e7987c
--- /dev/null
+++ b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-bit-cast.cpp
@@ -0,0 +1,291 @@
+// RUN: %check_clang_tidy -std=c++20-or-later %s modernize-use-bit-cast %t
+
+// CHECK-FIXES: #include <bit>
+
+void *memcpy(void *To, const void *From, unsigned long long Size);
+
+namespace std {
+using ::memcpy;
+}
+
+template <typename T>
+struct identity {
+  using type = T;
+};
+
+struct NonTrivial {
+  NonTrivial();
+  NonTrivial(const NonTrivial &);
+  int Value;
+};
+
+extern unsigned long long n;
+
+void basic_case() {
+  float src = 1.0f;
+  unsigned int dst;
+  std::memcpy(&dst, &src, sizeof(src));
+  // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: use 'std::bit_cast' instead of 'memcpy' for type punning [modernize-use-bit-cast]
+  // CHECK-FIXES: dst = std::bit_cast<unsigned int>(src);
+}
+
+void unqualified_case() {
+  float src = 1.0f;
+  unsigned int dst;
+  memcpy(&dst, &src, sizeof(dst));
+  // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+  // CHECK-FIXES: dst = std::bit_cast<unsigned int>(src);
+}
+
+void global_case() {
+  float src = 1.0f;
+  unsigned int dst;
+  ::memcpy(&dst, &src, sizeof(unsigned int));
+  // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+  // CHECK-FIXES: dst = std::bit_cast<unsigned int>(src);
+}
+
+void explicit_cast_case() {
+  float src = 1.0f;
+  unsigned int dst = 0;
+  std::memcpy(static_cast<void *>(&dst), static_cast<const void *>(&src),
+              sizeof(dst));
+  // CHECK-MESSAGES: :[[@LINE-2]]:3: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+  // CHECK-FIXES: dst = std::bit_cast<unsigned int>(src);
+}
+
+void alias_case() {
+  using U = identity<unsigned int>::type;
+  using F = identity<float>::type;
+  F src = 1.0f;
+  U dst;
+  std::memcpy(&dst, &src, sizeof(U));
+  // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+  // CHECK-FIXES: dst = std::bit_cast<U>(src);
+}
+
+void const_source_case() {
+  const float src = 1.0f;
+  unsigned int dst;
+  std::memcpy(&dst, &src, sizeof(src));
+  // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+  // CHECK-FIXES: dst = std::bit_cast<unsigned int>(src);
+}
+
+void lambda_case() {
+  auto L = [] {
+    float src = 1.0f;
+    unsigned int dst;
+    std::memcpy(&dst, &src, sizeof(src));
+    // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+    // CHECK-FIXES: dst = std::bit_cast<unsigned int>(src);
+  };
+  L();
+}
+
+void if_body_case(bool Cond) {
+  float src = 1.0f;
+  unsigned int dst;
+  if (Cond)
+    std::memcpy(&dst, &src, sizeof(src));
+  // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+  // CHECK-FIXES: if (Cond)
+  // CHECK-FIXES-NEXT: dst = std::bit_cast<unsigned int>(src);
+}
+
+void comma_lhs_case() {
+  float src = 1.0f;
+  unsigned int dst;
+  int value = (std::memcpy(&dst, &src, sizeof(src)), 42);
+  (void)value;
+  // CHECK-MESSAGES: :[[@LINE-2]]:16: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+  // CHECK-FIXES: int value = ((void)(dst = std::bit_cast<unsigned int>(src)), 42);
+}
+
+void void_cast_case() {
+  float src = 1.0f;
+  unsigned int dst;
+  (void)std::memcpy(&dst, &src, sizeof(src));
+  // CHECK-MESSAGES: :[[@LINE-1]]:9: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+  // CHECK-FIXES: (void)(dst = std::bit_cast<unsigned int>(src));
+}
+
+void same_type_case() {
+  float src = 1.0f;
+  float dst = 0.0f;
+  std::memcpy(&dst, &src, sizeof(src));
+  // CHECK-MESSAGES-NOT: :[[@LINE-1]]:3: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+}
+
+void pointer_case(int *srcp) {
+  int *dstp;
+  std::memcpy(&dstp, &srcp, sizeof(srcp));
+  // CHECK-MESSAGES-NOT: :[[@LINE-1]]:3: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+}
+
+void array_case() {
+  unsigned char bytes[sizeof(float)];
+  float src = 1.0f;
+  std::memcpy(bytes, &src, sizeof(src));
+  // CHECK-MESSAGES-NOT: :[[@LINE-1]]:3: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+}
+
+void buffer_pointer_case(float *srcp, unsigned int *dstp) {
+  std::memcpy(dstp, srcp, sizeof(*srcp));
+  // CHECK-MESSAGES-NOT: :[[@LINE-1]]:3: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+}
+
+void partial_copy_case() {
+  float src = 1.0f;
+  unsigned int dst;
+  std::memcpy(&dst, &src, 2);
+  // CHECK-MESSAGES-NOT: :[[@LINE-1]]:3: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+}
+
+void unknown_copy_case() {
+  float src = 1.0f;
+  unsigned int dst;
+  std::memcpy(&dst, &src, n);
+  // CHECK-MESSAGES-NOT: :[[@LINE-1]]:3: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+}
+
+void non_trivial_case(NonTrivial src) {
+  NonTrivial dst;
+  std::memcpy(&dst, &src, sizeof(src));
+  // CHECK-MESSAGES-NOT: :[[@LINE-1]]:3: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+}
+
+void volatile_case() {
+  volatile float src = 1.0f;
+  unsigned int dst;
+  std::memcpy(&dst, const_cast<const float *>(&src), sizeof(src));
+  // CHECK-MESSAGES-NOT: :[[@LINE-1]]:3: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+}
+
+struct Wrap {
+  float src;
+  unsigned int dst;
+};
+
+struct SourceStruct {
+  int Value;
+};
+
+struct DestStruct {
+  const int Value;
+};
+
+void member_case() {
+  Wrap W{1.0f, 0};
+  std::memcpy(&W.dst, &W.src, sizeof(W.src));
+  // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+  // CHECK-FIXES: W.dst = std::bit_cast<unsigned int>(W.src);
+}
+
+void pointer_member_case(Wrap *P) {
+  std::memcpy(&P->dst, &P->src, sizeof(P->src));
+  // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+  // CHECK-FIXES: P->dst = std::bit_cast<unsigned int>(P->src);
+}
+
+void member_pointer_case(Wrap W, float Wrap::*Src, unsigned int Wrap::*Dst) {
+  std::memcpy(&(W.*Dst), &(W.*Src), sizeof(W.*Src));
+  // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+  // CHECK-FIXES: W.*Dst = std::bit_cast<unsigned int>(W.*Src);
+}
+
+void pointer_member_pointer_case(Wrap *P, float Wrap::*Src,
+                                 unsigned int Wrap::*Dst) {
+  std::memcpy(&(P->*Dst), &(P->*Src), sizeof(P->*Src));
+  // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+  // CHECK-FIXES: P->*Dst = std::bit_cast<unsigned int>(P->*Src);
+}
+
+void builtin_case() {
+  float src = 1.0f;
+  unsigned int dst;
+  __builtin_memcpy(&dst, &src, sizeof(src));
+  // CHECK-MESSAGES-NOT: :[[@LINE-1]]:3: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+}
+
+namespace ns {
+struct A {
+  unsigned int Value;
+};
+
+struct B {
+  unsigned int Value;
+};
+
+void memcpy(B *, const A *, unsigned long long);
+
+void overload_case() {
+  A src{0};
+  B dst{0};
+  memcpy(&dst, &src, sizeof(src));
+  // CHECK-MESSAGES-NOT: :[[@LINE-1]]:3: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+}
+} // namespace ns
+
+#define DO_COPY(Dst, Src) std::memcpy(&(Dst), &(Src), sizeof(Src))
+
+void macro_case() {
+  float src = 1.0f;
+  unsigned int dst;
+  DO_COPY(dst, src);
+  // CHECK-MESSAGES-NOT: :[[@LINE-1]]:3: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+}
+
+template <typename To, typename From>
+requires(sizeof(To) == sizeof(From))
+To template_case(From src) {
+  To dst;
+  std::memcpy(&dst, &src, sizeof(src));
+  // CHECK-MESSAGES-NOT: :[[@LINE-1]]:3: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+  return dst;
+}
+
+void unevaluated_case() {
+  float src = 1.0f;
+  unsigned int dst;
+  (void)sizeof(std::memcpy(&dst, &src, sizeof(src)));
+  // CHECK-MESSAGES-NOT: :[[@LINE-1]]:16: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+}
+
+void used_return_value_case() {
+  float src = 1.0f;
+  unsigned int dst;
+  void *Ptr = std::memcpy(&dst, &src, sizeof(src));
+  (void)Ptr;
+  // CHECK-MESSAGES-NOT: :[[@LINE-2]]:15: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+}
+
+void comma_rhs_used_case() {
+  float src = 1.0f;
+  unsigned int dst;
+  void *Ptr = (0, std::memcpy(&dst, &src, sizeof(src)));
+  (void)Ptr;
+  // CHECK-MESSAGES-NOT: :[[@LINE-2]]:19: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+}
+
+void deleted_assignment_case(SourceStruct src) {
+  DestStruct dst{0};
+  std::memcpy(&dst, &src, sizeof(src));
+  // CHECK-MESSAGES-NOT: :[[@LINE-1]]:3: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+}
+
+void condition_use_case() {
+  float src = 1.0f;
+  unsigned int dst;
+  if (std::memcpy(&dst, &src, sizeof(src)))
+    (void)0;
+  // CHECK-MESSAGES-NOT: :[[@LINE-2]]:7: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+}
+
+void conditional_operand_case(bool Cond) {
+  float src = 1.0f;
+  unsigned int dst;
+  void *Ptr = nullptr;
+  (void)(Cond ? std::memcpy(&dst, &src, sizeof(src)) : Ptr);
+  // CHECK-MESSAGES-NOT: :[[@LINE-1]]:17: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+}

>From bfccbac8cc2786b75f21d9c1b802d4890a092ca3 Mon Sep 17 00:00:00 2001
From: Daniil Dudkin <unterumarmung at yandex.ru>
Date: Wed, 1 Apr 2026 23:19:35 +0300
Subject: [PATCH 02/14] fix revidew comment

---
 .../clang-tidy/modernize/UseBitCastCheck.cpp  | 50 +++++++------------
 .../checks/modernize/use-bit-cast.rst         | 12 +++--
 .../checkers/modernize/use-bit-cast.cpp       | 28 +++++++++++
 3 files changed, 53 insertions(+), 37 deletions(-)

diff --git a/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp b/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp
index bc6173a658890..6f681042b9a43 100644
--- a/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp
+++ b/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp
@@ -20,13 +20,6 @@ using namespace clang::ast_matchers;
 
 namespace clang::tidy::modernize {
 
-static const Expr *stripMemcpyArgument(const Expr *ExprNode) {
-  ExprNode = ExprNode->IgnoreParenImpCasts();
-  while (const auto *Cast = dyn_cast<ExplicitCastExpr>(ExprNode))
-    ExprNode = Cast->getSubExpr()->IgnoreParenImpCasts();
-  return ExprNode;
-}
-
 static bool isSupportedMemcpyObjectExpr(const Expr *ExprNode) {
   ExprNode = ExprNode->IgnoreParenImpCasts();
 
@@ -44,7 +37,7 @@ static bool isSupportedMemcpyObjectExpr(const Expr *ExprNode) {
 }
 
 static const Expr *extractMemcpyObjectExpr(const Expr *ExprNode) {
-  ExprNode = stripMemcpyArgument(ExprNode);
+  ExprNode = ExprNode->IgnoreParenCasts();
   const auto *AddressOf = dyn_cast<UnaryOperator>(ExprNode);
   if (!AddressOf || AddressOf->getOpcode() != UO_AddrOf)
     return nullptr;
@@ -53,30 +46,23 @@ static const Expr *extractMemcpyObjectExpr(const Expr *ExprNode) {
   return isSupportedMemcpyObjectExpr(ObjectExpr) ? ObjectExpr : nullptr;
 }
 
-static bool isSupportedMemcpyArgType(QualType Type, const ASTContext &Context,
-                                     bool RequireMutable) {
-  if (Type.isNull())
-    return false;
+static bool isBitCastableMemcpyObjectType(QualType Type,
+                                          const ASTContext &Context) {
+  Type = Type.getCanonicalType().getNonReferenceType();
+  return !Type.isNull() && !Type.isVolatileQualified() &&
+         !Type->isAnyPointerType() && !Type->isFunctionType() &&
+         Type.isTriviallyCopyableType(Context) &&
+         Type.isBitwiseCloneableType(Context);
+}
 
-  const QualType CanonicalType = Type.getCanonicalType().getNonReferenceType();
-  if (CanonicalType.isNull() || CanonicalType->isDependentType() ||
-      CanonicalType->isIncompleteType() ||
-      CanonicalType.isVolatileQualified() ||
-      CanonicalType->isAnyPointerType() || CanonicalType->isArrayType() ||
-      CanonicalType->isFunctionType())
+static bool canAssignBitCastResult(QualType Type) {
+  Type = Type.getCanonicalType().getNonReferenceType();
+  if (Type.isNull() || Type.isConstQualified() || Type->isArrayType())
     return false;
 
-  if (RequireMutable) {
-    if (CanonicalType.isConstQualified())
-      return false;
-
-    if (const auto *Record = CanonicalType->getAsCXXRecordDecl())
-      if (!Record->hasSimpleCopyAssignment() &&
-          !Record->hasSimpleMoveAssignment())
-        return false;
-  }
-
-  return Type.getNonReferenceType().isTriviallyCopyableType(Context);
+  const auto *Record = Type->getAsCXXRecordDecl();
+  return !Record || Record->hasSimpleCopyAssignment() ||
+         Record->hasSimpleMoveAssignment();
 }
 
 static bool isSameUnqualifiedCanonicalType(QualType LHS, QualType RHS) {
@@ -210,9 +196,9 @@ AST_MATCHER(CallExpr, isBitCastMemcpyCandidate) {
   const QualType DstType = DstExpr->getType().getNonReferenceType();
   const QualType SrcType = SrcExpr->getType().getNonReferenceType();
 
-  return isSupportedMemcpyArgType(DstType, Context, /*RequireMutable=*/true) &&
-         isSupportedMemcpyArgType(SrcType, Context,
-                                  /*RequireMutable=*/false) &&
+  return isBitCastableMemcpyObjectType(DstType, Context) &&
+         isBitCastableMemcpyObjectType(SrcType, Context) &&
+         canAssignBitCastResult(DstType) &&
          !isSameUnqualifiedCanonicalType(SrcType, DstType) &&
          isMatchingSizeOfExpression(Node.getArg(2), SrcType, DstType, Context);
 }
diff --git a/clang-tools-extra/docs/clang-tidy/checks/modernize/use-bit-cast.rst b/clang-tools-extra/docs/clang-tidy/checks/modernize/use-bit-cast.rst
index e847eeb656f49..682d21e77b9cc 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/modernize/use-bit-cast.rst
+++ b/clang-tools-extra/docs/clang-tidy/checks/modernize/use-bit-cast.rst
@@ -32,9 +32,11 @@ It only matches direct named source and destination objects, or direct
 field subobjects accessed through ``.``, ``->``, ``.*``, or ``->*``,
 and only when:
 
-* both object types are trivially copyable,
-* neither object type is a pointer, array, function type, or
-  volatile-qualified,
+* both object types are trivially copyable and bitwise-cloneable, and
+  neither is a pointer, function, or volatile-qualified type,
+* the destination type can be assigned from the ``std::bit_cast`` result,
+  so raw C array destinations are excluded while types such as
+  ``std::array`` are allowed,
 * the source and destination types differ,
 * the copy size is expressed as ``sizeof(...)`` for either copied type, and
 * the ``memcpy`` call appears in a discarded-value context, such as a statement
@@ -60,5 +62,5 @@ Options
 
 .. 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`.
diff --git a/clang-tools-extra/test/clang-tidy/checkers/modernize/use-bit-cast.cpp b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-bit-cast.cpp
index 9820bf6e7987c..27a5114b3e22b 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/modernize/use-bit-cast.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-bit-cast.cpp
@@ -5,6 +5,11 @@
 void *memcpy(void *To, const void *From, unsigned long long Size);
 
 namespace std {
+template <typename T, unsigned long long N>
+struct array {
+  T Storage[N];
+};
+
 using ::memcpy;
 }
 
@@ -72,6 +77,22 @@ void const_source_case() {
   // CHECK-FIXES: dst = std::bit_cast<unsigned int>(src);
 }
 
+void std_array_case() {
+  std::array<float, 1> src{{1.0f}};
+  std::array<unsigned int, 1> dst{};
+  std::memcpy(&dst, &src, sizeof(src));
+  // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+  // CHECK-FIXES: dst = std::bit_cast<std::array<unsigned int, 1>>(src);
+}
+
+void raw_array_source_case() {
+  float src[1] = {1.0f};
+  std::array<unsigned int, 1> dst{};
+  std::memcpy(&dst, &src, sizeof(src));
+  // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+  // CHECK-FIXES: dst = std::bit_cast<std::array<unsigned int, 1>>(src);
+}
+
 void lambda_case() {
   auto L = [] {
     float src = 1.0f;
@@ -130,6 +151,13 @@ void array_case() {
   // CHECK-MESSAGES-NOT: :[[@LINE-1]]:3: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
 }
 
+void raw_array_destination_case() {
+  std::array<float, 1> src{{1.0f}};
+  unsigned int dst[1];
+  std::memcpy(&dst, &src, sizeof(src));
+  // CHECK-MESSAGES-NOT: :[[@LINE-1]]:3: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+}
+
 void buffer_pointer_case(float *srcp, unsigned int *dstp) {
   std::memcpy(dstp, srcp, sizeof(*srcp));
   // CHECK-MESSAGES-NOT: :[[@LINE-1]]:3: warning: use 'std::bit_cast' instead of 'memcpy' for type punning

>From 76daad69a091b9724607df2dd9730bb483013ce9 Mon Sep 17 00:00:00 2001
From: Daniil Dudkin <unterumarmung at yandex.ru>
Date: Thu, 2 Apr 2026 19:29:57 +0300
Subject: [PATCH 03/14] Fix review comments

---
 .../clang-tidy/modernize/UseBitCastCheck.cpp  | 42 +++++++++----------
 1 file changed, 21 insertions(+), 21 deletions(-)

diff --git a/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp b/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp
index 6f681042b9a43..14e294548a0cb 100644
--- a/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp
+++ b/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp
@@ -15,6 +15,7 @@
 #include "clang/ASTMatchers/ASTMatchFinder.h"
 #include "clang/Lex/Lexer.h"
 #include "llvm/ADT/STLExtras.h"
+#include "llvm/ADT/Twine.h"
 
 using namespace clang::ast_matchers;
 
@@ -230,17 +231,15 @@ void UseBitCastCheck::registerPPCallbacks(const SourceManager &SM,
 void UseBitCastCheck::registerMatchers(MatchFinder *Finder) {
   Finder->addMatcher(
       callExpr(callee(functionDecl(hasName("::memcpy"))),
-               isDiscardedValueContext(), unless(isInTemplateInstantiation()),
-               unless(hasAncestor(expr(matchers::hasUnevaluatedContext()))),
-               isBitCastMemcpyCandidate())
+               isDiscardedValueContext(), isBitCastMemcpyCandidate(),
+               unless(hasAncestor(expr(matchers::hasUnevaluatedContext()))))
           .bind("memcpy"),
       this);
 }
 
 void UseBitCastCheck::check(const MatchFinder::MatchResult &Result) {
   const auto *MemcpyCall = Result.Nodes.getNodeAs<CallExpr>("memcpy");
-  if (!MemcpyCall)
-    return;
+  assert(MemcpyCall);
 
   const auto *DstExpr = extractMemcpyObjectExpr(MemcpyCall->getArg(0));
   const auto *SrcExpr = extractMemcpyObjectExpr(MemcpyCall->getArg(1));
@@ -262,22 +261,23 @@ void UseBitCastCheck::check(const MatchFinder::MatchResult &Result) {
   const PrintingPolicy Policy(LangOpts);
   const QualType DstType =
       DstExpr->getType().getNonReferenceType().getUnqualifiedType();
-  const std::string Assignment = std::string(DstText) + " = std::bit_cast<" +
-                                 DstType.getAsString(Policy) + ">(" +
-                                 std::string(SrcText) + ")";
-  std::string Replacement = Assignment;
-  switch (ReplacementForm) {
-  case MemcpyReplacementForm::StatementBody:
-    break;
-  case MemcpyReplacementForm::PreserveOuterVoidCast:
-    Replacement = "(" + Assignment + ")";
-    break;
-  case MemcpyReplacementForm::InjectVoidCast:
-    Replacement = "(void)(" + Assignment + ")";
-    break;
-  case MemcpyReplacementForm::None:
-    return;
-  }
+  const std::string DstTypeName = DstType.getAsString(Policy);
+  const std::string Replacement =
+      [&](const llvm::Twine &Assignment) -> std::string {
+    switch (ReplacementForm) {
+    case MemcpyReplacementForm::StatementBody:
+      return Assignment.str();
+    case MemcpyReplacementForm::PreserveOuterVoidCast:
+      return ("(" + Assignment + ")").str();
+    case MemcpyReplacementForm::InjectVoidCast:
+      return ("(void)(" + Assignment + ")").str();
+    case MemcpyReplacementForm::None:
+      return {};
+    }
+
+    return {};
+  }(llvm::Twine(DstText) + " = std::bit_cast<" + DstTypeName + ">(" + SrcText +
+                                         ")");
 
   const DiagnosticBuilder Diag =
       diag(MemcpyCall->getBeginLoc(),

>From 84f2cc791988c4cb58f25a622955f99959e65e23 Mon Sep 17 00:00:00 2001
From: Daniil Dudkin <unterumarmung at yandex.ru>
Date: Sat, 4 Apr 2026 20:58:00 +0300
Subject: [PATCH 04/14] Add use-bit-cast tests for sizeof(type) in templates

---
 .../checkers/modernize/use-bit-cast.cpp         | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/clang-tools-extra/test/clang-tidy/checkers/modernize/use-bit-cast.cpp b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-bit-cast.cpp
index 27a5114b3e22b..e62bff0445083 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/modernize/use-bit-cast.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-bit-cast.cpp
@@ -77,6 +77,14 @@ void const_source_case() {
   // CHECK-FIXES: dst = std::bit_cast<unsigned int>(src);
 }
 
+void sizeof_type_source_case() {
+  float src = 1.0f;
+  unsigned int dst;
+  std::memcpy(&dst, &src, sizeof(float));
+  // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+  // CHECK-FIXES: dst = std::bit_cast<unsigned int>(src);
+}
+
 void std_array_case() {
   std::array<float, 1> src{{1.0f}};
   std::array<unsigned int, 1> dst{};
@@ -273,6 +281,15 @@ To template_case(From src) {
   return dst;
 }
 
+template <typename T>
+void non_dependent_template_case() {
+  float src = 1.0f;
+  unsigned int dst;
+  memcpy(&dst, &src, sizeof(src));
+  // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+  // CHECK-FIXES: dst = std::bit_cast<unsigned int>(src);
+}
+
 void unevaluated_case() {
   float src = 1.0f;
   unsigned int dst;

>From d693d38dc91774f2feed86138670156d5173055e Mon Sep 17 00:00:00 2001
From: Daniil Dudkin <unterumarmung at yandex.ru>
Date: Sat, 4 Apr 2026 21:06:08 +0300
Subject: [PATCH 05/14] Bind memcpy object operands in matcher

---
 .../clang-tidy/modernize/UseBitCastCheck.cpp  | 24 +++++++++++--------
 1 file changed, 14 insertions(+), 10 deletions(-)

diff --git a/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp b/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp
index 14e294548a0cb..81035d63b6a60 100644
--- a/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp
+++ b/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp
@@ -197,11 +197,16 @@ AST_MATCHER(CallExpr, isBitCastMemcpyCandidate) {
   const QualType DstType = DstExpr->getType().getNonReferenceType();
   const QualType SrcType = SrcExpr->getType().getNonReferenceType();
 
-  return isBitCastableMemcpyObjectType(DstType, Context) &&
-         isBitCastableMemcpyObjectType(SrcType, Context) &&
-         canAssignBitCastResult(DstType) &&
-         !isSameUnqualifiedCanonicalType(SrcType, DstType) &&
-         isMatchingSizeOfExpression(Node.getArg(2), SrcType, DstType, Context);
+  if (!isBitCastableMemcpyObjectType(DstType, Context) ||
+      !isBitCastableMemcpyObjectType(SrcType, Context) ||
+      !canAssignBitCastResult(DstType) ||
+      isSameUnqualifiedCanonicalType(SrcType, DstType) ||
+      !isMatchingSizeOfExpression(Node.getArg(2), SrcType, DstType, Context))
+    return false;
+
+  Builder->setBinding("dstExpr", DynTypedNode::create(*DstExpr));
+  Builder->setBinding("srcExpr", DynTypedNode::create(*SrcExpr));
+  return true;
 }
 
 } // namespace
@@ -239,12 +244,11 @@ void UseBitCastCheck::registerMatchers(MatchFinder *Finder) {
 
 void UseBitCastCheck::check(const MatchFinder::MatchResult &Result) {
   const auto *MemcpyCall = Result.Nodes.getNodeAs<CallExpr>("memcpy");
+  const auto *DstExpr = Result.Nodes.getNodeAs<Expr>("dstExpr");
+  const auto *SrcExpr = Result.Nodes.getNodeAs<Expr>("srcExpr");
   assert(MemcpyCall);
-
-  const auto *DstExpr = extractMemcpyObjectExpr(MemcpyCall->getArg(0));
-  const auto *SrcExpr = extractMemcpyObjectExpr(MemcpyCall->getArg(1));
-  if (!DstExpr || !SrcExpr)
-    return;
+  assert(DstExpr);
+  assert(SrcExpr);
 
   const SourceManager &SM = *Result.SourceManager;
   const LangOptions &LangOpts = getLangOpts();

>From 740e6922d713ea629e2ca552fc9501522dafbde7 Mon Sep 17 00:00:00 2001
From: Daniil Dudkin <unterumarmung at yandex.ru>
Date: Sat, 4 Apr 2026 21:08:19 +0300
Subject: [PATCH 06/14] Match memcpy replacement context in the matcher

---
 .../clang-tidy/modernize/UseBitCastCheck.cpp  | 107 ++++++++----------
 .../checkers/modernize/use-bit-cast.cpp       |   2 +-
 2 files changed, 49 insertions(+), 60 deletions(-)

diff --git a/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp b/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp
index 81035d63b6a60..8a296f24afe0a 100644
--- a/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp
+++ b/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp
@@ -121,27 +121,14 @@ static bool isStatementBody(const Stmt *Current, const Stmt *Parent) {
 
 namespace {
 
-// These states describe how to spell the replacement when only the memcpy call
-// is replaced. An existing `(void)` cast is preserved by parenthesizing the
-// assignment, while comma/discarded subexpressions need an injected `(void)`.
-enum class MemcpyReplacementForm {
-  None,
-  StatementBody,
-  PreserveOuterVoidCast,
-  InjectVoidCast,
-};
-
-} // namespace
-
-static MemcpyReplacementForm getMemcpyReplacementForm(const Expr *ExprNode,
-                                                      ASTContext &Context) {
-  const Stmt *Current = ExprNode;
-  MemcpyReplacementForm Kind = MemcpyReplacementForm::StatementBody;
+AST_MATCHER(CallExpr, hasBitCastReplacementContext) {
+  const Stmt *Current = &Node;
+  const BinaryOperator *DiscardedComma = nullptr;
 
   while (true) {
-    auto Parents = Context.getParents(*Current);
+    auto Parents = Finder->getASTContext().getParents(*Current);
     if (Parents.size() != 1)
-      return MemcpyReplacementForm::None;
+      return false;
 
     if (const auto *ParentExpr = Parents[0].get<Expr>()) {
       if (isa<ExprWithCleanups, ImplicitCastExpr, MaterializeTemporaryExpr,
@@ -150,37 +137,46 @@ static MemcpyReplacementForm getMemcpyReplacementForm(const Expr *ExprNode,
         continue;
       }
 
-      if (const auto *Cast = dyn_cast<CastExpr>(ParentExpr))
-        if (Cast->getCastKind() == CK_ToVoid)
-          return MemcpyReplacementForm::PreserveOuterVoidCast;
-
-      if (const auto *Comma = dyn_cast<BinaryOperator>(ParentExpr)) {
-        if (Comma->getOpcode() != BO_Comma)
-          return MemcpyReplacementForm::None;
-        if (Comma->getLHS() == Current)
-          return MemcpyReplacementForm::InjectVoidCast;
-        if (Comma->getRHS() == Current) {
-          Current = Comma;
-          Kind = MemcpyReplacementForm::InjectVoidCast;
-          continue;
+      if (const auto *Cast = dyn_cast<CastExpr>(ParentExpr)) {
+        if (Cast->getCastKind() != CK_ToVoid)
+          return false;
+
+        if (!DiscardedComma) {
+          Builder->setBinding("replacementRoot", DynTypedNode::create(*Cast));
+          Builder->setBinding("discardedVoidCast", DynTypedNode::create(*Cast));
+          return true;
         }
+
+        Current = Cast;
+        continue;
+      }
+
+      const auto *Comma = dyn_cast<BinaryOperator>(ParentExpr);
+      if (!Comma || Comma->getOpcode() != BO_Comma)
+        return false;
+      if (Comma->getLHS() == Current) {
+        Builder->setBinding("replacementRoot", DynTypedNode::create(Node));
+        Builder->setBinding("discardedComma", DynTypedNode::create(*Comma));
+        return true;
       }
+      if (Comma->getRHS() != Current)
+        return false;
 
-      return MemcpyReplacementForm::None;
+      DiscardedComma = Comma;
+      Current = Comma;
+      continue;
     }
 
     const auto *ParentStmt = Parents[0].get<Stmt>();
     if (!ParentStmt || !isStatementBody(Current, ParentStmt))
-      return MemcpyReplacementForm::None;
-    return Kind;
-  }
-}
-
-namespace {
+      return false;
 
-AST_MATCHER(CallExpr, isDiscardedValueContext) {
-  return getMemcpyReplacementForm(&Node, Finder->getASTContext()) !=
-         MemcpyReplacementForm::None;
+    Builder->setBinding("replacementRoot", DynTypedNode::create(Node));
+    if (DiscardedComma)
+      Builder->setBinding("discardedComma",
+                          DynTypedNode::create(*DiscardedComma));
+    return true;
+  }
 }
 
 AST_MATCHER(CallExpr, isBitCastMemcpyCandidate) {
@@ -236,7 +232,7 @@ void UseBitCastCheck::registerPPCallbacks(const SourceManager &SM,
 void UseBitCastCheck::registerMatchers(MatchFinder *Finder) {
   Finder->addMatcher(
       callExpr(callee(functionDecl(hasName("::memcpy"))),
-               isDiscardedValueContext(), isBitCastMemcpyCandidate(),
+               hasBitCastReplacementContext(), isBitCastMemcpyCandidate(),
                unless(hasAncestor(expr(matchers::hasUnevaluatedContext()))))
           .bind("memcpy"),
       this);
@@ -246,9 +242,16 @@ void UseBitCastCheck::check(const MatchFinder::MatchResult &Result) {
   const auto *MemcpyCall = Result.Nodes.getNodeAs<CallExpr>("memcpy");
   const auto *DstExpr = Result.Nodes.getNodeAs<Expr>("dstExpr");
   const auto *SrcExpr = Result.Nodes.getNodeAs<Expr>("srcExpr");
+  const auto *ReplacementRoot = Result.Nodes.getNodeAs<Expr>("replacementRoot");
+  const auto *DiscardedComma =
+      Result.Nodes.getNodeAs<BinaryOperator>("discardedComma");
+  const auto *DiscardedVoidCast =
+      Result.Nodes.getNodeAs<CastExpr>("discardedVoidCast");
   assert(MemcpyCall);
   assert(DstExpr);
   assert(SrcExpr);
+  assert(ReplacementRoot);
+  assert(!DiscardedVoidCast || ReplacementRoot == DiscardedVoidCast);
 
   const SourceManager &SM = *Result.SourceManager;
   const LangOptions &LangOpts = getLangOpts();
@@ -257,36 +260,22 @@ void UseBitCastCheck::check(const MatchFinder::MatchResult &Result) {
   if (DstText.empty() || SrcText.empty())
     return;
 
-  const MemcpyReplacementForm ReplacementForm =
-      getMemcpyReplacementForm(MemcpyCall, *Result.Context);
-  if (ReplacementForm == MemcpyReplacementForm::None)
-    return;
-
   const PrintingPolicy Policy(LangOpts);
   const QualType DstType =
       DstExpr->getType().getNonReferenceType().getUnqualifiedType();
   const std::string DstTypeName = DstType.getAsString(Policy);
   const std::string Replacement =
       [&](const llvm::Twine &Assignment) -> std::string {
-    switch (ReplacementForm) {
-    case MemcpyReplacementForm::StatementBody:
-      return Assignment.str();
-    case MemcpyReplacementForm::PreserveOuterVoidCast:
-      return ("(" + Assignment + ")").str();
-    case MemcpyReplacementForm::InjectVoidCast:
+    if (DiscardedComma)
       return ("(void)(" + Assignment + ")").str();
-    case MemcpyReplacementForm::None:
-      return {};
-    }
-
-    return {};
+    return Assignment.str();
   }(llvm::Twine(DstText) + " = std::bit_cast<" + DstTypeName + ">(" + SrcText +
                                          ")");
 
   const DiagnosticBuilder Diag =
       diag(MemcpyCall->getBeginLoc(),
            "use 'std::bit_cast' instead of 'memcpy' for type punning");
-  Diag << FixItHint::CreateReplacement(MemcpyCall->getSourceRange(),
+  Diag << FixItHint::CreateReplacement(ReplacementRoot->getSourceRange(),
                                        Replacement);
   Diag << IncludeInserter.createIncludeInsertion(
       SM.getFileID(MemcpyCall->getBeginLoc()), "<bit>");
diff --git a/clang-tools-extra/test/clang-tidy/checkers/modernize/use-bit-cast.cpp b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-bit-cast.cpp
index e62bff0445083..599ceebe95f2b 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/modernize/use-bit-cast.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-bit-cast.cpp
@@ -136,7 +136,7 @@ void void_cast_case() {
   unsigned int dst;
   (void)std::memcpy(&dst, &src, sizeof(src));
   // CHECK-MESSAGES: :[[@LINE-1]]:9: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
-  // CHECK-FIXES: (void)(dst = std::bit_cast<unsigned int>(src));
+  // CHECK-FIXES: dst = std::bit_cast<unsigned int>(src);
 }
 
 void same_type_case() {

>From 142246c7fce2cbf87f5db0f22e67affe863248eb Mon Sep 17 00:00:00 2001
From: Daniil Dudkin <unterumarmung at yandex.ru>
Date: Sat, 4 Apr 2026 21:12:49 +0300
Subject: [PATCH 07/14] Clean up use-bit-cast matcher and printing

---
 .../clang-tidy/modernize/UseBitCastCheck.cpp      | 15 +++++++--------
 1 file changed, 7 insertions(+), 8 deletions(-)

diff --git a/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp b/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp
index 8a296f24afe0a..8418cf83eb8cf 100644
--- a/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp
+++ b/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp
@@ -7,7 +7,6 @@
 //===----------------------------------------------------------------------===//
 
 #include "UseBitCastCheck.h"
-#include "../utils/Matchers.h"
 #include "clang/AST/ASTContext.h"
 #include "clang/AST/Expr.h"
 #include "clang/AST/ExprCXX.h"
@@ -16,6 +15,7 @@
 #include "clang/Lex/Lexer.h"
 #include "llvm/ADT/STLExtras.h"
 #include "llvm/ADT/Twine.h"
+#include <cassert>
 
 using namespace clang::ast_matchers;
 
@@ -230,12 +230,11 @@ void UseBitCastCheck::registerPPCallbacks(const SourceManager &SM,
 }
 
 void UseBitCastCheck::registerMatchers(MatchFinder *Finder) {
-  Finder->addMatcher(
-      callExpr(callee(functionDecl(hasName("::memcpy"))),
-               hasBitCastReplacementContext(), isBitCastMemcpyCandidate(),
-               unless(hasAncestor(expr(matchers::hasUnevaluatedContext()))))
-          .bind("memcpy"),
-      this);
+  Finder->addMatcher(callExpr(callee(functionDecl(hasName("::memcpy"))),
+                              hasBitCastReplacementContext(),
+                              isBitCastMemcpyCandidate())
+                         .bind("memcpy"),
+                     this);
 }
 
 void UseBitCastCheck::check(const MatchFinder::MatchResult &Result) {
@@ -260,7 +259,7 @@ void UseBitCastCheck::check(const MatchFinder::MatchResult &Result) {
   if (DstText.empty() || SrcText.empty())
     return;
 
-  const PrintingPolicy Policy(LangOpts);
+  const PrintingPolicy &Policy = Result.Context->getPrintingPolicy();
   const QualType DstType =
       DstExpr->getType().getNonReferenceType().getUnqualifiedType();
   const std::string DstTypeName = DstType.getAsString(Policy);

>From b2dfd4f17efc8f9090abedd922e263a25a3868ce Mon Sep 17 00:00:00 2001
From: Daniil Dudkin <unterumarmung at yandex.ru>
Date: Sat, 4 Apr 2026 21:24:02 +0300
Subject: [PATCH 08/14] Add sizeof destination type coverage

---
 .../test/clang-tidy/checkers/modernize/use-bit-cast.cpp   | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/clang-tools-extra/test/clang-tidy/checkers/modernize/use-bit-cast.cpp b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-bit-cast.cpp
index 599ceebe95f2b..400337a6dfa1c 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/modernize/use-bit-cast.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-bit-cast.cpp
@@ -85,6 +85,14 @@ void sizeof_type_source_case() {
   // CHECK-FIXES: dst = std::bit_cast<unsigned int>(src);
 }
 
+void sizeof_type_destination_case() {
+  float src = 1.0f;
+  unsigned int dst;
+  std::memcpy(&dst, &src, sizeof(unsigned int));
+  // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+  // CHECK-FIXES: dst = std::bit_cast<unsigned int>(src);
+}
+
 void std_array_case() {
   std::array<float, 1> src{{1.0f}};
   std::array<unsigned int, 1> dst{};

>From 89e7cc84b5fc3f2abfbcb30e81393a6d5dc570e6 Mon Sep 17 00:00:00 2001
From: Daniil Dudkin <unterumarmung at yandex.ru>
Date: Sat, 4 Apr 2026 23:39:16 +0300
Subject: [PATCH 09/14] Fix warning

---
 clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp b/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp
index 8418cf83eb8cf..a86a2a70ae9b2 100644
--- a/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp
+++ b/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp
@@ -143,7 +143,6 @@ AST_MATCHER(CallExpr, hasBitCastReplacementContext) {
 
         if (!DiscardedComma) {
           Builder->setBinding("replacementRoot", DynTypedNode::create(*Cast));
-          Builder->setBinding("discardedVoidCast", DynTypedNode::create(*Cast));
           return true;
         }
 
@@ -244,13 +243,10 @@ void UseBitCastCheck::check(const MatchFinder::MatchResult &Result) {
   const auto *ReplacementRoot = Result.Nodes.getNodeAs<Expr>("replacementRoot");
   const auto *DiscardedComma =
       Result.Nodes.getNodeAs<BinaryOperator>("discardedComma");
-  const auto *DiscardedVoidCast =
-      Result.Nodes.getNodeAs<CastExpr>("discardedVoidCast");
   assert(MemcpyCall);
   assert(DstExpr);
   assert(SrcExpr);
   assert(ReplacementRoot);
-  assert(!DiscardedVoidCast || ReplacementRoot == DiscardedVoidCast);
 
   const SourceManager &SM = *Result.SourceManager;
   const LangOptions &LangOpts = getLangOpts();

>From 9007fc5413bc36fc28e08a9a0fb02531ddc40265 Mon Sep 17 00:00:00 2001
From: Daniil Dudkin <unterumarmung at yandex.ru>
Date: Sun, 5 Apr 2026 00:06:23 +0300
Subject: [PATCH 10/14] Simplify docs

---
 clang-tools-extra/docs/ReleaseNotes.rst       |  4 +-
 .../checks/modernize/use-bit-cast.rst         | 50 +++++++------------
 2 files changed, 20 insertions(+), 34 deletions(-)

diff --git a/clang-tools-extra/docs/ReleaseNotes.rst b/clang-tools-extra/docs/ReleaseNotes.rst
index b7d7c8a1459b3..87dbfd889d77b 100644
--- a/clang-tools-extra/docs/ReleaseNotes.rst
+++ b/clang-tools-extra/docs/ReleaseNotes.rst
@@ -148,8 +148,8 @@ New checks
 - New :doc:`modernize-use-bit-cast
   <clang-tidy/checks/modernize/use-bit-cast>` check.
 
-  Finds conservative object-to-object ``memcpy`` type punning that can be
-  rewritten as ``std::bit_cast`` in C++20 and later.
+  Finds ``memcpy``-based type punning that can be rewritten as
+  ``std::bit_cast`` in C++20 and later.
 
 - New :doc:`modernize-use-std-bit
   <clang-tidy/checks/modernize/use-std-bit>` check.
diff --git a/clang-tools-extra/docs/clang-tidy/checks/modernize/use-bit-cast.rst b/clang-tools-extra/docs/clang-tidy/checks/modernize/use-bit-cast.rst
index 682d21e77b9cc..260576944497f 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/modernize/use-bit-cast.rst
+++ b/clang-tools-extra/docs/clang-tidy/checks/modernize/use-bit-cast.rst
@@ -3,12 +3,8 @@
 modernize-use-bit-cast
 ======================
 
-Finds conservative object-to-object ``memcpy`` type punning that can be
-rewritten as ``std::bit_cast`` in C++20 and later.
-
-The check targets the common pattern of copying the full object representation
-of one trivially copyable object into another trivially copyable object of a
-different type:
+Finds ``memcpy``-based type punning that can be rewritten as ``std::bit_cast``
+in C++20 and later.
 
 .. code-block:: c++
 
@@ -24,35 +20,25 @@ This is rewritten to:
   unsigned int dst;
   dst = std::bit_cast<unsigned int>(src);
 
-The fix intentionally replaces only the ``memcpy`` call. It does not fold a
-preceding declaration into ``auto dst = ...`` because doing so can change the
-construction behavior of the destination object.
-
-It only matches direct named source and destination objects, or direct
-field subobjects accessed through ``.``, ``->``, ``.*``, or ``->*``,
-and only when:
-
-* both object types are trivially copyable and bitwise-cloneable, and
-  neither is a pointer, function, or volatile-qualified type,
-* the destination type can be assigned from the ``std::bit_cast`` result,
-  so raw C array destinations are excluded while types such as
-  ``std::array`` are allowed,
-* the source and destination types differ,
-* the copy size is expressed as ``sizeof(...)`` for either copied type, and
-* the ``memcpy`` call appears in a discarded-value context, such as a statement
-  body, the operand of an explicit ``(void)`` cast, or a comma subexpression
-  whose value is discarded.
-
-The check intentionally does not diagnose:
-
-* pointer punning,
-* array or buffer manipulation,
+The fix replaces only the ``memcpy`` call. It does not rewrite a preceding
+declaration into ``auto dst = ...``.
+
+It matches only object-to-object copies where:
+
+* both object types are trivially copyable, and neither is a pointer,
+  function, or ``volatile``-qualified type,
+* the destination can be assigned from ``std::bit_cast``, so raw C array
+  destinations are excluded,
+* the source and destination are not the same type after removing aliases and
+  cv-qualifiers,
+* the size argument is ``sizeof`` of either copied type, and
+* the ``memcpy`` result is not used.
+
+It intentionally does not diagnose:
+
 * macro expansions,
 * dependent template cases,
 * unevaluated contexts such as ``sizeof(memcpy(...))``,
-* larger expressions where the ``memcpy`` value affects the enclosing
-  expression, such as conditions or operands of unrelated operators,
-* calls where the return value of ``memcpy`` is used, or
 * unrelated overloads such as a user-defined ``memcpy``.
 
 If needed, the fix also inserts ``#include <bit>``.

>From f048ffc834b8627710e77ee6e1bb698b8d2fcc5a Mon Sep 17 00:00:00 2001
From: Daniil Dudkin <unterumarmung at yandex.ru>
Date: Sun, 5 Apr 2026 01:18:57 +0300
Subject: [PATCH 11/14] Refactor hasBitCastReplacementContext and found false
 negative

---
 .../clang-tidy/modernize/UseBitCastCheck.cpp  | 101 ++++++++++--------
 .../checkers/modernize/use-bit-cast.cpp       |   8 ++
 2 files changed, 65 insertions(+), 44 deletions(-)

diff --git a/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp b/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp
index a86a2a70ae9b2..d560d5b2f607a 100644
--- a/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp
+++ b/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp
@@ -15,7 +15,7 @@
 #include "clang/Lex/Lexer.h"
 #include "llvm/ADT/STLExtras.h"
 #include "llvm/ADT/Twine.h"
-#include <cassert>
+#include "llvm/ADT/TypeSwitch.h"
 
 using namespace clang::ast_matchers;
 
@@ -96,34 +96,55 @@ static bool isMatchingSizeOfExpression(const Expr *SizeExpr, QualType SrcType,
 }
 
 static bool isStatementBody(const Stmt *Current, const Stmt *Parent) {
-  if (const auto *Block = dyn_cast<CompoundStmt>(Parent))
-    return llvm::is_contained(Block->body(), Current);
-
-  if (const auto *If = dyn_cast<IfStmt>(Parent))
-    return If->getThen() == Current || If->getElse() == Current;
-  if (const auto *While = dyn_cast<WhileStmt>(Parent))
-    return While->getBody() == Current;
-  if (const auto *Do = dyn_cast<DoStmt>(Parent))
-    return Do->getBody() == Current;
-  if (const auto *For = dyn_cast<ForStmt>(Parent))
-    return For->getBody() == Current;
-  if (const auto *RangeFor = dyn_cast<CXXForRangeStmt>(Parent))
-    return RangeFor->getBody() == Current;
-  if (const auto *Label = dyn_cast<LabelStmt>(Parent))
-    return Label->getSubStmt() == Current;
-  if (const auto *Case = dyn_cast<SwitchCase>(Parent))
-    return Case->getSubStmt() == Current;
-  if (const auto *Attributed = dyn_cast<AttributedStmt>(Parent))
-    return Attributed->getSubStmt() == Current;
-
-  return false;
+  const auto IsCurrentBody = [Current](const Stmt *Body) {
+    if (Body == Current)
+      return true;
+
+    // IgnoreUnlessSpelledInSource can make `Current` skip over a parenthesized
+    // body expression even though the enclosing statement still stores it.
+    const auto *BodyExpr = dyn_cast_or_null<Expr>(Body);
+    return BodyExpr && BodyExpr->IgnoreParenImpCasts() == Current;
+  };
+
+  return llvm::TypeSwitch<const Stmt *, bool>(Parent)
+      .Case<CompoundStmt>([&](const CompoundStmt *Block) {
+        return llvm::any_of(Block->body(), IsCurrentBody);
+      })
+      .Case<IfStmt>([&](const IfStmt *If) {
+        return IsCurrentBody(If->getThen()) || IsCurrentBody(If->getElse());
+      })
+      .Case<WhileStmt, DoStmt, ForStmt, CXXForRangeStmt>(
+          [&](const auto *Loop) { return IsCurrentBody(Loop->getBody()); })
+      .Case<LabelStmt, SwitchCase, AttributedStmt>([&](const auto *Wrapper) {
+        return IsCurrentBody(Wrapper->getSubStmt());
+      })
+      .Default(false);
 }
 
 namespace {
 
+// Accept only discarded-value uses of the memcpy call:
+//   memcpy(...);
+//   (void)memcpy(...);
+//   (memcpy(...), rhs);
+//   (lhs, memcpy(...));    if the enclosing comma expression is discarded
+//   (void)(lhs, memcpy(...));
+// Skip transparent wrappers on the way up and reject any other parent shape.
 AST_MATCHER(CallExpr, hasBitCastReplacementContext) {
   const Stmt *Current = &Node;
-  const BinaryOperator *DiscardedComma = nullptr;
+  bool SawDiscardedCommaRHS = false;
+  const auto IsTransparentReplacementParent = [](const Expr *ExprNode) {
+    return isa<ExprWithCleanups, ImplicitCastExpr, MaterializeTemporaryExpr,
+               CXXBindTemporaryExpr, ParenExpr>(ExprNode);
+  };
+  const auto BindReplacementContext = [&](const Expr &ReplacementRoot,
+                                          const BinaryOperator *CommaLHS) {
+    Builder->setBinding("replacementRoot",
+                        DynTypedNode::create(ReplacementRoot));
+    if (CommaLHS)
+      Builder->setBinding("commaLHS", DynTypedNode::create(*CommaLHS));
+    return true;
+  };
 
   while (true) {
     auto Parents = Finder->getASTContext().getParents(*Current);
@@ -131,8 +152,7 @@ AST_MATCHER(CallExpr, hasBitCastReplacementContext) {
       return false;
 
     if (const auto *ParentExpr = Parents[0].get<Expr>()) {
-      if (isa<ExprWithCleanups, ImplicitCastExpr, MaterializeTemporaryExpr,
-              CXXBindTemporaryExpr, ParenExpr>(ParentExpr)) {
+      if (IsTransparentReplacementParent(ParentExpr)) {
         Current = ParentExpr;
         continue;
       }
@@ -140,11 +160,8 @@ AST_MATCHER(CallExpr, hasBitCastReplacementContext) {
       if (const auto *Cast = dyn_cast<CastExpr>(ParentExpr)) {
         if (Cast->getCastKind() != CK_ToVoid)
           return false;
-
-        if (!DiscardedComma) {
-          Builder->setBinding("replacementRoot", DynTypedNode::create(*Cast));
-          return true;
-        }
+        if (!SawDiscardedCommaRHS)
+          return BindReplacementContext(*Cast, nullptr);
 
         Current = Cast;
         continue;
@@ -153,15 +170,16 @@ AST_MATCHER(CallExpr, hasBitCastReplacementContext) {
       const auto *Comma = dyn_cast<BinaryOperator>(ParentExpr);
       if (!Comma || Comma->getOpcode() != BO_Comma)
         return false;
-      if (Comma->getLHS() == Current) {
-        Builder->setBinding("replacementRoot", DynTypedNode::create(Node));
-        Builder->setBinding("discardedComma", DynTypedNode::create(*Comma));
-        return true;
-      }
+      if (Comma->getLHS() == Current)
+        return BindReplacementContext(Node, Comma);
       if (Comma->getRHS() != Current)
         return false;
 
-      DiscardedComma = Comma;
+      // A memcpy on the right-hand side of `,` is safe only if the enclosing
+      // comma expression is itself discarded, so keep walking from the comma
+      // node. Inject `(void)` only if that comma expression later becomes the
+      // left-hand side of another comma.
+      SawDiscardedCommaRHS = true;
       Current = Comma;
       continue;
     }
@@ -170,11 +188,7 @@ AST_MATCHER(CallExpr, hasBitCastReplacementContext) {
     if (!ParentStmt || !isStatementBody(Current, ParentStmt))
       return false;
 
-    Builder->setBinding("replacementRoot", DynTypedNode::create(Node));
-    if (DiscardedComma)
-      Builder->setBinding("discardedComma",
-                          DynTypedNode::create(*DiscardedComma));
-    return true;
+    return BindReplacementContext(Node, nullptr);
   }
 }
 
@@ -241,8 +255,7 @@ void UseBitCastCheck::check(const MatchFinder::MatchResult &Result) {
   const auto *DstExpr = Result.Nodes.getNodeAs<Expr>("dstExpr");
   const auto *SrcExpr = Result.Nodes.getNodeAs<Expr>("srcExpr");
   const auto *ReplacementRoot = Result.Nodes.getNodeAs<Expr>("replacementRoot");
-  const auto *DiscardedComma =
-      Result.Nodes.getNodeAs<BinaryOperator>("discardedComma");
+  const auto *CommaLHS = Result.Nodes.getNodeAs<BinaryOperator>("commaLHS");
   assert(MemcpyCall);
   assert(DstExpr);
   assert(SrcExpr);
@@ -261,7 +274,7 @@ void UseBitCastCheck::check(const MatchFinder::MatchResult &Result) {
   const std::string DstTypeName = DstType.getAsString(Policy);
   const std::string Replacement =
       [&](const llvm::Twine &Assignment) -> std::string {
-    if (DiscardedComma)
+    if (CommaLHS)
       return ("(void)(" + Assignment + ")").str();
     return Assignment.str();
   }(llvm::Twine(DstText) + " = std::bit_cast<" + DstTypeName + ">(" + SrcText +
diff --git a/clang-tools-extra/test/clang-tidy/checkers/modernize/use-bit-cast.cpp b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-bit-cast.cpp
index 400337a6dfa1c..4e054ffd1d96e 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/modernize/use-bit-cast.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-bit-cast.cpp
@@ -139,6 +139,14 @@ void comma_lhs_case() {
   // CHECK-FIXES: int value = ((void)(dst = std::bit_cast<unsigned int>(src)), 42);
 }
 
+void comma_rhs_case() {
+  float src = 1.0f;
+  unsigned int dst;
+  (0, std::memcpy(&dst, &src, sizeof(src)));
+  // CHECK-MESSAGES: :[[@LINE-1]]:7: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+  // CHECK-FIXES: (0, dst = std::bit_cast<unsigned int>(src));
+}
+
 void void_cast_case() {
   float src = 1.0f;
   unsigned int dst;

>From b78524860af4fb3b382780b334073c7e0c943337 Mon Sep 17 00:00:00 2001
From: Daniil Dudkin <unterumarmung at yandex.ru>
Date: Sun, 3 May 2026 21:56:00 +0300
Subject: [PATCH 12/14] Address use-bit-cast review comments

---
 .../clang-tidy/modernize/UseBitCastCheck.cpp  | 33 ++++++++++---------
 .../checkers/modernize/use-bit-cast.cpp       | 22 +++++++++++++
 2 files changed, 39 insertions(+), 16 deletions(-)

diff --git a/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp b/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp
index d560d5b2f607a..896a7ced28848 100644
--- a/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp
+++ b/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp
@@ -14,8 +14,8 @@
 #include "clang/ASTMatchers/ASTMatchFinder.h"
 #include "clang/Lex/Lexer.h"
 #include "llvm/ADT/STLExtras.h"
-#include "llvm/ADT/Twine.h"
 #include "llvm/ADT/TypeSwitch.h"
+#include "llvm/Support/FormatVariadic.h"
 
 using namespace clang::ast_matchers;
 
@@ -27,14 +27,15 @@ static bool isSupportedMemcpyObjectExpr(const Expr *ExprNode) {
   if (isa<DeclRefExpr>(ExprNode))
     return true;
 
-  const auto *Member = dyn_cast<MemberExpr>(ExprNode);
-  if (!Member || !isa<FieldDecl>(Member->getMemberDecl()))
-    if (const auto *MemberPointer = dyn_cast<BinaryOperator>(ExprNode))
-      if (MemberPointer->getOpcode() == BO_PtrMemD ||
-          MemberPointer->getOpcode() == BO_PtrMemI)
-        return isSupportedMemcpyObjectExpr(MemberPointer->getLHS());
+  if (const auto *MemberPointer = dyn_cast<BinaryOperator>(ExprNode))
+    return MemberPointer->isPtrMemOp() &&
+           isSupportedMemcpyObjectExpr(MemberPointer->getLHS());
 
-  return Member && isSupportedMemcpyObjectExpr(Member->getBase());
+  if (const auto *Member = dyn_cast<MemberExpr>(ExprNode))
+    return isa<FieldDecl>(Member->getMemberDecl()) &&
+           isSupportedMemcpyObjectExpr(Member->getBase());
+
+  return false;
 }
 
 static const Expr *extractMemcpyObjectExpr(const Expr *ExprNode) {
@@ -51,8 +52,7 @@ static bool isBitCastableMemcpyObjectType(QualType Type,
                                           const ASTContext &Context) {
   Type = Type.getCanonicalType().getNonReferenceType();
   return !Type.isNull() && !Type.isVolatileQualified() &&
-         !Type->isAnyPointerType() && !Type->isFunctionType() &&
-         Type.isTriviallyCopyableType(Context) &&
+         !Type->isAnyPointerType() && Type.isTriviallyCopyableType(Context) &&
          Type.isBitwiseCloneableType(Context);
 }
 
@@ -272,13 +272,14 @@ void UseBitCastCheck::check(const MatchFinder::MatchResult &Result) {
   const QualType DstType =
       DstExpr->getType().getNonReferenceType().getUnqualifiedType();
   const std::string DstTypeName = DstType.getAsString(Policy);
-  const std::string Replacement =
-      [&](const llvm::Twine &Assignment) -> std::string {
+  const std::string Replacement = [&]() -> std::string {
+    std::string Assignment = llvm::formatv("{0} = std::bit_cast<{1}>({2})",
+                                           DstText, DstTypeName, SrcText)
+                                 .str();
     if (CommaLHS)
-      return ("(void)(" + Assignment + ")").str();
-    return Assignment.str();
-  }(llvm::Twine(DstText) + " = std::bit_cast<" + DstTypeName + ">(" + SrcText +
-                                         ")");
+      return llvm::formatv("(void)({0})", Assignment).str();
+    return Assignment;
+  }();
 
   const DiagnosticBuilder Diag =
       diag(MemcpyCall->getBeginLoc(),
diff --git a/clang-tools-extra/test/clang-tidy/checkers/modernize/use-bit-cast.cpp b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-bit-cast.cpp
index 4e054ffd1d96e..907745e1ff9e3 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/modernize/use-bit-cast.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-bit-cast.cpp
@@ -93,6 +93,28 @@ void sizeof_type_destination_case() {
   // CHECK-FIXES: dst = std::bit_cast<unsigned int>(src);
 }
 
+void sizeof_dereferenced_source_pointer_case() {
+  float src = 1.0f;
+  float *srcp = &src;
+  unsigned int dst;
+  std::memcpy(&dst, &src, sizeof(*srcp));
+  // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+  // CHECK-FIXES: dst = std::bit_cast<unsigned int>(src);
+  std::memcpy(&dst, srcp, sizeof(*srcp));
+  // CHECK-MESSAGES-NOT: :[[@LINE-1]]:3: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+}
+
+void sizeof_dereferenced_destination_pointer_case() {
+  float src = 1.0f;
+  unsigned int dst;
+  unsigned int *dstp = &dst;
+  std::memcpy(&dst, &src, sizeof(*dstp));
+  // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+  // CHECK-FIXES: dst = std::bit_cast<unsigned int>(src);
+  std::memcpy(dstp, &src, sizeof(*dstp));
+  // CHECK-MESSAGES-NOT: :[[@LINE-1]]:3: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+}
+
 void std_array_case() {
   std::array<float, 1> src{{1.0f}};
   std::array<unsigned int, 1> dst{};

>From e3b427df723875c8d64035900bc53a7a8603d94a Mon Sep 17 00:00:00 2001
From: Daniil Dudkin <unterumarmung at yandex.ru>
Date: Sun, 3 May 2026 23:05:06 +0300
Subject: [PATCH 13/14] Avoid unnecessary void casts in bit-cast fix-its

---
 .../clang-tidy/modernize/UseBitCastCheck.cpp  | 164 +++++++++++++++++-
 .../checkers/modernize/use-bit-cast.cpp       |  72 +++++++-
 2 files changed, 230 insertions(+), 6 deletions(-)

diff --git a/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp b/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp
index 896a7ced28848..ecdeacd61b4b6 100644
--- a/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp
+++ b/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp
@@ -8,6 +8,8 @@
 
 #include "UseBitCastCheck.h"
 #include "clang/AST/ASTContext.h"
+#include "clang/AST/DeclFriend.h"
+#include "clang/AST/DeclTemplate.h"
 #include "clang/AST/Expr.h"
 #include "clang/AST/ExprCXX.h"
 #include "clang/AST/Type.h"
@@ -66,11 +68,161 @@ static bool canAssignBitCastResult(QualType Type) {
          Record->hasSimpleMoveAssignment();
 }
 
+static QualType getUnqualifiedCanonicalNonReferenceType(QualType Type) {
+  if (Type.isNull())
+    return {};
+  return Type.getCanonicalType().getNonReferenceType().getUnqualifiedType();
+}
+
 static bool isSameUnqualifiedCanonicalType(QualType LHS, QualType RHS) {
   return LHS.getCanonicalType().getUnqualifiedType() ==
          RHS.getCanonicalType().getUnqualifiedType();
 }
 
+static bool isSameOrDerivedFrom(QualType Type, QualType Other) {
+  Type = getUnqualifiedCanonicalNonReferenceType(Type);
+  Other = getUnqualifiedCanonicalNonReferenceType(Other);
+  if (Type == Other)
+    return true;
+
+  const auto *Record = Type->getAsCXXRecordDecl();
+  const auto *OtherRecord = Other->getAsCXXRecordDecl();
+  return Record && OtherRecord && Record->hasDefinition() &&
+         OtherRecord->hasDefinition() && Record->isDerivedFrom(OtherRecord);
+}
+
+// This is only a cheap candidate search for preserving comma behavior in
+// fix-its. It intentionally does not run overload resolution for the
+// synthesized replacement expression.
+static bool canBindCommaOperand(QualType OperandType, QualType ParamType) {
+  OperandType = getUnqualifiedCanonicalNonReferenceType(OperandType);
+  ParamType = getUnqualifiedCanonicalNonReferenceType(ParamType);
+  if (OperandType.isNull() || ParamType.isNull())
+    return false;
+
+  if (OperandType->isDependentType() || ParamType->isDependentType())
+    return true;
+
+  if (isSameOrDerivedFrom(OperandType, ParamType))
+    return true;
+
+  if (OperandType->isArithmeticType() && ParamType->isArithmeticType())
+    return true;
+
+  if (OperandType->isIntegralOrEnumerationType() &&
+      ParamType->isIntegralOrEnumerationType())
+    return true;
+
+  return OperandType->isAnyPointerType() && ParamType->isAnyPointerType();
+}
+
+static bool isPotentialCommaOperatorForTypes(const FunctionDecl *Function,
+                                             QualType LHS, QualType RHS) {
+  if (!Function || Function->getOverloadedOperator() != OO_Comma)
+    return false;
+
+  if (isa<CXXMethodDecl>(Function))
+    return false;
+
+  if (Function->getNumParams() != 2)
+    return true;
+
+  return canBindCommaOperand(LHS, Function->getParamDecl(0)->getType()) &&
+         canBindCommaOperand(RHS, Function->getParamDecl(1)->getType());
+}
+
+static bool isPotentialCommaOperatorForTypes(const NamedDecl *Decl,
+                                             QualType LHS, QualType RHS) {
+  if (!Decl)
+    return false;
+
+  if (const auto *FunctionTemplate = dyn_cast<FunctionTemplateDecl>(Decl))
+    return isPotentialCommaOperatorForTypes(
+        FunctionTemplate->getTemplatedDecl(), LHS, RHS);
+  return isPotentialCommaOperatorForTypes(Decl->getAsFunction(), LHS, RHS);
+}
+
+static bool hasPotentialNamespaceCommaOperator(const DeclContext *Context,
+                                               const ASTContext &ASTContext,
+                                               QualType LHS, QualType RHS) {
+  if (!Context)
+    return false;
+
+  const DeclarationName CommaOperatorName =
+      ASTContext.DeclarationNames.getCXXOperatorName(OO_Comma);
+  for (; Context; Context = Context->getParent()) {
+    if (!isa<NamespaceDecl, TranslationUnitDecl>(Context))
+      continue;
+
+    for (const NamedDecl *Decl :
+         Context->getRedeclContext()->lookup(CommaOperatorName)) {
+      if (isPotentialCommaOperatorForTypes(Decl, LHS, RHS))
+        return true;
+    }
+  }
+
+  return false;
+}
+
+static const CXXRecordDecl *getDefinition(const CXXRecordDecl *Record) {
+  if (!Record)
+    return nullptr;
+  if (const auto *Definition = Record->getDefinition())
+    return Definition;
+  return Record;
+}
+
+static bool hasMemberCommaOperator(const CXXRecordDecl *Record) {
+  Record = getDefinition(Record);
+  return Record &&
+         llvm::any_of(Record->methods(), [](const CXXMethodDecl *Method) {
+           return Method->getOverloadedOperator() == OO_Comma;
+         });
+}
+
+static bool hasPotentialFriendCommaOperator(const CXXRecordDecl *Record,
+                                            QualType LHS, QualType RHS) {
+  Record = getDefinition(Record);
+  return Record &&
+         llvm::any_of(Record->friends(), [&](const FriendDecl *Friend) {
+           return isPotentialCommaOperatorForTypes(Friend->getFriendDecl(), LHS,
+                                                   RHS);
+         });
+}
+
+static bool mayFindAssociatedCommaOperator(QualType Type,
+                                           const ASTContext &ASTContext,
+                                           QualType LHS, QualType RHS,
+                                           bool IncludeMemberOperators) {
+  Type = getUnqualifiedCanonicalNonReferenceType(Type);
+  if (Type.isNull())
+    return false;
+
+  if (const auto *Record = Type->getAsCXXRecordDecl())
+    return (IncludeMemberOperators && hasMemberCommaOperator(Record)) ||
+           hasPotentialFriendCommaOperator(Record, LHS, RHS) ||
+           hasPotentialNamespaceCommaOperator(Record->getDeclContext(),
+                                              ASTContext, LHS, RHS);
+
+  if (const auto *Enum = Type->getAs<EnumType>())
+    return hasPotentialNamespaceCommaOperator(Enum->getDecl()->getDeclContext(),
+                                              ASTContext, LHS, RHS);
+
+  return false;
+}
+
+static bool mayCallOverloadedComma(QualType AssignmentType,
+                                   const Expr *CommaRHS,
+                                   const ASTContext &ASTContext) {
+  const QualType CommaRHSType = CommaRHS ? CommaRHS->getType() : QualType();
+  return mayFindAssociatedCommaOperator(AssignmentType, ASTContext,
+                                        AssignmentType, CommaRHSType,
+                                        /*IncludeMemberOperators=*/true) ||
+         mayFindAssociatedCommaOperator(CommaRHSType, ASTContext,
+                                        AssignmentType, CommaRHSType,
+                                        /*IncludeMemberOperators=*/false);
+}
+
 static bool isMatchingSizeOfExpression(const Expr *SizeExpr, QualType SrcType,
                                        QualType DstType,
                                        const ASTContext &Context) {
@@ -138,11 +290,11 @@ AST_MATCHER(CallExpr, hasBitCastReplacementContext) {
                CXXBindTemporaryExpr, ParenExpr>(ExprNode);
   };
   const auto BindReplacementContext = [&](const Expr &ReplacementRoot,
-                                          const BinaryOperator *CommaLHS) {
+                                          const BinaryOperator *CommaContext) {
     Builder->setBinding("replacementRoot",
                         DynTypedNode::create(ReplacementRoot));
-    if (CommaLHS)
-      Builder->setBinding("commaLHS", DynTypedNode::create(*CommaLHS));
+    if (CommaContext)
+      Builder->setBinding("commaContext", DynTypedNode::create(*CommaContext));
     return true;
   };
 
@@ -255,7 +407,8 @@ void UseBitCastCheck::check(const MatchFinder::MatchResult &Result) {
   const auto *DstExpr = Result.Nodes.getNodeAs<Expr>("dstExpr");
   const auto *SrcExpr = Result.Nodes.getNodeAs<Expr>("srcExpr");
   const auto *ReplacementRoot = Result.Nodes.getNodeAs<Expr>("replacementRoot");
-  const auto *CommaLHS = Result.Nodes.getNodeAs<BinaryOperator>("commaLHS");
+  const auto *CommaContext =
+      Result.Nodes.getNodeAs<BinaryOperator>("commaContext");
   assert(MemcpyCall);
   assert(DstExpr);
   assert(SrcExpr);
@@ -276,7 +429,8 @@ void UseBitCastCheck::check(const MatchFinder::MatchResult &Result) {
     std::string Assignment = llvm::formatv("{0} = std::bit_cast<{1}>({2})",
                                            DstText, DstTypeName, SrcText)
                                  .str();
-    if (CommaLHS)
+    if (CommaContext && mayCallOverloadedComma(DstType, CommaContext->getRHS(),
+                                               *Result.Context))
       return llvm::formatv("(void)({0})", Assignment).str();
     return Assignment;
   }();
diff --git a/clang-tools-extra/test/clang-tidy/checkers/modernize/use-bit-cast.cpp b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-bit-cast.cpp
index 907745e1ff9e3..01a55772417d7 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/modernize/use-bit-cast.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-bit-cast.cpp
@@ -158,7 +158,77 @@ void comma_lhs_case() {
   int value = (std::memcpy(&dst, &src, sizeof(src)), 42);
   (void)value;
   // CHECK-MESSAGES: :[[@LINE-2]]:16: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
-  // CHECK-FIXES: int value = ((void)(dst = std::bit_cast<unsigned int>(src)), 42);
+  // CHECK-FIXES: int value = (dst = std::bit_cast<unsigned int>(src), 42);
+}
+
+struct SimpleCommaDst {
+  unsigned int Value;
+};
+
+void comma_lhs_simple_record_case() {
+  unsigned int src = 1;
+  SimpleCommaDst dst{};
+  int value = (std::memcpy(&dst, &src, sizeof(src)), 42);
+  (void)value;
+  // CHECK-MESSAGES: :[[@LINE-2]]:16: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+  // CHECK-FIXES: int value = (dst = std::bit_cast<SimpleCommaDst>(src), 42);
+}
+
+struct MemberCommaDst {
+  unsigned int Value;
+  int operator,(int) const;
+};
+
+void comma_lhs_member_operator_case() {
+  unsigned int src = 1;
+  MemberCommaDst dst{};
+  int value = (std::memcpy(&dst, &src, sizeof(src)), 42);
+  (void)value;
+  // CHECK-MESSAGES: :[[@LINE-2]]:16: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+  // CHECK-FIXES: int value = ((void)(dst = std::bit_cast<MemberCommaDst>(src)), 42);
+}
+
+struct NamespaceCommaDst {
+  unsigned int Value;
+};
+
+int operator,(NamespaceCommaDst, int);
+
+void comma_lhs_namespace_operator_case() {
+  unsigned int src = 1;
+  NamespaceCommaDst dst{};
+  int value = (std::memcpy(&dst, &src, sizeof(src)), 42);
+  (void)value;
+  // CHECK-MESSAGES: :[[@LINE-2]]:16: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+  // CHECK-FIXES: int value = ((void)(dst = std::bit_cast<NamespaceCommaDst>(src)), 42);
+}
+
+enum class CommaEnum : unsigned int {};
+
+int operator,(CommaEnum, int);
+
+void comma_lhs_enum_operator_case() {
+  unsigned int src = 1;
+  CommaEnum dst{};
+  int value = (std::memcpy(&dst, &src, sizeof(src)), 42);
+  (void)value;
+  // CHECK-MESSAGES: :[[@LINE-2]]:16: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+  // CHECK-FIXES: int value = ((void)(dst = std::bit_cast<CommaEnum>(src)), 42);
+}
+
+namespace comma_rhs_operator {
+struct Token {};
+
+int operator,(unsigned int, Token);
+} // namespace comma_rhs_operator
+
+void comma_lhs_rhs_operator_case() {
+  float src = 1.0f;
+  unsigned int dst;
+  comma_rhs_operator::Token token;
+  (std::memcpy(&dst, &src, sizeof(src)), token);
+  // CHECK-MESSAGES: :[[@LINE-1]]:4: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
+  // CHECK-FIXES: ((void)(dst = std::bit_cast<unsigned int>(src)), token);
 }
 
 void comma_rhs_case() {

>From 3dafe58135d0dd22d044860f9180f289ec0dcbf8 Mon Sep 17 00:00:00 2001
From: Daniil Dudkin <unterumarmung at yandex.ru>
Date: Sun, 3 May 2026 23:10:12 +0300
Subject: [PATCH 14/14] Revert "Avoid unnecessary void casts in bit-cast
 fix-its"

This reverts commit c686a0ef9f6597bf701b464eae31fd2ecdcdf119.
---
 .../clang-tidy/modernize/UseBitCastCheck.cpp  | 164 +-----------------
 .../checkers/modernize/use-bit-cast.cpp       |  72 +-------
 2 files changed, 6 insertions(+), 230 deletions(-)

diff --git a/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp b/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp
index ecdeacd61b4b6..896a7ced28848 100644
--- a/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp
+++ b/clang-tools-extra/clang-tidy/modernize/UseBitCastCheck.cpp
@@ -8,8 +8,6 @@
 
 #include "UseBitCastCheck.h"
 #include "clang/AST/ASTContext.h"
-#include "clang/AST/DeclFriend.h"
-#include "clang/AST/DeclTemplate.h"
 #include "clang/AST/Expr.h"
 #include "clang/AST/ExprCXX.h"
 #include "clang/AST/Type.h"
@@ -68,161 +66,11 @@ static bool canAssignBitCastResult(QualType Type) {
          Record->hasSimpleMoveAssignment();
 }
 
-static QualType getUnqualifiedCanonicalNonReferenceType(QualType Type) {
-  if (Type.isNull())
-    return {};
-  return Type.getCanonicalType().getNonReferenceType().getUnqualifiedType();
-}
-
 static bool isSameUnqualifiedCanonicalType(QualType LHS, QualType RHS) {
   return LHS.getCanonicalType().getUnqualifiedType() ==
          RHS.getCanonicalType().getUnqualifiedType();
 }
 
-static bool isSameOrDerivedFrom(QualType Type, QualType Other) {
-  Type = getUnqualifiedCanonicalNonReferenceType(Type);
-  Other = getUnqualifiedCanonicalNonReferenceType(Other);
-  if (Type == Other)
-    return true;
-
-  const auto *Record = Type->getAsCXXRecordDecl();
-  const auto *OtherRecord = Other->getAsCXXRecordDecl();
-  return Record && OtherRecord && Record->hasDefinition() &&
-         OtherRecord->hasDefinition() && Record->isDerivedFrom(OtherRecord);
-}
-
-// This is only a cheap candidate search for preserving comma behavior in
-// fix-its. It intentionally does not run overload resolution for the
-// synthesized replacement expression.
-static bool canBindCommaOperand(QualType OperandType, QualType ParamType) {
-  OperandType = getUnqualifiedCanonicalNonReferenceType(OperandType);
-  ParamType = getUnqualifiedCanonicalNonReferenceType(ParamType);
-  if (OperandType.isNull() || ParamType.isNull())
-    return false;
-
-  if (OperandType->isDependentType() || ParamType->isDependentType())
-    return true;
-
-  if (isSameOrDerivedFrom(OperandType, ParamType))
-    return true;
-
-  if (OperandType->isArithmeticType() && ParamType->isArithmeticType())
-    return true;
-
-  if (OperandType->isIntegralOrEnumerationType() &&
-      ParamType->isIntegralOrEnumerationType())
-    return true;
-
-  return OperandType->isAnyPointerType() && ParamType->isAnyPointerType();
-}
-
-static bool isPotentialCommaOperatorForTypes(const FunctionDecl *Function,
-                                             QualType LHS, QualType RHS) {
-  if (!Function || Function->getOverloadedOperator() != OO_Comma)
-    return false;
-
-  if (isa<CXXMethodDecl>(Function))
-    return false;
-
-  if (Function->getNumParams() != 2)
-    return true;
-
-  return canBindCommaOperand(LHS, Function->getParamDecl(0)->getType()) &&
-         canBindCommaOperand(RHS, Function->getParamDecl(1)->getType());
-}
-
-static bool isPotentialCommaOperatorForTypes(const NamedDecl *Decl,
-                                             QualType LHS, QualType RHS) {
-  if (!Decl)
-    return false;
-
-  if (const auto *FunctionTemplate = dyn_cast<FunctionTemplateDecl>(Decl))
-    return isPotentialCommaOperatorForTypes(
-        FunctionTemplate->getTemplatedDecl(), LHS, RHS);
-  return isPotentialCommaOperatorForTypes(Decl->getAsFunction(), LHS, RHS);
-}
-
-static bool hasPotentialNamespaceCommaOperator(const DeclContext *Context,
-                                               const ASTContext &ASTContext,
-                                               QualType LHS, QualType RHS) {
-  if (!Context)
-    return false;
-
-  const DeclarationName CommaOperatorName =
-      ASTContext.DeclarationNames.getCXXOperatorName(OO_Comma);
-  for (; Context; Context = Context->getParent()) {
-    if (!isa<NamespaceDecl, TranslationUnitDecl>(Context))
-      continue;
-
-    for (const NamedDecl *Decl :
-         Context->getRedeclContext()->lookup(CommaOperatorName)) {
-      if (isPotentialCommaOperatorForTypes(Decl, LHS, RHS))
-        return true;
-    }
-  }
-
-  return false;
-}
-
-static const CXXRecordDecl *getDefinition(const CXXRecordDecl *Record) {
-  if (!Record)
-    return nullptr;
-  if (const auto *Definition = Record->getDefinition())
-    return Definition;
-  return Record;
-}
-
-static bool hasMemberCommaOperator(const CXXRecordDecl *Record) {
-  Record = getDefinition(Record);
-  return Record &&
-         llvm::any_of(Record->methods(), [](const CXXMethodDecl *Method) {
-           return Method->getOverloadedOperator() == OO_Comma;
-         });
-}
-
-static bool hasPotentialFriendCommaOperator(const CXXRecordDecl *Record,
-                                            QualType LHS, QualType RHS) {
-  Record = getDefinition(Record);
-  return Record &&
-         llvm::any_of(Record->friends(), [&](const FriendDecl *Friend) {
-           return isPotentialCommaOperatorForTypes(Friend->getFriendDecl(), LHS,
-                                                   RHS);
-         });
-}
-
-static bool mayFindAssociatedCommaOperator(QualType Type,
-                                           const ASTContext &ASTContext,
-                                           QualType LHS, QualType RHS,
-                                           bool IncludeMemberOperators) {
-  Type = getUnqualifiedCanonicalNonReferenceType(Type);
-  if (Type.isNull())
-    return false;
-
-  if (const auto *Record = Type->getAsCXXRecordDecl())
-    return (IncludeMemberOperators && hasMemberCommaOperator(Record)) ||
-           hasPotentialFriendCommaOperator(Record, LHS, RHS) ||
-           hasPotentialNamespaceCommaOperator(Record->getDeclContext(),
-                                              ASTContext, LHS, RHS);
-
-  if (const auto *Enum = Type->getAs<EnumType>())
-    return hasPotentialNamespaceCommaOperator(Enum->getDecl()->getDeclContext(),
-                                              ASTContext, LHS, RHS);
-
-  return false;
-}
-
-static bool mayCallOverloadedComma(QualType AssignmentType,
-                                   const Expr *CommaRHS,
-                                   const ASTContext &ASTContext) {
-  const QualType CommaRHSType = CommaRHS ? CommaRHS->getType() : QualType();
-  return mayFindAssociatedCommaOperator(AssignmentType, ASTContext,
-                                        AssignmentType, CommaRHSType,
-                                        /*IncludeMemberOperators=*/true) ||
-         mayFindAssociatedCommaOperator(CommaRHSType, ASTContext,
-                                        AssignmentType, CommaRHSType,
-                                        /*IncludeMemberOperators=*/false);
-}
-
 static bool isMatchingSizeOfExpression(const Expr *SizeExpr, QualType SrcType,
                                        QualType DstType,
                                        const ASTContext &Context) {
@@ -290,11 +138,11 @@ AST_MATCHER(CallExpr, hasBitCastReplacementContext) {
                CXXBindTemporaryExpr, ParenExpr>(ExprNode);
   };
   const auto BindReplacementContext = [&](const Expr &ReplacementRoot,
-                                          const BinaryOperator *CommaContext) {
+                                          const BinaryOperator *CommaLHS) {
     Builder->setBinding("replacementRoot",
                         DynTypedNode::create(ReplacementRoot));
-    if (CommaContext)
-      Builder->setBinding("commaContext", DynTypedNode::create(*CommaContext));
+    if (CommaLHS)
+      Builder->setBinding("commaLHS", DynTypedNode::create(*CommaLHS));
     return true;
   };
 
@@ -407,8 +255,7 @@ void UseBitCastCheck::check(const MatchFinder::MatchResult &Result) {
   const auto *DstExpr = Result.Nodes.getNodeAs<Expr>("dstExpr");
   const auto *SrcExpr = Result.Nodes.getNodeAs<Expr>("srcExpr");
   const auto *ReplacementRoot = Result.Nodes.getNodeAs<Expr>("replacementRoot");
-  const auto *CommaContext =
-      Result.Nodes.getNodeAs<BinaryOperator>("commaContext");
+  const auto *CommaLHS = Result.Nodes.getNodeAs<BinaryOperator>("commaLHS");
   assert(MemcpyCall);
   assert(DstExpr);
   assert(SrcExpr);
@@ -429,8 +276,7 @@ void UseBitCastCheck::check(const MatchFinder::MatchResult &Result) {
     std::string Assignment = llvm::formatv("{0} = std::bit_cast<{1}>({2})",
                                            DstText, DstTypeName, SrcText)
                                  .str();
-    if (CommaContext && mayCallOverloadedComma(DstType, CommaContext->getRHS(),
-                                               *Result.Context))
+    if (CommaLHS)
       return llvm::formatv("(void)({0})", Assignment).str();
     return Assignment;
   }();
diff --git a/clang-tools-extra/test/clang-tidy/checkers/modernize/use-bit-cast.cpp b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-bit-cast.cpp
index 01a55772417d7..907745e1ff9e3 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/modernize/use-bit-cast.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-bit-cast.cpp
@@ -158,77 +158,7 @@ void comma_lhs_case() {
   int value = (std::memcpy(&dst, &src, sizeof(src)), 42);
   (void)value;
   // CHECK-MESSAGES: :[[@LINE-2]]:16: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
-  // CHECK-FIXES: int value = (dst = std::bit_cast<unsigned int>(src), 42);
-}
-
-struct SimpleCommaDst {
-  unsigned int Value;
-};
-
-void comma_lhs_simple_record_case() {
-  unsigned int src = 1;
-  SimpleCommaDst dst{};
-  int value = (std::memcpy(&dst, &src, sizeof(src)), 42);
-  (void)value;
-  // CHECK-MESSAGES: :[[@LINE-2]]:16: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
-  // CHECK-FIXES: int value = (dst = std::bit_cast<SimpleCommaDst>(src), 42);
-}
-
-struct MemberCommaDst {
-  unsigned int Value;
-  int operator,(int) const;
-};
-
-void comma_lhs_member_operator_case() {
-  unsigned int src = 1;
-  MemberCommaDst dst{};
-  int value = (std::memcpy(&dst, &src, sizeof(src)), 42);
-  (void)value;
-  // CHECK-MESSAGES: :[[@LINE-2]]:16: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
-  // CHECK-FIXES: int value = ((void)(dst = std::bit_cast<MemberCommaDst>(src)), 42);
-}
-
-struct NamespaceCommaDst {
-  unsigned int Value;
-};
-
-int operator,(NamespaceCommaDst, int);
-
-void comma_lhs_namespace_operator_case() {
-  unsigned int src = 1;
-  NamespaceCommaDst dst{};
-  int value = (std::memcpy(&dst, &src, sizeof(src)), 42);
-  (void)value;
-  // CHECK-MESSAGES: :[[@LINE-2]]:16: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
-  // CHECK-FIXES: int value = ((void)(dst = std::bit_cast<NamespaceCommaDst>(src)), 42);
-}
-
-enum class CommaEnum : unsigned int {};
-
-int operator,(CommaEnum, int);
-
-void comma_lhs_enum_operator_case() {
-  unsigned int src = 1;
-  CommaEnum dst{};
-  int value = (std::memcpy(&dst, &src, sizeof(src)), 42);
-  (void)value;
-  // CHECK-MESSAGES: :[[@LINE-2]]:16: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
-  // CHECK-FIXES: int value = ((void)(dst = std::bit_cast<CommaEnum>(src)), 42);
-}
-
-namespace comma_rhs_operator {
-struct Token {};
-
-int operator,(unsigned int, Token);
-} // namespace comma_rhs_operator
-
-void comma_lhs_rhs_operator_case() {
-  float src = 1.0f;
-  unsigned int dst;
-  comma_rhs_operator::Token token;
-  (std::memcpy(&dst, &src, sizeof(src)), token);
-  // CHECK-MESSAGES: :[[@LINE-1]]:4: warning: use 'std::bit_cast' instead of 'memcpy' for type punning
-  // CHECK-FIXES: ((void)(dst = std::bit_cast<unsigned int>(src)), token);
+  // CHECK-FIXES: int value = ((void)(dst = std::bit_cast<unsigned int>(src)), 42);
 }
 
 void comma_rhs_case() {



More information about the cfe-commits mailing list