Skip to content

Commit 56762b7

Browse files
[clang-tidy] Add new check bugprone-unintended-char-ostream-output (llvm#127720)
It wants to find unintended character output from `uint8_t` and `int8_t` to an ostream. e.g. ```c++ uint8_t v = 9; std::cout << v; ``` --------- Co-authored-by: whisperity <whisperity@gmail.com>
1 parent 7b263fa commit 56762b7

9 files changed

+361
-0
lines changed

clang-tools-extra/clang-tidy/bugprone/BugproneTidyModule.cpp

+3
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
#include "UndelegatedConstructorCheck.h"
9191
#include "UnhandledExceptionAtNewCheck.h"
9292
#include "UnhandledSelfAssignmentCheck.h"
93+
#include "UnintendedCharOstreamOutputCheck.h"
9394
#include "UniquePtrArrayMismatchCheck.h"
9495
#include "UnsafeFunctionsCheck.h"
9596
#include "UnusedLocalNonTrivialVariableCheck.h"
@@ -147,6 +148,8 @@ class BugproneModule : public ClangTidyModule {
147148
"bugprone-incorrect-enable-if");
148149
CheckFactories.registerCheck<IncorrectEnableSharedFromThisCheck>(
149150
"bugprone-incorrect-enable-shared-from-this");
151+
CheckFactories.registerCheck<UnintendedCharOstreamOutputCheck>(
152+
"bugprone-unintended-char-ostream-output");
150153
CheckFactories.registerCheck<ReturnConstRefFromParameterCheck>(
151154
"bugprone-return-const-ref-from-parameter");
152155
CheckFactories.registerCheck<SwitchMissingDefaultCaseCheck>(

clang-tools-extra/clang-tidy/bugprone/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ add_clang_library(clangTidyBugproneModule STATIC
2929
InaccurateEraseCheck.cpp
3030
IncorrectEnableIfCheck.cpp
3131
IncorrectEnableSharedFromThisCheck.cpp
32+
UnintendedCharOstreamOutputCheck.cpp
3233
ReturnConstRefFromParameterCheck.cpp
3334
SuspiciousStringviewDataUsageCheck.cpp
3435
SwitchMissingDefaultCaseCheck.cpp
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
//===--- UnintendedCharOstreamOutputCheck.cpp - clang-tidy ----------------===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
#include "UnintendedCharOstreamOutputCheck.h"
10+
#include "clang/AST/Type.h"
11+
#include "clang/ASTMatchers/ASTMatchFinder.h"
12+
#include "clang/ASTMatchers/ASTMatchers.h"
13+
#include "clang/Basic/Diagnostic.h"
14+
#include "clang/Tooling/FixIt.h"
15+
16+
using namespace clang::ast_matchers;
17+
18+
namespace clang::tidy::bugprone {
19+
20+
namespace {
21+
22+
// check if the type is unsigned char or signed char
23+
AST_MATCHER(Type, isNumericChar) {
24+
return Node.isSpecificBuiltinType(BuiltinType::SChar) ||
25+
Node.isSpecificBuiltinType(BuiltinType::UChar);
26+
}
27+
28+
// check if the type is char
29+
AST_MATCHER(Type, isChar) {
30+
return Node.isSpecificBuiltinType(BuiltinType::Char_S) ||
31+
Node.isSpecificBuiltinType(BuiltinType::Char_U);
32+
}
33+
34+
} // namespace
35+
36+
UnintendedCharOstreamOutputCheck::UnintendedCharOstreamOutputCheck(
37+
StringRef Name, ClangTidyContext *Context)
38+
: ClangTidyCheck(Name, Context), CastTypeName(Options.get("CastTypeName")) {
39+
}
40+
void UnintendedCharOstreamOutputCheck::storeOptions(
41+
ClangTidyOptions::OptionMap &Opts) {
42+
if (CastTypeName.has_value())
43+
Options.store(Opts, "CastTypeName", CastTypeName.value());
44+
}
45+
46+
void UnintendedCharOstreamOutputCheck::registerMatchers(MatchFinder *Finder) {
47+
auto BasicOstream =
48+
cxxRecordDecl(hasName("::std::basic_ostream"),
49+
// only basic_ostream<char, Traits> has overload operator<<
50+
// with char / unsigned char / signed char
51+
classTemplateSpecializationDecl(
52+
hasTemplateArgument(0, refersToType(isChar()))));
53+
Finder->addMatcher(
54+
cxxOperatorCallExpr(
55+
hasOverloadedOperatorName("<<"),
56+
hasLHS(hasType(hasUnqualifiedDesugaredType(
57+
recordType(hasDeclaration(cxxRecordDecl(
58+
anyOf(BasicOstream, isDerivedFrom(BasicOstream)))))))),
59+
hasRHS(hasType(hasUnqualifiedDesugaredType(isNumericChar()))))
60+
.bind("x"),
61+
this);
62+
}
63+
64+
void UnintendedCharOstreamOutputCheck::check(
65+
const MatchFinder::MatchResult &Result) {
66+
const auto *Call = Result.Nodes.getNodeAs<CXXOperatorCallExpr>("x");
67+
const Expr *Value = Call->getArg(1);
68+
const SourceRange SourceRange = Value->getSourceRange();
69+
70+
DiagnosticBuilder Builder =
71+
diag(Call->getOperatorLoc(),
72+
"%0 passed to 'operator<<' outputs as character instead of integer. "
73+
"cast to 'unsigned int' to print numeric value or cast to 'char' to "
74+
"print as character")
75+
<< Value->getType() << SourceRange;
76+
77+
QualType T = Value->getType();
78+
const Type *UnqualifiedDesugaredType = T->getUnqualifiedDesugaredType();
79+
80+
llvm::StringRef CastType = CastTypeName.value_or(
81+
UnqualifiedDesugaredType->isSpecificBuiltinType(BuiltinType::SChar)
82+
? "int"
83+
: "unsigned int");
84+
85+
Builder << FixItHint::CreateReplacement(
86+
SourceRange, ("static_cast<" + CastType + ">(" +
87+
tooling::fixit::getText(*Value, *Result.Context) + ")")
88+
.str());
89+
}
90+
91+
} // namespace clang::tidy::bugprone
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//===--- UnintendedCharOstreamOutputCheck.h - clang-tidy --------*- C++ -*-===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_BUGPRONE_UNINTENDEDCHAROSTREAMOUTPUTCHECK_H
10+
#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_BUGPRONE_UNINTENDEDCHAROSTREAMOUTPUTCHECK_H
11+
12+
#include "../ClangTidyCheck.h"
13+
#include <optional>
14+
15+
namespace clang::tidy::bugprone {
16+
17+
/// Finds unintended character output from `unsigned char` and `signed char` to
18+
/// an ostream.
19+
///
20+
/// For the user-facing documentation see:
21+
/// http://clang.llvm.org/extra/clang-tidy/checks/bugprone/unintended-char-ostream-output.html
22+
class UnintendedCharOstreamOutputCheck : public ClangTidyCheck {
23+
public:
24+
UnintendedCharOstreamOutputCheck(StringRef Name, ClangTidyContext *Context);
25+
void storeOptions(ClangTidyOptions::OptionMap &Opts) override;
26+
void registerMatchers(ast_matchers::MatchFinder *Finder) override;
27+
void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
28+
bool isLanguageVersionSupported(const LangOptions &LangOpts) const override {
29+
return LangOpts.CPlusPlus;
30+
}
31+
32+
private:
33+
const std::optional<StringRef> CastTypeName;
34+
};
35+
36+
} // namespace clang::tidy::bugprone
37+
38+
#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_BUGPRONE_UNINTENDEDCHAROSTREAMOUTPUTCHECK_H

clang-tools-extra/docs/ReleaseNotes.rst

+6
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@ Improvements to clang-tidy
9191
New checks
9292
^^^^^^^^^^
9393

94+
- New :doc:`bugprone-unintended-char-ostream-output
95+
<clang-tidy/checks/bugprone/unintended-char-ostream-output>` check.
96+
97+
Finds unintended character output from ``unsigned char`` and ``signed char`` to an
98+
``ostream``.
99+
94100
New check aliases
95101
^^^^^^^^^^^^^^^^^
96102

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
.. title:: clang-tidy - bugprone-unintended-char-ostream-output
2+
3+
bugprone-unintended-char-ostream-output
4+
=======================================
5+
6+
Finds unintended character output from ``unsigned char`` and ``signed char`` to an
7+
``ostream``.
8+
9+
Normally, when ``unsigned char (uint8_t)`` or ``signed char (int8_t)`` is used, it
10+
is more likely a number than a character. However, when it is passed directly to
11+
``std::ostream``'s ``operator<<``, the result is the character output instead
12+
of the numeric value. This often contradicts the developer's intent to print
13+
integer values.
14+
15+
.. code-block:: c++
16+
17+
uint8_t v = 65;
18+
std::cout << v; // output 'A' instead of '65'
19+
20+
The check will suggest casting the value to an appropriate type to indicate the
21+
intent, by default, it will cast to ``unsigned int`` for ``unsigned char`` and
22+
``int`` for ``signed char``.
23+
24+
.. code-block:: c++
25+
26+
std::cout << static_cast<unsigned int>(v); // when v is unsigned char
27+
std::cout << static_cast<int>(v); // when v is signed char
28+
29+
To avoid lengthy cast statements, add prefix ``+`` to the variable can also
30+
suppress warnings because unary expression will promote the value to an ``int``.
31+
32+
.. code-block:: c++
33+
34+
std::cout << +v;
35+
36+
Or cast to char to explicitly indicate that output should be a character.
37+
38+
.. code-block:: c++
39+
40+
std::cout << static_cast<char>(v);
41+
42+
.. option:: CastTypeName
43+
44+
When `CastTypeName` is specified, the fix-it will use `CastTypeName` as the
45+
cast target type. Otherwise, fix-it will automatically infer the type.

clang-tools-extra/docs/clang-tidy/checks/list.rst

+1
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ Clang-Tidy Checks
158158
:doc:`bugprone-undelegated-constructor <bugprone/undelegated-constructor>`,
159159
:doc:`bugprone-unhandled-exception-at-new <bugprone/unhandled-exception-at-new>`,
160160
:doc:`bugprone-unhandled-self-assignment <bugprone/unhandled-self-assignment>`,
161+
:doc:`bugprone-unintended-char-ostream-output <bugprone/unintended-char-ostream-output>`, "Yes"
161162
:doc:`bugprone-unique-ptr-array-mismatch <bugprone/unique-ptr-array-mismatch>`, "Yes"
162163
:doc:`bugprone-unsafe-functions <bugprone/unsafe-functions>`,
163164
:doc:`bugprone-unused-local-non-trivial-variable <bugprone/unused-local-non-trivial-variable>`,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// RUN: %check_clang_tidy %s bugprone-unintended-char-ostream-output %t -- \
2+
// RUN: -config="{CheckOptions: \
3+
// RUN: {bugprone-unintended-char-ostream-output.CastTypeName: "uint8_t"}}"
4+
5+
namespace std {
6+
7+
template <class _CharT, class _Traits = void> class basic_ostream {
8+
public:
9+
basic_ostream &operator<<(int);
10+
basic_ostream &operator<<(unsigned int);
11+
};
12+
13+
template <class CharT, class Traits>
14+
basic_ostream<CharT, Traits> &operator<<(basic_ostream<CharT, Traits> &, CharT);
15+
template <class CharT, class Traits>
16+
basic_ostream<CharT, Traits> &operator<<(basic_ostream<CharT, Traits> &, char);
17+
template <class _Traits>
18+
basic_ostream<char, _Traits> &operator<<(basic_ostream<char, _Traits> &, char);
19+
template <class _Traits>
20+
basic_ostream<char, _Traits> &operator<<(basic_ostream<char, _Traits> &,
21+
signed char);
22+
template <class _Traits>
23+
basic_ostream<char, _Traits> &operator<<(basic_ostream<char, _Traits> &,
24+
unsigned char);
25+
26+
using ostream = basic_ostream<char>;
27+
28+
} // namespace std
29+
30+
class A : public std::ostream {};
31+
32+
void origin_ostream(std::ostream &os) {
33+
unsigned char unsigned_value = 9;
34+
os << unsigned_value;
35+
// CHECK-MESSAGES: [[@LINE-1]]:6: warning: 'unsigned char' passed to 'operator<<' outputs as character instead of integer
36+
// CHECK-FIXES: os << static_cast<uint8_t>(unsigned_value);
37+
38+
signed char signed_value = 9;
39+
os << signed_value;
40+
// CHECK-MESSAGES: [[@LINE-1]]:6: warning: 'signed char' passed to 'operator<<' outputs as character instead of integer
41+
// CHECK-FIXES: os << static_cast<uint8_t>(signed_value);
42+
43+
char char_value = 9;
44+
os << char_value;
45+
}

0 commit comments

Comments
 (0)