[clang] [clang-format] Don't case-fix numeric literals in macro invocations (PR #212328)
Gustas JanuĊĦonis via cfe-commits
cfe-commits at lists.llvm.org
Sat Sep 5 16:39:08 PDT 2026
https://github.com/LeGusto updated https://github.com/llvm/llvm-project/pull/212328
>From a42fc20b9252a4f6e70ef497d46d172b513fa346 Mon Sep 17 00:00:00 2001
From: LeGusto <janusonis.gustas at gmail.com>
Date: Mon, 27 Jul 2026 22:00:43 +0300
Subject: [PATCH 1/2] [clang-format] Don't case-fix numeric literals in macro
invocations
NumericLiteralCaseFixer transformed macro invocations such as FOO(0xa) to FOO(0xA), which is incorrect when the argument is stringized.
Fix recognizes possible macro invocations in NumericLiteralCaseFixer::process by replicating the logic of FormatToken::isPossibleMacro and tracking the nesting of the invocation parentheses.
Addresses #200731
---
clang/lib/Format/NumericLiteralCaseFixer.cpp | 26 +++++++++++++++++--
.../Format/NumericLiteralCaseTest.cpp | 26 +++++++++++++++++++
2 files changed, 50 insertions(+), 2 deletions(-)
diff --git a/clang/lib/Format/NumericLiteralCaseFixer.cpp b/clang/lib/Format/NumericLiteralCaseFixer.cpp
index b58b3c7ee0189..ba55ebdcb8f41 100644
--- a/clang/lib/Format/NumericLiteralCaseFixer.cpp
+++ b/clang/lib/Format/NumericLiteralCaseFixer.cpp
@@ -43,6 +43,11 @@ static bool isNumericLiteralCaseFixerNeeded(const FormatStyle &Style) {
Option.ExponentLetter != Leave || Option.Suffix != Leave;
}
+// An all-caps identifier is likely a macro; see FormatToken::isPossibleMacro.
+static bool isPossibleMacroName(StringRef Name) {
+ return Name.size() > 1 && Name == Name.upper();
+}
+
static std::string
transformComponent(StringRef Component,
FormatStyle::NumericLiteralComponentStyle ConfigValue) {
@@ -137,8 +142,25 @@ NumericLiteralCaseFixer::process(const Environment &Env,
Token Tok;
tooling::Replacements Result;
+ int MacroArgDepth = 0; // Paren depth inside a possible macro invocation.
+
+ for (bool Skip = false, AfterMacroName = false, AfterScope = false;
+ !Lex.LexFromRawLexer(Tok);) {
+ // Leave arguments of a likely macro invocation (e.g. FOO(0xa)) untouched.
+ if (Tok.is(tok::raw_identifier)) {
+ AfterMacroName =
+ !AfterScope && isPossibleMacroName(Tok.getRawIdentifier());
+ } else if (Tok.is(tok::l_paren)) {
+ if (MacroArgDepth > 0 || AfterMacroName)
+ ++MacroArgDepth;
+ AfterMacroName = false;
+ } else {
+ if (Tok.is(tok::r_paren) && MacroArgDepth > 0)
+ --MacroArgDepth;
+ AfterMacroName = false;
+ }
+ AfterScope = Tok.is(tok::coloncolon);
- for (bool Skip = false; !Lex.LexFromRawLexer(Tok);) {
// Skip tokens that are too small to contain a formattable literal.
// Size=2 is the smallest possible literal that could contain formattable
// components, for example "1u".
@@ -157,7 +179,7 @@ NumericLiteralCaseFixer::process(const Environment &Env,
continue;
}
- if (Skip || Tok.isNot(tok::numeric_constant) ||
+ if (Skip || MacroArgDepth > 0 || Tok.isNot(tok::numeric_constant) ||
!AffectedRangeMgr.affectsCharSourceRange(
CharSourceRange::getCharRange(Location, Tok.getEndLoc()))) {
continue;
diff --git a/clang/unittests/Format/NumericLiteralCaseTest.cpp b/clang/unittests/Format/NumericLiteralCaseTest.cpp
index ecd230d73f692..f59fbed2c5ab0 100644
--- a/clang/unittests/Format/NumericLiteralCaseTest.cpp
+++ b/clang/unittests/Format/NumericLiteralCaseTest.cpp
@@ -340,6 +340,32 @@ TEST_F(NumericLiteralCaseTest, UnderScoreSeparatorLanguages) {
verifyFormat("o = 0o0_10_010;", "o = 0O0_10_010;", Style);
}
+TEST_F(NumericLiteralCaseTest, IgnoresMacroInvocations) {
+ // Arguments of a likely macro invocation aren't literals.
+ constexpr StringRef A("FOO(0xabc);");
+ constexpr StringRef B("FOO(bar(1), 0xabc);");
+ // Non-macro contexts, where literals are still formatted.
+ constexpr StringRef C("foo(0xabc);");
+ constexpr StringRef D("FOO(1) + 0xabc;");
+ constexpr StringRef E("T(0xabc);");
+ constexpr StringRef F("NS::FOO(0xabc);");
+ verifyFormat(A);
+ verifyFormat(B);
+ verifyFormat(C);
+ verifyFormat(D);
+ verifyFormat(E);
+ verifyFormat(F);
+
+ auto Style = getLLVMStyle();
+ Style.NumericLiteralCase.HexDigit = FormatStyle::NLCS_Upper;
+ verifyFormat(A, Style);
+ verifyFormat(B, Style);
+ verifyFormat("foo(0xABC);", C, Style);
+ verifyFormat("FOO(1) + 0xABC;", D, Style);
+ verifyFormat("T(0xABC);", E, Style);
+ verifyFormat("NS::FOO(0xABC);", F, Style);
+}
+
} // namespace
} // namespace test
} // namespace format
>From 8c051f3ba1809cf2c559252c6f5caaa53d72e659 Mon Sep 17 00:00:00 2001
From: LeGusto <janusonis.gustas at gmail.com>
Date: Sun, 6 Sep 2026 02:38:41 +0300
Subject: [PATCH 2/2] Change custom macro detection logic to instead check
WhitespaceSensitiveMacros
---
clang/lib/Format/NumericLiteralCaseFixer.cpp | 23 ++++++------
.../Format/NumericLiteralCaseTest.cpp | 36 ++++++++++++-------
2 files changed, 34 insertions(+), 25 deletions(-)
diff --git a/clang/lib/Format/NumericLiteralCaseFixer.cpp b/clang/lib/Format/NumericLiteralCaseFixer.cpp
index ba55ebdcb8f41..a28853c36f65b 100644
--- a/clang/lib/Format/NumericLiteralCaseFixer.cpp
+++ b/clang/lib/Format/NumericLiteralCaseFixer.cpp
@@ -16,6 +16,7 @@
#include "NumericLiteralInfo.h"
#include "llvm/ADT/StringExtras.h"
+#include "llvm/ADT/StringSet.h"
#include <algorithm>
@@ -43,11 +44,6 @@ static bool isNumericLiteralCaseFixerNeeded(const FormatStyle &Style) {
Option.ExponentLetter != Leave || Option.Suffix != Leave;
}
-// An all-caps identifier is likely a macro; see FormatToken::isPossibleMacro.
-static bool isPossibleMacroName(StringRef Name) {
- return Name.size() > 1 && Name == Name.upper();
-}
-
static std::string
transformComponent(StringRef Component,
FormatStyle::NumericLiteralComponentStyle ConfigValue) {
@@ -135,6 +131,12 @@ NumericLiteralCaseFixer::process(const Environment &Env,
const auto &SourceMgr = Env.getSourceManager();
AffectedRangeManager AffectedRangeMgr(SourceMgr, Env.getCharRanges());
+ // Don't recase pp-numbers inside whitespace-sensitive macros (e.g.
+ // STRINGIZE(0xa)); recasing a stringized argument would change the result.
+ llvm::StringSet<> UntouchableMacros;
+ for (StringRef Macro : Style.WhitespaceSensitiveMacros)
+ UntouchableMacros.insert(Macro);
+
const auto ID = Env.getFileID();
const auto LangOpts = getFormattingLangOpts(Style);
Lexer Lex(ID, SourceMgr.getBufferOrFake(ID), SourceMgr, LangOpts);
@@ -142,14 +144,12 @@ NumericLiteralCaseFixer::process(const Environment &Env,
Token Tok;
tooling::Replacements Result;
- int MacroArgDepth = 0; // Paren depth inside a possible macro invocation.
+ int MacroArgDepth = 0; // Paren depth inside an untouchable macro invocation.
- for (bool Skip = false, AfterMacroName = false, AfterScope = false;
- !Lex.LexFromRawLexer(Tok);) {
- // Leave arguments of a likely macro invocation (e.g. FOO(0xa)) untouched.
+ for (bool Skip = false, AfterMacroName = false; !Lex.LexFromRawLexer(Tok);) {
+ // Track whether we are inside the argument list of an untouchable macro.
if (Tok.is(tok::raw_identifier)) {
- AfterMacroName =
- !AfterScope && isPossibleMacroName(Tok.getRawIdentifier());
+ AfterMacroName = UntouchableMacros.contains(Tok.getRawIdentifier());
} else if (Tok.is(tok::l_paren)) {
if (MacroArgDepth > 0 || AfterMacroName)
++MacroArgDepth;
@@ -159,7 +159,6 @@ NumericLiteralCaseFixer::process(const Environment &Env,
--MacroArgDepth;
AfterMacroName = false;
}
- AfterScope = Tok.is(tok::coloncolon);
// Skip tokens that are too small to contain a formattable literal.
// Size=2 is the smallest possible literal that could contain formattable
diff --git a/clang/unittests/Format/NumericLiteralCaseTest.cpp b/clang/unittests/Format/NumericLiteralCaseTest.cpp
index f59fbed2c5ab0..f1b9f1e6b3e08 100644
--- a/clang/unittests/Format/NumericLiteralCaseTest.cpp
+++ b/clang/unittests/Format/NumericLiteralCaseTest.cpp
@@ -340,15 +340,19 @@ TEST_F(NumericLiteralCaseTest, UnderScoreSeparatorLanguages) {
verifyFormat("o = 0o0_10_010;", "o = 0O0_10_010;", Style);
}
-TEST_F(NumericLiteralCaseTest, IgnoresMacroInvocations) {
- // Arguments of a likely macro invocation aren't literals.
- constexpr StringRef A("FOO(0xabc);");
- constexpr StringRef B("FOO(bar(1), 0xabc);");
- // Non-macro contexts, where literals are still formatted.
- constexpr StringRef C("foo(0xabc);");
- constexpr StringRef D("FOO(1) + 0xabc;");
- constexpr StringRef E("T(0xabc);");
- constexpr StringRef F("NS::FOO(0xabc);");
+TEST_F(NumericLiteralCaseTest, IgnoresWhitespaceSensitiveMacros) {
+ // Arguments of a whitespace-sensitive macro (e.g. a stringizing macro) must
+ // not be recased; everything else still is.
+ constexpr StringRef A("a = STRINGIZE(0xabc);");
+ constexpr StringRef B("b = PP_STRINGIZE(0xa, 0xb);");
+ // Nested parens and multiple literals inside the macro are all left alone.
+ constexpr StringRef C("c = STRINGIZE(f(0xa) + 0xb);");
+ // A literal outside the macro is still formatted.
+ constexpr StringRef D("d = STRINGIZE(1) + 0xabc;");
+ // A macro that is not whitespace-sensitive, and a plain literal, are still
+ // formatted.
+ constexpr StringRef E("e = FOO(0xabc);");
+ constexpr StringRef F("f = 0xabc;");
verifyFormat(A);
verifyFormat(B);
verifyFormat(C);
@@ -358,12 +362,18 @@ TEST_F(NumericLiteralCaseTest, IgnoresMacroInvocations) {
auto Style = getLLVMStyle();
Style.NumericLiteralCase.HexDigit = FormatStyle::NLCS_Upper;
+ // STRINGIZE and PP_STRINGIZE are whitespace-sensitive by default.
verifyFormat(A, Style);
verifyFormat(B, Style);
- verifyFormat("foo(0xABC);", C, Style);
- verifyFormat("FOO(1) + 0xABC;", D, Style);
- verifyFormat("T(0xABC);", E, Style);
- verifyFormat("NS::FOO(0xABC);", F, Style);
+ verifyFormat(C, Style);
+ verifyFormat("d = STRINGIZE(1) + 0xABC;", D, Style);
+ verifyFormat("e = FOO(0xABC);", E, Style);
+ verifyFormat("f = 0xABC;", F, Style);
+
+ // A user-declared macro is honored too, including its nested arguments.
+ Style.WhitespaceSensitiveMacros.push_back("FOO");
+ verifyFormat(E, Style);
+ verifyFormat("g = FOO(h(0xa), 0xbc);", Style);
}
} // namespace
More information about the cfe-commits
mailing list