Skip to content

Conversation

DaveBrantonCTCT
Copy link

@DaveBrantonCTCT DaveBrantonCTCT commented Aug 25, 2025

These two examples illustrate the two problems

int8_t int_8_value;
printf("%d", int_8_value) ;

Will fail to replace correctly. The int8_t will cause this error message, and the replacement will be skipped.

Unknown corresponding signed type for non-BuiltinType 'int8_t'

SomeEnum enum_value;
printf("%x", enum_value) ;

This example will proceed with the replacement, but will fail to add the necessary cast to the enum's underlying type, and so the format string will break your build. That is, the replacement will be

SomeEnum enum_value;
fmt::print("{:x}", enum_value) ;

Instead of

SomeEnum enum_value;
fmt::print("{:x}", static_cast<int>(enum_value)) ;

Fixes #150343

Copy link

Thank you for submitting a Pull Request (PR) to the LLVM Project!

This PR will be automatically labeled and the relevant teams will be notified.

If you wish to, you can add reviewers by using the "Reviewers" section on this page.

If this is not working for you, it is probably because you do not have write permissions for the repository. In which case you can instead tag reviewers by name in a comment by using @ followed by their GitHub username.

If you have received no comments on your PR for a week, you can request a review by "ping"ing the PR by adding a comment “Ping”. The common courtesy "ping" rate is once a week. Please remember that you are asking for valuable time from other developers.

If you have further questions, they may be answered by the LLVM GitHub User Guide.

You can also ask questions in a comment on this PR, on the LLVM Discord or on the forums.

@llvmbot
Copy link
Member

llvmbot commented Aug 25, 2025

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

@llvm/pr-subscribers-clang-tidy

Author: None (DaveBrantonCTCT)

Changes

These two examples illustrate the two problems

int8_t int_8_value;
printf("%d", int_8_value) ;

Will fail to replace correctly. The int8_t will cause this error message, and the replacement will be skipped.

Unknown corresponding signed type for non-BuiltinType 'int8_t'

SomeEnum enum_value;
printf("%x", enum_value) ;

This example will proceed with the replacement, but will fail to add the necessary cast to the enum's underlying type, and so the format string will break your build. That is, the replacement will be

SomeEnum enum_value;
fmt::print("{:x}", enum_value) ;

Instead of

SomeEnum enum_value;
fmt::print("{:x}", static_cast&lt;int&gt;(enum_value)) ;
``

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


1 Files Affected:

- (modified) clang-tools-extra/clang-tidy/utils/FormatStringConverter.cpp (+68-11) 


