Skip to content

[clang-tidy] treat unsigned char and signed char as char type by default in bugprone-unintended-char-ostream-output #134870

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conversation

HerrCai0907
Copy link
Contributor

@HerrCai0907 HerrCai0907 commented Apr 8, 2025

Add AllowedTypes options to support custom defined char like type.
treat unsigned char and signed char as char like type by default.
The allowed types only effect when the var decl or explicit cast to this
non-canonical type names.

Fixed: #133425

@HerrCai0907 HerrCai0907 marked this pull request as ready for review April 8, 2025 15:44
@llvmbot
Copy link
Member

llvmbot commented Apr 8, 2025

@llvm/pr-subscribers-clang-tools-extra

@llvm/pr-subscribers-clang-tidy

Author: Congcong Cai (HerrCai0907)

Changes

Add AllowedTypes options to support custom defined char like type.
treat unsigned char and signed char as char like type by default.
The allowed types only effect when the var decl or explicit cast to this
non-canonical type names.

Fixed: #133425


Full diff: https://github.com/llvm/llvm-project/pull/134870.diff

6 Files Affected:

  • (modified) clang-tools-extra/clang-tidy/bugprone/UnintendedCharOstreamOutputCheck.cpp (+16-3)
  • (modified) clang-tools-extra/clang-tidy/bugprone/UnintendedCharOstreamOutputCheck.h (+1)
  • (modified) clang-tools-extra/docs/clang-tidy/checks/bugprone/unintended-char-ostream-output.rst (+8)
  • (added) clang-tools-extra/test/clang-tidy/checkers/bugprone/unintended-char-ostream-output-allowed-types.cpp (+41)
  • (modified) clang-tools-extra/test/clang-tidy/checkers/bugprone/unintended-char-ostream-output-cast-type.cpp (+6-5)
  • (modified) clang-tools-extra/test/clang-tidy/checkers/bugprone/unintended-char-ostream-output.cpp (+34-36)
diff --git a/clang-tools-extra/clang-tidy/bugprone/UnintendedCharOstreamOutputCheck.cpp b/clang-tools-extra/clang-tidy/bugprone/UnintendedCharOstreamOutputCheck.cpp
index 7250e4ccb8c69..57e1f744fcd7d 100644
--- a/clang-tools-extra/clang-tidy/bugprone/UnintendedCharOstreamOutputCheck.cpp
+++ b/clang-tools-extra/clang-tidy/bugprone/UnintendedCharOstreamOutputCheck.cpp
@@ -7,6 +7,8 @@
 //===----------------------------------------------------------------------===//
 
 #include "UnintendedCharOstreamOutputCheck.h"
+#include "../utils/Matchers.h"
+#include "../utils/OptionsUtils.h"
 #include "clang/AST/Type.h"
 #include "clang/ASTMatchers/ASTMatchFinder.h"
 #include "clang/ASTMatchers/ASTMatchers.h"
@@ -35,10 +37,14 @@ AST_MATCHER(Type, isChar) {
 
 UnintendedCharOstreamOutputCheck::UnintendedCharOstreamOutputCheck(
     StringRef Name, ClangTidyContext *Context)
-    : ClangTidyCheck(Name, Context), CastTypeName(Options.get("CastTypeName")) {
-}
+    : ClangTidyCheck(Name, Context),
+      AllowedTypes(utils::options::parseStringList(
+          Options.get("AllowedTypes", "unsigned char;signed char"))),
+      CastTypeName(Options.get("CastTypeName")) {}
 void UnintendedCharOstreamOutputCheck::storeOptions(
     ClangTidyOptions::OptionMap &Opts) {
+  Options.store(Opts, "AllowedTypes",
+                utils::options::serializeStringList(AllowedTypes));
   if (CastTypeName.has_value())
     Options.store(Opts, "CastTypeName", CastTypeName.value());
 }
@@ -50,13 +56,20 @@ void UnintendedCharOstreamOutputCheck::registerMatchers(MatchFinder *Finder) {
                     // with char / unsigned char / signed char
                     classTemplateSpecializationDecl(
                         hasTemplateArgument(0, refersToType(isChar()))));
+  auto IsDeclRefExprFromAllowedTypes = declRefExpr(to(varDecl(
+      hasType(matchers::matchesAnyListedTypeName(AllowedTypes, false)))));
+  auto IsExplicitCastExprFromAllowedTypes = explicitCastExpr(hasDestinationType(
+      matchers::matchesAnyListedTypeName(AllowedTypes, false)));
   Finder->addMatcher(
       cxxOperatorCallExpr(
           hasOverloadedOperatorName("<<"),
           hasLHS(hasType(hasUnqualifiedDesugaredType(
               recordType(hasDeclaration(cxxRecordDecl(
                   anyOf(BasicOstream, isDerivedFrom(BasicOstream)))))))),
-          hasRHS(hasType(hasUnqualifiedDesugaredType(isNumericChar()))))
+          hasRHS(expr(hasType(hasUnqualifiedDesugaredType(isNumericChar())),
+                      unless(ignoringParenImpCasts(
+                          anyOf(IsDeclRefExprFromAllowedTypes,
+                                IsExplicitCastExprFromAllowedTypes))))))
           .bind("x"),
       this);
 }
