[clang-tools-extra] [clang-tidy]add new check bugprone-unintended-char-ostream-output (PR #127720)

Congcong Cai via cfe-commits cfe-commits at lists.llvm.org
Wed Feb 19 01:54:57 PST 2025


https://github.com/HerrCai0907 updated https://github.com/llvm/llvm-project/pull/127720

>From b69bb465a24f2175f2f9f91f220252d3bcb27bde Mon Sep 17 00:00:00 2001
From: Congcong Cai <congcongcai0907 at 163.com>
Date: Wed, 19 Feb 2025 07:38:37 +0800
Subject: [PATCH 1/3] [clang-tidy]add new check
 bugprone-unintended-char-ostream-output

It wants to find unintended character output from  and  to an ostream.
e.g.
uint8_t v = 9;
std::cout << v;
---
 .../bugprone/BugproneTidyModule.cpp           |  3 +
 .../clang-tidy/bugprone/CMakeLists.txt        |  1 +
 .../UnintendedCharOstreamOutputCheck.cpp      | 70 +++++++++++++++++++
 .../UnintendedCharOstreamOutputCheck.h        | 34 +++++++++
 clang-tools-extra/docs/ReleaseNotes.rst       |  6 ++
 .../unintended-char-ostream-output.rst        | 30 ++++++++
 .../docs/clang-tidy/checks/list.rst           |  1 +
 .../unintended-char-ostream-output.cpp        | 70 +++++++++++++++++++
 8 files changed, 215 insertions(+)
 create mode 100644 clang-tools-extra/clang-tidy/bugprone/UnintendedCharOstreamOutputCheck.cpp
 create mode 100644 clang-tools-extra/clang-tidy/bugprone/UnintendedCharOstreamOutputCheck.h
 create mode 100644 clang-tools-extra/docs/clang-tidy/checks/bugprone/unintended-char-ostream-output.rst
 create mode 100644 clang-tools-extra/test/clang-tidy/checkers/bugprone/unintended-char-ostream-output.cpp

diff --git a/clang-tools-extra/clang-tidy/bugprone/BugproneTidyModule.cpp b/clang-tools-extra/clang-tidy/bugprone/BugproneTidyModule.cpp
index c5f0b5b28418f..0a3376949b6e5 100644
--- a/clang-tools-extra/clang-tidy/bugprone/BugproneTidyModule.cpp
+++ b/clang-tools-extra/clang-tidy/bugprone/BugproneTidyModule.cpp
@@ -90,6 +90,7 @@
 #include "UndelegatedConstructorCheck.h"
 #include "UnhandledExceptionAtNewCheck.h"
 #include "UnhandledSelfAssignmentCheck.h"
+#include "UnintendedCharOstreamOutputCheck.h"
 #include "UniquePtrArrayMismatchCheck.h"
 #include "UnsafeFunctionsCheck.h"
 #include "UnusedLocalNonTrivialVariableCheck.h"
@@ -147,6 +148,8 @@ class BugproneModule : public ClangTidyModule {
         "bugprone-incorrect-enable-if");
     CheckFactories.registerCheck<IncorrectEnableSharedFromThisCheck>(
         "bugprone-incorrect-enable-shared-from-this");
+    CheckFactories.registerCheck<UnintendedCharOstreamOutputCheck>(
+        "bugprone-unintended-char-ostream-output");
     CheckFactories.registerCheck<ReturnConstRefFromParameterCheck>(
         "bugprone-return-const-ref-from-parameter");
     CheckFactories.registerCheck<SwitchMissingDefaultCaseCheck>(
diff --git a/clang-tools-extra/clang-tidy/bugprone/CMakeLists.txt b/clang-tools-extra/clang-tidy/bugprone/CMakeLists.txt
index e8309c68b7fca..9758d7259bf65 100644
--- a/clang-tools-extra/clang-tidy/bugprone/CMakeLists.txt
+++ b/clang-tools-extra/clang-tidy/bugprone/CMakeLists.txt
@@ -29,6 +29,7 @@ add_clang_library(clangTidyBugproneModule STATIC
   InaccurateEraseCheck.cpp
   IncorrectEnableIfCheck.cpp
   IncorrectEnableSharedFromThisCheck.cpp
+  UnintendedCharOstreamOutputCheck.cpp
   ReturnConstRefFromParameterCheck.cpp
   SuspiciousStringviewDataUsageCheck.cpp
   SwitchMissingDefaultCaseCheck.cpp
diff --git a/clang-tools-extra/clang-tidy/bugprone/UnintendedCharOstreamOutputCheck.cpp b/clang-tools-extra/clang-tidy/bugprone/UnintendedCharOstreamOutputCheck.cpp
new file mode 100644
index 0000000000000..7c54ef1486b2f
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/bugprone/UnintendedCharOstreamOutputCheck.cpp
@@ -0,0 +1,70 @@
+//===--- UnintendedCharOstreamOutputCheck.cpp - clang-tidy
+//---------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "UnintendedCharOstreamOutputCheck.h"
+#include "clang/AST/Type.h"
+#include "clang/ASTMatchers/ASTMatchFinder.h"
+#include "clang/ASTMatchers/ASTMatchers.h"
+
+using namespace clang::ast_matchers;
+
+namespace clang::tidy::bugprone {
+
+namespace {
+
+// check if the type is unsigned char or signed char
+AST_MATCHER(Type, isNumericChar) {
+  const auto *BT = dyn_cast<BuiltinType>(&Node);
+  if (BT == nullptr)
+    return false;
+  const BuiltinType::Kind K = BT->getKind();
+  return K == BuiltinType::UChar || K == BuiltinType::SChar;
+}
+
+// check if the type is char
+AST_MATCHER(Type, isChar) {
+  const auto *BT = dyn_cast<BuiltinType>(&Node);
+  if (BT == nullptr)
+    return false;
+  const BuiltinType::Kind K = BT->getKind();
+  return K == BuiltinType::Char_U || K == BuiltinType::Char_S;
+}
+
+} // namespace
+
+void UnintendedCharOstreamOutputCheck::registerMatchers(MatchFinder *Finder) {
+  auto BasicOstream =
+      cxxRecordDecl(hasName("::std::basic_ostream"),
+                    // only basic_ostream<char, Traits> has overload operator<<
+                    // with char / unsigned char / signed char
+                    classTemplateSpecializationDecl(
+                        hasTemplateArgument(0, refersToType(isChar()))));
+  Finder->addMatcher(
+      cxxOperatorCallExpr(
+          hasOverloadedOperatorName("<<"),
+          hasLHS(hasType(hasUnqualifiedDesugaredType(
+              recordType(hasDeclaration(cxxRecordDecl(
+                  anyOf(BasicOstream, isDerivedFrom(BasicOstream)))))))),
+          hasRHS(hasType(hasUnqualifiedDesugaredType(isNumericChar()))))
+          .bind("x"),
+      this);
+}
+
+void UnintendedCharOstreamOutputCheck::check(
+    const MatchFinder::MatchResult &Result) {
+  const auto *Call = Result.Nodes.getNodeAs<CXXOperatorCallExpr>("x");
+  const Expr *Value = Call->getArg(1);
+  diag(Call->getOperatorLoc(),
+       "(%0 passed to 'operator<<' outputs as character instead of integer. "
+       "cast to 'unsigned' to print numeric value or cast to 'char' to print "
+       "as character)")
+      << Value->getType() << Value->getSourceRange();
+}
+
+} // namespace clang::tidy::bugprone
diff --git a/clang-tools-extra/clang-tidy/bugprone/UnintendedCharOstreamOutputCheck.h b/clang-tools-extra/clang-tidy/bugprone/UnintendedCharOstreamOutputCheck.h
new file mode 100644
index 0000000000000..071baac3216c0
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/bugprone/UnintendedCharOstreamOutputCheck.h
@@ -0,0 +1,34 @@
+//===--- UnintendedCharOstreamOutputCheck.h - clang-tidy --------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_BUGPRONE_UNINTENDEDCHAROSTREAMOUTPUTCHECK_H
+#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_BUGPRONE_UNINTENDEDCHAROSTREAMOUTPUTCHECK_H
+
+#include "../ClangTidyCheck.h"
+
+namespace clang::tidy::bugprone {
+
+/// Finds unintended character output from `unsigned char` and `signed char` to
+/// an ostream.
+///
+/// For the user-facing documentation see:
+/// http://clang.llvm.org/extra/clang-tidy/checks/bugprone/unintended-char-ostream-output.html
+class UnintendedCharOstreamOutputCheck : public ClangTidyCheck {
+public:
+  UnintendedCharOstreamOutputCheck(StringRef Name, ClangTidyContext *Context)
+      : ClangTidyCheck(Name, Context) {}
+  void registerMatchers(ast_matchers::MatchFinder *Finder) override;
+  void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
+  bool isLanguageVersionSupported(const LangOptions &LangOpts) const override {
+    return LangOpts.CPlusPlus;
+  }
+};
+
+} // namespace clang::tidy::bugprone
+
+#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_BUGPRONE_UNINTENDEDCHAROSTREAMOUTPUTCHECK_H
diff --git a/clang-tools-extra/docs/ReleaseNotes.rst b/clang-tools-extra/docs/ReleaseNotes.rst
index 6b8fe22242417..57f37c8e02e2e 100644
--- a/clang-tools-extra/docs/ReleaseNotes.rst
+++ b/clang-tools-extra/docs/ReleaseNotes.rst
@@ -91,6 +91,12 @@ Improvements to clang-tidy
 New checks
 ^^^^^^^^^^
 
+- New :doc:`bugprone-unintended-char-ostream-output
+  <clang-tidy/checks/bugprone/unintended-char-ostream-output>` check.
+
+  Finds unintended character output from `unsigned char` and `signed char` to an
+  ostream.
+
 New check aliases
 ^^^^^^^^^^^^^^^^^
 
diff --git a/clang-tools-extra/docs/clang-tidy/checks/bugprone/unintended-char-ostream-output.rst b/clang-tools-extra/docs/clang-tidy/checks/bugprone/unintended-char-ostream-output.rst
new file mode 100644
index 0000000000000..1e60698a5d445
--- /dev/null
+++ b/clang-tools-extra/docs/clang-tidy/checks/bugprone/unintended-char-ostream-output.rst
@@ -0,0 +1,30 @@
+.. title:: clang-tidy - bugprone-unintended-char-ostream-output
+
+bugprone-unintended-char-ostream-output
+=======================================
+
+Finds unintended character output from `unsigned char` and `signed char` to an
+``ostream``.
+
+Normally, when ``unsigned char (uint8_t)`` or ``signed char (int8_t)`` is used, it
+is more likely a number than a character. However, when it is passed directly to
+``std::ostream``'s ``operator<<``, resulting in character-based output instead
+of numeric value. This often contradicts the developer's intent to print
+integer values.
+
+.. code-block:: c++
+
+    uint8_t v = 9;
+    std::cout << v; // output '\t' instead of '9'
+
+It could be fixed as
+
+.. code-block:: c++
+
+    std::cout << (uint32_t)v;
+
+Or cast to char to explicitly indicate the intent
+
+.. code-block:: c++
+
+    std::cout << (char)v;
diff --git a/clang-tools-extra/docs/clang-tidy/checks/list.rst b/clang-tools-extra/docs/clang-tidy/checks/list.rst
index 7b9b905ef7671..9306dfe95fa45 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/list.rst
+++ b/clang-tools-extra/docs/clang-tidy/checks/list.rst
@@ -158,6 +158,7 @@ Clang-Tidy Checks
    :doc:`bugprone-undelegated-constructor <bugprone/undelegated-constructor>`,
    :doc:`bugprone-unhandled-exception-at-new <bugprone/unhandled-exception-at-new>`,
    :doc:`bugprone-unhandled-self-assignment <bugprone/unhandled-self-assignment>`,
+   :doc:`bugprone-unintended-char-ostream-output <bugprone/unintended-char-ostream-output>`,
    :doc:`bugprone-unique-ptr-array-mismatch <bugprone/unique-ptr-array-mismatch>`, "Yes"
    :doc:`bugprone-unsafe-functions <bugprone/unsafe-functions>`,
    :doc:`bugprone-unused-local-non-trivial-variable <bugprone/unused-local-non-trivial-variable>`,
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unintended-char-ostream-output.cpp b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unintended-char-ostream-output.cpp
new file mode 100644
index 0000000000000..fd2382dcd730c
--- /dev/null
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unintended-char-ostream-output.cpp
@@ -0,0 +1,70 @@
+// RUN: %check_clang_tidy %s bugprone-unintended-char-ostream-output %t
+
+namespace std {
+
+template <class _CharT, class _Traits = void> class basic_ostream {
+public:
+  basic_ostream &operator<<(int);
+  basic_ostream &operator<<(unsigned int);
+};
+
+template <class CharT, class Traits>
+basic_ostream<CharT, Traits> &operator<<(basic_ostream<CharT, Traits> &, CharT);
+template <class CharT, class Traits>
+basic_ostream<CharT, Traits> &operator<<(basic_ostream<CharT, Traits> &, char);
+template <class _Traits>
+basic_ostream<char, _Traits> &operator<<(basic_ostream<char, _Traits> &, char);
+template <class _Traits>
+basic_ostream<char, _Traits> &operator<<(basic_ostream<char, _Traits> &,
+                                          signed char);
+template <class _Traits>
+basic_ostream<char, _Traits> &operator<<(basic_ostream<char, _Traits> &,
+                                          unsigned char);
+
+using ostream = basic_ostream<char>;
+
+
+} // namespace std
+
+class A : public std::ostream {};
+
+void origin_ostream(std::ostream &os) {
+  unsigned char unsigned_value = 9;
+  os << unsigned_value;
+  // CHECK-MESSAGES: [[@LINE-1]]:6: warning: ('unsigned char' passed to
+  // 'operator<<' outputs as character instead of integer
+
+  signed char signed_value = 9;
+  os << signed_value;
+  // CHECK-MESSAGES: [[@LINE-1]]:6: warning: ('signed char' passed to
+  // 'operator<<' outputs as character instead of integer
+
+  char char_value = 9;
+  os << char_value;
+}
+
+void based_on_ostream(A &os) {
+  unsigned char unsigned_value = 9;
+  os << unsigned_value;
+  // CHECK-MESSAGES: [[@LINE-1]]:6: warning: ('unsigned char' passed to
+  // 'operator<<' outputs as character instead of integer
+
+  signed char signed_value = 9;
+  os << signed_value;
+  // CHECK-MESSAGES: [[@LINE-1]]:6: warning: ('signed char' passed to
+  // 'operator<<' outputs as character instead of integer
+
+  char char_value = 9;
+  os << char_value;
+}
+
+void based_on_ostream(std::basic_ostream<unsigned char> &os) {
+  unsigned char unsigned_value = 9;
+  os << unsigned_value;
+
+  signed char signed_value = 9;
+  os << signed_value;
+
+  char char_value = 9;
+  os << char_value;
+}

>From 22b8bd830cfaa12a86a6bbc0cab58db721de70c2 Mon Sep 17 00:00:00 2001
From: Congcong Cai <congcongcai0907 at 163.com>
Date: Wed, 19 Feb 2025 14:54:02 +0800
Subject: [PATCH 2/3] fix review

---
 .../clang-tidy/bugprone/UnintendedCharOstreamOutputCheck.cpp   | 3 +--
 clang-tools-extra/docs/ReleaseNotes.rst                        | 2 +-
 .../checks/bugprone/unintended-char-ostream-output.rst         | 2 +-
 .../checkers/bugprone/unintended-char-ostream-output.cpp       | 1 -
 4 files changed, 3 insertions(+), 5 deletions(-)

diff --git a/clang-tools-extra/clang-tidy/bugprone/UnintendedCharOstreamOutputCheck.cpp b/clang-tools-extra/clang-tidy/bugprone/UnintendedCharOstreamOutputCheck.cpp
index 7c54ef1486b2f..6c3ae59305af7 100644
--- a/clang-tools-extra/clang-tidy/bugprone/UnintendedCharOstreamOutputCheck.cpp
+++ b/clang-tools-extra/clang-tidy/bugprone/UnintendedCharOstreamOutputCheck.cpp
@@ -1,5 +1,4 @@
-//===--- UnintendedCharOstreamOutputCheck.cpp - clang-tidy
-//---------------------===//
+//===--- UnintendedCharOstreamOutputCheck.cpp - clang-tidy ----------------===//
 //
 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
 // See https://llvm.org/LICENSE.txt for license information.
diff --git a/clang-tools-extra/docs/ReleaseNotes.rst b/clang-tools-extra/docs/ReleaseNotes.rst
index 57f37c8e02e2e..662737a3802cb 100644
--- a/clang-tools-extra/docs/ReleaseNotes.rst
+++ b/clang-tools-extra/docs/ReleaseNotes.rst
@@ -94,7 +94,7 @@ New checks
 - New :doc:`bugprone-unintended-char-ostream-output
   <clang-tidy/checks/bugprone/unintended-char-ostream-output>` check.
 
-  Finds unintended character output from `unsigned char` and `signed char` to an
+  Finds unintended character output from ``unsigned char`` and ``signed char`` to an
   ostream.
 
 New check aliases
diff --git a/clang-tools-extra/docs/clang-tidy/checks/bugprone/unintended-char-ostream-output.rst b/clang-tools-extra/docs/clang-tidy/checks/bugprone/unintended-char-ostream-output.rst
index 1e60698a5d445..4d43fe93096bb 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/bugprone/unintended-char-ostream-output.rst
+++ b/clang-tools-extra/docs/clang-tidy/checks/bugprone/unintended-char-ostream-output.rst
@@ -3,7 +3,7 @@
 bugprone-unintended-char-ostream-output
 =======================================
 
-Finds unintended character output from `unsigned char` and `signed char` to an
+Finds unintended character output from ``unsigned char`` and ``signed char`` to an
 ``ostream``.
 
 Normally, when ``unsigned char (uint8_t)`` or ``signed char (int8_t)`` is used, it
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unintended-char-ostream-output.cpp b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unintended-char-ostream-output.cpp
index fd2382dcd730c..f1c56083a42c5 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unintended-char-ostream-output.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unintended-char-ostream-output.cpp
@@ -23,7 +23,6 @@ basic_ostream<char, _Traits> &operator<<(basic_ostream<char, _Traits> &,
 
 using ostream = basic_ostream<char>;
 
-
 } // namespace std
 
 class A : public std::ostream {};

>From 9033197f511fff7787ba397daa53efaefbbfd246 Mon Sep 17 00:00:00 2001
From: Congcong Cai <congcongcai0907 at 163.com>
Date: Wed, 19 Feb 2025 17:48:59 +0800
Subject: [PATCH 3/3] fix review

---
 .../bugprone/UnintendedCharOstreamOutputCheck.cpp         | 4 ++--
 .../checks/bugprone/unintended-char-ostream-output.rst    | 4 ++--
 .../checkers/bugprone/unintended-char-ostream-output.cpp  | 8 ++++----
 3 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/clang-tools-extra/clang-tidy/bugprone/UnintendedCharOstreamOutputCheck.cpp b/clang-tools-extra/clang-tidy/bugprone/UnintendedCharOstreamOutputCheck.cpp
index 6c3ae59305af7..342999597b027 100644
--- a/clang-tools-extra/clang-tidy/bugprone/UnintendedCharOstreamOutputCheck.cpp
+++ b/clang-tools-extra/clang-tidy/bugprone/UnintendedCharOstreamOutputCheck.cpp
@@ -60,9 +60,9 @@ void UnintendedCharOstreamOutputCheck::check(
   const auto *Call = Result.Nodes.getNodeAs<CXXOperatorCallExpr>("x");
   const Expr *Value = Call->getArg(1);
   diag(Call->getOperatorLoc(),
-       "(%0 passed to 'operator<<' outputs as character instead of integer. "
+       "%0 passed to 'operator<<' outputs as character instead of integer. "
        "cast to 'unsigned' to print numeric value or cast to 'char' to print "
-       "as character)")
+       "as character")
       << Value->getType() << Value->getSourceRange();
 }
 
diff --git a/clang-tools-extra/docs/clang-tidy/checks/bugprone/unintended-char-ostream-output.rst b/clang-tools-extra/docs/clang-tidy/checks/bugprone/unintended-char-ostream-output.rst
index 4d43fe93096bb..e312f93dd4b93 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/bugprone/unintended-char-ostream-output.rst
+++ b/clang-tools-extra/docs/clang-tidy/checks/bugprone/unintended-char-ostream-output.rst
@@ -21,10 +21,10 @@ It could be fixed as
 
 .. code-block:: c++
 
-    std::cout << (uint32_t)v;
+    std::cout << static_cast<uint32_t>(v);
 
 Or cast to char to explicitly indicate the intent
 
 .. code-block:: c++
 
-    std::cout << (char)v;
+    std::cout << static_cast<char>(v);
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unintended-char-ostream-output.cpp b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unintended-char-ostream-output.cpp
index f1c56083a42c5..d2fcdc6797628 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unintended-char-ostream-output.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unintended-char-ostream-output.cpp
@@ -30,12 +30,12 @@ class A : public std::ostream {};
 void origin_ostream(std::ostream &os) {
   unsigned char unsigned_value = 9;
   os << unsigned_value;
-  // CHECK-MESSAGES: [[@LINE-1]]:6: warning: ('unsigned char' passed to
+  // CHECK-MESSAGES: [[@LINE-1]]:6: warning: 'unsigned char' passed to
   // 'operator<<' outputs as character instead of integer
 
   signed char signed_value = 9;
   os << signed_value;
-  // CHECK-MESSAGES: [[@LINE-1]]:6: warning: ('signed char' passed to
+  // CHECK-MESSAGES: [[@LINE-1]]:6: warning: 'signed char' passed to
   // 'operator<<' outputs as character instead of integer
 
   char char_value = 9;
@@ -45,12 +45,12 @@ void origin_ostream(std::ostream &os) {
 void based_on_ostream(A &os) {
   unsigned char unsigned_value = 9;
   os << unsigned_value;
-  // CHECK-MESSAGES: [[@LINE-1]]:6: warning: ('unsigned char' passed to
+  // CHECK-MESSAGES: [[@LINE-1]]:6: warning: 'unsigned char' passed to
   // 'operator<<' outputs as character instead of integer
 
   signed char signed_value = 9;
   os << signed_value;
-  // CHECK-MESSAGES: [[@LINE-1]]:6: warning: ('signed char' passed to
+  // CHECK-MESSAGES: [[@LINE-1]]:6: warning: 'signed char' passed to
   // 'operator<<' outputs as character instead of integer
 
   char char_value = 9;



More information about the cfe-commits mailing list