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

Carlos Galvez via cfe-commits cfe-commits at lists.llvm.org
Wed Feb 19 06:26:07 PST 2025


================
@@ -0,0 +1,69 @@
+//===--- 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();
----------------
carlosgalvezp wrote:

That's a fairly obscure solution, relying on implicit arithmetic promotions. It's banned in some coding guidelines (e.g. MISRA). Maybe just add an explicit `static_cast<int>`? Otherwise give the option to the user to choose what datatype to cast to, defaulting to `int`.

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


More information about the cfe-commits mailing list