[clang-tidy] treat unsigned char and signed char as char type by default in bugprone-unintended-char-ostream-output (#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: #133425
This commit is contained in:
Congcong Cai 2025-04-13 12:09:50 +08:00 committed by GitHub
parent 302bc41410
commit 06814834a6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 108 additions and 44 deletions

View File

@ -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);
}

View File

@ -30,6 +30,7 @@ public:
}
private:
const std::vector<StringRef> AllowedTypes;
const std::optional<StringRef> CastTypeName;
};

View File

@ -42,6 +42,16 @@ 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 like the ``char``
type: the check will not report variables declared with with these types or
explicit cast expressions to these types. Note that this distinguishes type
aliases from the original type, so specifying e.g. ``unsigned char`` here
will not suppress reports about ``uint8_t`` even if it is defined as a
``typedef`` alias for ``unsigned char``.
Default is `unsigned char;signed char`.
.. option:: CastTypeName
When `CastTypeName` is specified, the fix-it will use `CastTypeName` as the

View File

@ -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;
}

View File

@ -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;

View File

@ -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);
}