diff --git a/clang-tools-extra/clang-tidy/bugprone/UnintendedCharOstreamOutputCheck.h b/clang-tools-extra/clang-tidy/bugprone/UnintendedCharOstreamOutputCheck.h
index 61ea623d139ea..0759e3d1eb460 100644
--- a/clang-tools-extra/clang-tidy/bugprone/UnintendedCharOstreamOutputCheck.h
+++ b/clang-tools-extra/clang-tidy/bugprone/UnintendedCharOstreamOutputCheck.h
@@ -30,6 +30,7 @@ class UnintendedCharOstreamOutputCheck : public ClangTidyCheck {
   }
 
 private:
+  const std::vector<StringRef> AllowedTypes;
   const std::optional<StringRef> CastTypeName;
 };
 
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 95d02b3e2ddda..9ad08188d7fb2 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
@@ -42,6 +42,14 @@ Or cast to char to explicitly indicate that output should be a character.
 Options
 -------
 
+.. option:: AllowedTypes
+
+  A semicolon-separated list of type names that will be treated as ``char``
+  type. It only contains the non canonical type names without the alias of type
+  names. Explicit casting to these types or use the variable defined with these
+  types will be ignored.
+  Default is `unsigned char;signed char`.
+
 .. option:: CastTypeName
 
   When `CastTypeName` is specified, the fix-it will use `CastTypeName` as the
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unintended-char-ostream-output-allowed-types.cpp b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unintended-char-ostream-output-allowed-types.cpp
new file mode 100644
index 0000000000000..11dc207dfb0c3
--- /dev/null
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unintended-char-ostream-output-allowed-types.cpp
@@ -0,0 +1,41 @@
+// RUN: %check_clang_tidy %s bugprone-unintended-char-ostream-output %t -- \
+// RUN:   -config="{CheckOptions: \
+// RUN:             {bugprone-unintended-char-ostream-output.AllowedTypes: \"\"}}"
+
+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
+
+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;
+}
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unintended-char-ostream-output-cast-type.cpp b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unintended-char-ostream-output-cast-type.cpp
index 72020d90e0369..f3c72dac654ad 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unintended-char-ostream-output-cast-type.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unintended-char-ostream-output-cast-type.cpp
@@ -27,17 +27,18 @@ using ostream = basic_ostream<char>;
 
 } // namespace std
 