``````````diff
diff --git a/clang-tools-extra/clang-tidy/utils/FormatStringConverter.cpp b/clang-tools-extra/clang-tidy/utils/FormatStringConverter.cpp
index 0df8e913100fc..76a14c5800589 100644
--- a/clang-tools-extra/clang-tidy/utils/FormatStringConverter.cpp
+++ b/clang-tools-extra/clang-tidy/utils/FormatStringConverter.cpp
@@ -42,7 +42,7 @@ static bool isRealCharType(const clang::QualType &Ty) {
 
 /// If possible, return the text name of the signed type that corresponds to the
 /// passed integer type. If the passed type is already signed then its name is
-/// just returned. Only supports BuiltinTypes.
+/// just returned. Supports BuiltinTypes and types from <cstdint>
 static std::optional<std::string>
 getCorrespondingSignedTypeName(const clang::QualType &QT) {
   using namespace clang;
@@ -80,6 +80,10 @@ getCorrespondingSignedTypeName(const clang::QualType &QT) {
   const bool InStd = SimplifiedTypeName.consume_front("std::");
   const StringRef Prefix = InStd ? "std::" : "";
 
+  if (SimplifiedTypeName.starts_with("int") &&
+      SimplifiedTypeName.ends_with("_t"))
+    return (Twine(Prefix) + SimplifiedTypeName).str();
+
   if (SimplifiedTypeName.starts_with("uint") &&
       SimplifiedTypeName.ends_with("_t"))
     return (Twine(Prefix) + SimplifiedTypeName.drop_front()).str();
@@ -453,8 +457,38 @@ bool FormatStringConverter::emitIntegerArgument(
     // std::format will print bool as either "true" or "false" by default,
     // but printf prints them as "0" or "1". Be compatible with printf by
     // requesting decimal output.
-    FormatSpec.push_back('d');
+
+    // In cases where `x` or `X` was specified in the format string
+    // these will technically have no effect, since the bool can only be zero or
+    // one. However, it seems best to leave them as-is anyway.
+    switch(ArgKind)
+    {
+      case ConversionSpecifier::Kind::xArg:
+        FormatSpec.push_back('x'); // Not strictly needed
+        break;
+      case ConversionSpecifier::Kind::XArg:
+        FormatSpec.push_back('X');
+        break;
+      default:
+        FormatSpec.push_back('d');
+    }
+    
   } else if (ArgType->isEnumeralType()) {
+
+    // If the format string contained `x` or `X`, then use these
+    // format modifiers. Otherwise the default will work.
+    switch(ArgKind)
+    {
+      case ConversionSpecifier::Kind::xArg:
+        FormatSpec.push_back('x');
+        break;
+      case ConversionSpecifier::Kind::XArg:
+        FormatSpec.push_back('X');
+        break;
+      default:
+        break;
+    }
+
     // std::format will try to find a specialization to print the enum
     // (and probably fail), whereas printf would have just expected it to
     // be passed as its underlying type. However, printf will have forced
@@ -478,8 +512,22 @@ bool FormatStringConverter::emitIntegerArgument(
     // unsigned unless we cast it.
     if (const std::optional<std::string> MaybeCastType =
             castTypeForArgument(ArgKind, ArgType))
+    {
+      switch(ArgKind)
+      {
+        case ConversionSpecifier::Kind::xArg:
+          FormatSpec.push_back('x');
+          break;
+        case ConversionSpecifier::Kind::XArg:
+          FormatSpec.push_back('X');
+          break;
+        default:
+          break;
+      }
+
       ArgFixes.emplace_back(
           ArgIndex, (Twine("static_cast<") + *MaybeCastType + ">(").str());
+    }
     else
       return conversionNotPossible(
           (Twine("argument ") + Twine(ArgIndex) + " cannot be cast to " +
@@ -488,9 +536,22 @@ bool FormatStringConverter::emitIntegerArgument(
            " integer type to match format"
            " specifier and StrictMode is enabled")
               .str());
-  } else if (isRealCharType(ArgType) || !ArgType->isIntegerType()) {
-    // Only specify integer if the argument is of a different type
-    FormatSpec.push_back('d');
+  } else
+  {
+    switch(ArgKind)
+    {
+      case ConversionSpecifier::Kind::xArg:
+        FormatSpec.push_back('x');
+        break;
+      case ConversionSpecifier::Kind::XArg:
+        FormatSpec.push_back('X');
+        break;
+      default:
+        if (isRealCharType(ArgType) || !ArgType->isIntegerType()) {
+        // Only specify integer if the argument is of a different type
+        FormatSpec.push_back('d');
+      }
+    }
   }
   return true;
 }
@@ -514,6 +575,8 @@ bool FormatStringConverter::emitType(const PrintfSpecifier &FS, const Expr *Arg,
   case ConversionSpecifier::Kind::dArg:
   case ConversionSpecifier::Kind::iArg:
   case ConversionSpecifier::Kind::uArg:
+  case ConversionSpecifier::Kind::xArg:
+  case ConversionSpecifier::Kind::XArg:
     if (!emitIntegerArgument(ArgKind, Arg, FS.getArgIndex() + ArgsOffset,
                              FormatSpec))
       return false;
@@ -526,12 +589,6 @@ bool FormatStringConverter::emitType(const PrintfSpecifier &FS, const Expr *Arg,
                             "static_cast<const void *>(");
     break;
   }
-  case ConversionSpecifier::Kind::xArg:
-    FormatSpec.push_back('x');
-    break;
-  case ConversionSpecifier::Kind::XArg:
-    FormatSpec.push_back('X');
-    break;
   case ConversionSpecifier::Kind::oArg:
     FormatSpec.push_back('o');
     break;

Copy link
Contributor

@vbvictor vbvictor left a comment

Choose a reason for hiding this comment

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

Please add tests and release notes about your change in clang-tools-extra/docs/ReleaseNotes.rst

Copy link

⚠️ C/C++ code formatter, clang-format found issues in your code. ⚠️

You can test this locally with the following command:
git-clang-format --diff HEAD~1 HEAD --extensions cpp -- clang-tools-extra/clang-tidy/utils/FormatStringConverter.cpp
View the diff from clang-format here.
diff --git a/clang-tools-extra/clang-tidy/utils/FormatStringConverter.cpp b/clang-tools-extra/clang-tidy/utils/FormatStringConverter.cpp
index 76a14c580..104ce5eee 100644
--- a/clang-tools-extra/clang-tidy/utils/FormatStringConverter.cpp
+++ b/clang-tools-extra/clang-tidy/utils/FormatStringConverter.cpp
@@ -461,32 +461,30 @@ bool FormatStringConverter::emitIntegerArgument(
     // In cases where `x` or `X` was specified in the format string
     // these will technically have no effect, since the bool can only be zero or
     // one. However, it seems best to leave them as-is anyway.
-    switch(ArgKind)
-    {
-      case ConversionSpecifier::Kind::xArg:
-        FormatSpec.push_back('x'); // Not strictly needed
-        break;
-      case ConversionSpecifier::Kind::XArg:
-        FormatSpec.push_back('X');
-        break;
-      default:
-        FormatSpec.push_back('d');
+    switch (ArgKind) {
+    case ConversionSpecifier::Kind::xArg:
+      FormatSpec.push_back('x'); // Not strictly needed
+      break;
+    case ConversionSpecifier::Kind::XArg:
+      FormatSpec.push_back('X');
+      break;
+    default:
+      FormatSpec.push_back('d');
     }
-    
+
   } else if (ArgType->isEnumeralType()) {
 
     // If the format string contained `x` or `X`, then use these
     // format modifiers. Otherwise the default will work.
-    switch(ArgKind)
-    {
-      case ConversionSpecifier::Kind::xArg:
-        FormatSpec.push_back('x');
-        break;
-      case ConversionSpecifier::Kind::XArg:
-        FormatSpec.push_back('X');
-        break;
-      default:
-        break;
+    switch (ArgKind) {
+    case ConversionSpecifier::Kind::xArg:
+      FormatSpec.push_back('x');
+      break;
+    case ConversionSpecifier::Kind::XArg:
+      FormatSpec.push_back('X');
+      break;
+    default:
+      break;
     }
 
     // std::format will try to find a specialization to print the enum
@@ -511,24 +509,21 @@ bool FormatStringConverter::emitIntegerArgument(
     // Even -Wformat doesn't warn for this. std::format will format as
     // unsigned unless we cast it.
     if (const std::optional<std::string> MaybeCastType =
-            castTypeForArgument(ArgKind, ArgType))
-    {
-      switch(ArgKind)
-      {
-        case ConversionSpecifier::Kind::xArg:
-          FormatSpec.push_back('x');
-          break;
-        case ConversionSpecifier::Kind::XArg:
-          FormatSpec.push_back('X');
-          break;
-        default:
-          break;
+            castTypeForArgument(ArgKind, ArgType)) {
+      switch (ArgKind) {
+      case ConversionSpecifier::Kind::xArg:
+        FormatSpec.push_back('x');
+        break;
+      case ConversionSpecifier::Kind::XArg:
+        FormatSpec.push_back('X');
+        break;
+      default:
+        break;
       }
 
       ArgFixes.emplace_back(
           ArgIndex, (Twine("static_cast<") + *MaybeCastType + ">(").str());
-    }
-    else
+    } else
       return conversionNotPossible(
           (Twine("argument ") + Twine(ArgIndex) + " cannot be cast to " +
            Twine(ArgKind == ConversionSpecifier::Kind::uArg ? "unsigned"
@@ -536,18 +531,16 @@ bool FormatStringConverter::emitIntegerArgument(
            " integer type to match format"
            " specifier and StrictMode is enabled")
               .str());
-  } else
-  {
-    switch(ArgKind)
-    {
-      case ConversionSpecifier::Kind::xArg:
-        FormatSpec.push_back('x');
-        break;
-      case ConversionSpecifier::Kind::XArg:
-        FormatSpec.push_back('X');
-        break;
-      default:
-        if (isRealCharType(ArgType) || !ArgType->isIntegerType()) {
+  } else {
+    switch (ArgKind) {
+    case ConversionSpecifier::Kind::xArg:
+      FormatSpec.push_back('x');
+      break;
+    case ConversionSpecifier::Kind::XArg:
+      FormatSpec.push_back('X');
+      break;
+    default:
+      if (isRealCharType(ArgType) || !ArgType->isIntegerType()) {
         // Only specify integer if the argument is of a different type
         FormatSpec.push_back('d');
       }

Copy link
Contributor

@EugeneZelenko EugeneZelenko left a comment

Choose a reason for hiding this comment

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

Please mention changes in Release Notes.

// In cases where `x` or `X` was specified in the format string
// these will technically have no effect, since the bool can only be zero or
// one. However, it seems best to leave them as-is anyway.
switch(ArgKind)
Copy link
Contributor

Choose a reason for hiding this comment

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

Please run Clang-Format.

FormatSpec.push_back('X');
break;
default:
FormatSpec.push_back('d');
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
FormatSpec.push_back('d');
FormatSpec.push_back('d');
break;

if (isRealCharType(ArgType) || !ArgType->isIntegerType()) {
// Only specify integer if the argument is of a different type
FormatSpec.push_back('d');
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
}
}
break;

@DaveBrantonCTCT DaveBrantonCTCT force-pushed the std-format-modernize-fix-enums-and-signed-types branch from e37d17a to 19b4ddf Compare September 1, 2025 20:59
@@ -150,6 +150,9 @@ Changes to the Debug Info
Changes to the LLVM tools
---------------------------------

* modernize-use-std-format now correctly replaces signed types, and correctly
Copy link
Contributor

Choose a reason for hiding this comment

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

Such entry should be in clang-tools-extra/docs/ReleaseNotes.rst. See other entries there as examples.

Copy link
Author

Choose a reason for hiding this comment

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

Thank you, have put this in the right place.

@@ -249,6 +249,10 @@ Changes in existing checks
<clang-tidy/checks/readability/uppercase-literal-suffix>` check to recognize
literal suffixes added in C++23 and C23.

- Improved :doc:`modernize-use-std-format
Copy link
Contributor

Choose a reason for hiding this comment

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

Please keep alphabetcial order (by check name) in this list.

@@ -249,6 +249,10 @@ Changes in existing checks
<clang-tidy/checks/readability/uppercase-literal-suffix>` check to recognize
literal suffixes added in C++23 and C23.

- Improved :doc:`modernize-use-std-format
<clang-tidy/checks/modernize/use-std-format>` now correctly replaces signed types, and correctly
Copy link
Contributor

Choose a reason for hiding this comment

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

Please follow 80 characters limit.

@@ -213,6 +213,11 @@ Changes in existing checks
when the format string is converted to a different type by an implicit
constructor call.

- Improved :doc:`modernize-use-std-format
Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry, I missed existing modernize-use-std-format entry. Convention is to have single entry per check. Just merge existing statement with your text. At least previous release should have such examples.

Copy link
Contributor

@vbvictor vbvictor left a comment

Choose a reason for hiding this comment

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

Please write tests for your patch.

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.

clang-tidy modernize-use-std-format breaks enums when used with '%x', and fails replacements when signed stdint types are used.
4 participants