-class A : public std::ostream {};
+using uint8_t = unsigned char;
+using int8_t = signed char;
 
 void origin_ostream(std::ostream &os) {
-  unsigned char unsigned_value = 9;
+  uint8_t unsigned_value = 9;
   os << unsigned_value;
-  // CHECK-MESSAGES: [[@LINE-1]]:6: warning: 'unsigned char' passed to 'operator<<' outputs as character instead of integer
+  // CHECK-MESSAGES: [[@LINE-1]]:6: warning: 'uint8_t' (aka 'unsigned char') passed to 'operator<<' outputs as character instead of integer
   // CHECK-FIXES: os << static_cast<unsigned char>(unsigned_value);
 
-  signed char signed_value = 9;
+  int8_t signed_value = 9;
   os << signed_value;
-  // CHECK-MESSAGES: [[@LINE-1]]:6: warning: 'signed char' passed to 'operator<<' outputs as character instead of integer
+  // CHECK-MESSAGES: [[@LINE-1]]:6: warning: 'int8_t' (aka 'signed char') passed to 'operator<<' outputs as character instead of integer
   // CHECK-FIXES: os << static_cast<unsigned char>(signed_value);
 
   char char_value = 9;
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 573c429bf049f..b458e55b7abc4 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
@@ -27,41 +27,56 @@ using ostream = basic_ostream<char>;
 
 class A : public std::ostream {};
 
+using uint8_t = unsigned char;
+using int8_t = signed char;
+
 void origin_ostream(std::ostream &os) {
-  unsigned char unsigned_value = 9;
+  uint8_t unsigned_value = 9;
   os << unsigned_value;
-  // CHECK-MESSAGES: [[@LINE-1]]:6: warning: 'unsigned char' passed to 'operator<<' outputs as character instead of integer
+  // CHECK-MESSAGES: [[@LINE-1]]:6: warning: 'uint8_t' (aka 'unsigned char') passed to 'operator<<' outputs as character instead of integer
   // CHECK-FIXES: os << static_cast<unsigned int>(unsigned_value);
 
-  signed char signed_value = 9;
+  int8_t signed_value = 9;
   os << signed_value;
-  // CHECK-MESSAGES: [[@LINE-1]]:6: warning: 'signed char' passed to 'operator<<' outputs as character instead of integer
+  // CHECK-MESSAGES: [[@LINE-1]]:6: warning: 'int8_t' (aka 'signed char') passed to 'operator<<' outputs as character instead of integer
   // CHECK-FIXES: os << static_cast<int>(signed_value);
 
   char char_value = 9;
   os << char_value;
+  unsigned char unsigned_char_value = 9;
+  os << unsigned_char_value;
+  signed char signed_char_value = 9;
+  os << signed_char_value;
+}
+
+void explicit_cast_to_char_type(std::ostream &os) {
+  enum V : uint8_t {};
+  V e{};
+  os << static_cast<unsigned char>(e);
+  os << (unsigned char)(e);
+  os << (static_cast<unsigned char>(e));
 }
 
 void based_on_ostream(A &os) {
-  unsigned char unsigned_value = 9;
+  uint8_t unsigned_value = 9;
   os << unsigned_value;
-  // CHECK-MESSAGES: [[@LINE-1]]:6: warning: 'unsigned char' passed to 'operator<<' outputs as character instead of integer
+  // CHECK-MESSAGES: [[@LINE-1]]:6: warning: 'uint8_t' (aka 'unsigned char') passed to 'operator<<' outputs as character instead of integer
   // CHECK-FIXES: os << static_cast<unsigned int>(unsigned_value);
 
-  signed char signed_value = 9;
+  int8_t signed_value = 9;
   os << signed_value;
-  // CHECK-MESSAGES: [[@LINE-1]]:6: warning: 'signed char' passed to 'operator<<' outputs as character instead of integer
+  // CHECK-MESSAGES: [[@LINE-1]]:6: warning: 'int8_t' (aka 'signed char') passed to 'operator<<' outputs as character instead of integer
   // CHECK-FIXES: os << static_cast<int>(signed_value);
 
   char char_value = 9;
   os << char_value;
 }
 
-void other_ostream_template_parameters(std::basic_ostream<unsigned char> &os) {
-  unsigned char unsigned_value = 9;
+void other_ostream_template_parameters(std::basic_ostream<uint8_t> &os) {
+  uint8_t unsigned_value = 9;
   os << unsigned_value;
 
-  signed char signed_value = 9;
+  int8_t signed_value = 9;
   os << signed_value;
 
   char char_value = 9;
@@ -70,23 +85,22 @@ void other_ostream_template_parameters(std::basic_ostream<unsigned char> &os) {
 
 template <class T> class B : public std::ostream {};
 void template_based_on_ostream(B<int> &os) {
-  unsigned char unsigned_value = 9;
+  uint8_t unsigned_value = 9;
   os << unsigned_value;
-  // CHECK-MESSAGES: [[@LINE-1]]:6: warning: 'unsigned char' passed to 'operator<<' outputs as character instead of integer
+  // CHECK-MESSAGES: [[@LINE-1]]:6: warning: 'uint8_t' (aka 'unsigned char') passed to 'operator<<' outputs as character instead of integer
   // CHECK-FIXES: os << static_cast<unsigned int>(unsigned_value);
 }
 
 template<class T> void template_fn_1(T &os) {
-  unsigned char unsigned_value = 9;
+  uint8_t unsigned_value = 9;
   os << unsigned_value;
-  // CHECK-MESSAGES: [[@LINE-1]]:6: warning: 'unsigned char' passed to 'operator<<' outputs as character instead of integer
+  // CHECK-MESSAGES: [[@LINE-1]]:6: warning: 'uint8_t' (aka 'unsigned char') passed to 'operator<<' outputs as character instead of integer
   // CHECK-FIXES: os << static_cast<unsigned int>(unsigned_value);
 }
 template<class T> void template_fn_2(std::ostream &os) {
   T unsigned_value = 9;
   os << unsigned_value;
-  // CHECK-MESSAGES: [[@LINE-1]]:6: warning: 'unsigned char' passed to 'operator<<' outputs as character instead of integer
-  // CHECK-FIXES: os << static_cast<unsigned int>(unsigned_value);
+  // It should be detected as well. But we cannot get the sugared type name for SubstTemplateTypeParmType.
 }
 template<class T> void template_fn_3(std::ostream &os) {
   T unsigned_value = 9;
@@ -95,26 +109,10 @@ template<class T> void template_fn_3(std::ostream &os) {
 void call_template_fn() {
   A a{};
   template_fn_1(a);
-  template_fn_2<unsigned char>(a);
+  template_fn_2<uint8_t>(a);
   template_fn_3<char>(a);
 }
 
-using U8 = unsigned char;
-void alias_unsigned_char(std::ostream &os) {
-  U8 v = 9;
-  os << v;
-  // CHECK-MESSAGES: [[@LINE-1]]:6: warning: 'U8' (aka 'unsigned char') passed to 'operator<<' outputs as character instead of integer
-  // CHECK-FIXES: os << static_cast<unsigned int>(v);
-}
-
-using I8 = signed char;
-void alias_signed_char(std::ostream &os) {
-  I8 v = 9;
-  os << v;
-  // CHECK-MESSAGES: [[@LINE-1]]:6: warning: 'I8' (aka 'signed char') passed to 'operator<<' outputs as character instead of integer
-  // CHECK-FIXES: os << static_cast<int>(v);
-}
-
 using C8 = char;
 void alias_char(std::ostream &os) {
   C8 v = 9;
@@ -124,8 +122,8 @@ void alias_char(std::ostream &os) {
 
 #define MACRO_VARIANT_NAME a
 void macro_variant_name(std::ostream &os) {
-  unsigned char MACRO_VARIANT_NAME = 9;
+  uint8_t MACRO_VARIANT_NAME = 9;
   os << MACRO_VARIANT_NAME;
-  // CHECK-MESSAGES: [[@LINE-1]]:6: warning: 'unsigned char' passed to 'operator<<' outputs as character instead of integer
+  // CHECK-MESSAGES: [[@LINE-1]]:6: warning: 'uint8_t' (aka 'unsigned char') passed to 'operator<<' outputs as character instead of integer
   // CHECK-FIXES: os << static_cast<unsigned int>(MACRO_VARIANT_NAME);
 }

@HerrCai0907 HerrCai0907 force-pushed the users/ccc04-08-_clang-tidy_matchesanylistedtypename_support_non_canonical_types branch from 164409d to 27b7028 Compare April 8, 2025 15:47
@HerrCai0907 HerrCai0907 force-pushed the users/ccc04-08-_clang-tidy_treat_unsigned_char_and_signed_char_as_char_type_by_default_in_bugprone-unintended-char-ostream-output branch from f860128 to 9d29fd3 Compare April 8, 2025 15:48
Copy link
Contributor

@NagyDonat NagyDonat left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall looks good to me, I have one suggestion about the documentation.

@EugeneZelenko EugeneZelenko requested a review from PiotrZSL April 9, 2025 15:09
Copy link
Contributor Author

HerrCai0907 commented Apr 13, 2025

Merge activity

  • Apr 12, 11:58 PM EDT: A user started a stack merge that includes this pull request via Graphite.
  • Apr 13, 12:07 AM EDT: Graphite rebased this pull request as part of a merge.
  • Apr 13, 12:09 AM EDT: A user merged this pull request with Graphite.

@HerrCai0907 HerrCai0907 force-pushed the users/ccc04-08-_clang-tidy_matchesanylistedtypename_support_non_canonical_types branch 2 times, most recently from 833d092 to 104d0cc Compare April 13, 2025 04:03
Base automatically changed from users/ccc04-08-_clang-tidy_matchesanylistedtypename_support_non_canonical_types to main April 13, 2025 04:06
…ult in bugprone-unintended-char-ostream-output

Add `AllowedTypes` options to support custom defined char like type.
treat `unsigned char` and `signed char` as char like type by default.
The allowed types only effect when the var decl or explicit cast to this
non-canonical type names.
@HerrCai0907 HerrCai0907 force-pushed the users/ccc04-08-_clang-tidy_treat_unsigned_char_and_signed_char_as_char_type_by_default_in_bugprone-unintended-char-ostream-output branch from ef9322a to b9b424a Compare April 13, 2025 04:07
@HerrCai0907 HerrCai0907 merged commit 0681483 into main Apr 13, 2025
6 of 10 checks passed
@HerrCai0907 HerrCai0907 deleted the users/ccc04-08-_clang-tidy_treat_unsigned_char_and_signed_char_as_char_type_by_default_in_bugprone-unintended-char-ostream-output branch April 13, 2025 04:09
var-const pushed a commit to ldionne/llvm-project that referenced this pull request Apr 17, 2025
…ult in bugprone-unintended-char-ostream-output (llvm#134870)

Add `AllowedTypes` options to support custom defined char like type.
treat `unsigned char` and `signed char` as char like type by default.
The allowed types only effect when the var decl or explicit cast to this
non-canonical type names.

Fixed: llvm#133425
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

bugprone-unintended-char-ostream-output reported with sufficient cast
3 participants