[clang] b8e5ea1 - [clang] Improve diagnostics for invalid named-universal-characters (#206326)
via cfe-commits
cfe-commits at lists.llvm.org
Wed Jul 1 04:14:52 PDT 2026
Author: Jan Schultke
Date: 2026-07-01T13:14:48+02:00
New Revision: b8e5ea1ab3e206d861b10d124b77e1026c2a2eb4
URL: https://github.com/llvm/llvm-project/commit/b8e5ea1ab3e206d861b10d124b77e1026c2a2eb4
DIFF: https://github.com/llvm/llvm-project/commit/b8e5ea1ab3e206d861b10d124b77e1026c2a2eb4.diff
LOG: [clang] Improve diagnostics for invalid named-universal-characters (#206326)
1. Fix typo in `note_invalid_ucn_name_loose_matching` message.
2. Fix unprintable characters appearing in diagnostic messages.
3. Stop offering low-value fix suggestions when illegal characters
appear in the name.
Added:
Modified:
clang/include/clang/Basic/Diagnostic.h
clang/include/clang/Basic/DiagnosticLexKinds.td
clang/lib/Basic/Diagnostic.cpp
clang/lib/Lex/LiteralSupport.cpp
clang/test/CXX/drs/cwg26xx.cpp
clang/test/Lexer/char-escapes-delimited.c
clang/test/Lexer/unicode.c
clang/test/Preprocessor/ucn-pp-identifier.c
clang/unittests/Basic/DiagnosticTest.cpp
Removed:
################################################################################
diff --git a/clang/include/clang/Basic/Diagnostic.h b/clang/include/clang/Basic/Diagnostic.h
index 826b747f2c751..c033320687078 100644
--- a/clang/include/clang/Basic/Diagnostic.h
+++ b/clang/include/clang/Basic/Diagnostic.h
@@ -28,6 +28,7 @@
#include "llvm/ADT/StringExtras.h"
#include "llvm/ADT/iterator_range.h"
#include "llvm/Support/Compiler.h"
+#include "llvm/Support/ConvertUTF.h"
#include <cassert>
#include <cstdint>
#include <limits>
@@ -1876,6 +1877,7 @@ void ProcessWarningOptions(DiagnosticsEngine &Diags,
const DiagnosticOptions &Opts,
llvm::vfs::FileSystem &VFS, bool ReportDiags = true);
void EscapeStringForDiagnostic(StringRef Str, SmallVectorImpl<char> &OutStr);
+llvm::SmallString<16> DisplayCodePointForDiagnostic(llvm::UTF32 CodePoint);
} // namespace clang
#endif // LLVM_CLANG_BASIC_DIAGNOSTIC_H
diff --git a/clang/include/clang/Basic/DiagnosticLexKinds.td b/clang/include/clang/Basic/DiagnosticLexKinds.td
index 383bf1a7fdb3f..decfd6c781dbf 100644
--- a/clang/include/clang/Basic/DiagnosticLexKinds.td
+++ b/clang/include/clang/Basic/DiagnosticLexKinds.td
@@ -170,9 +170,11 @@ def err_hex_escape_no_digits : Error<
def err_invalid_ucn_name : Error<
"'%0' is not a valid Unicode character name">;
def note_invalid_ucn_name_loose_matching : Note<
- "characters names in Unicode escape sequences are sensitive to case and whitespaces">;
+ "character names in Unicode escape sequences are sensitive to case and whitespaces">;
def note_invalid_ucn_name_candidate : Note<
- "did you mean %0 ('%2' U+%1)?">;
+ "did you mean %0 (%1)?">;
+def note_invalid_ucn_name_character : Note<
+ "character %0 cannot appear in a Unicode character name">;
def warn_ucn_escape_no_digits : Warning<
"\\%0 used with no following hex digits; "
diff --git a/clang/lib/Basic/Diagnostic.cpp b/clang/lib/Basic/Diagnostic.cpp
index 4802478c379bb..57fa8a16d3a50 100644
--- a/clang/lib/Basic/Diagnostic.cpp
+++ b/clang/lib/Basic/Diagnostic.cpp
@@ -1085,6 +1085,26 @@ void clang::EscapeStringForDiagnostic(StringRef Str,
}
}
+/// Displays a single Unicode codepoint in U+NNNN notation, optionally
+/// prepending the quoted codepoint itself if printable.
+llvm::SmallString<16>
+clang::DisplayCodePointForDiagnostic(llvm::UTF32 CodePoint) {
+ llvm::SmallString<16> Result;
+ if (llvm::sys::unicode::isPrintable(CodePoint)) {
+ std::string CharUTF8;
+ llvm::convertUTF32ToUTF8String(llvm::ArrayRef<llvm::UTF32>(&CodePoint, 1),
+ CharUTF8);
+ Result.append("'");
+ Result.append(CharUTF8);
+ Result.append("' U+");
+ } else {
+ Result.append("U+");
+ }
+ llvm::raw_svector_ostream OS(Result);
+ llvm::write_hex(OS, CodePoint, llvm::HexPrintStyle::Upper, 4);
+ return Result;
+}
+
void Diagnostic::FormatDiagnostic(const char *DiagStr, const char *DiagEnd,
SmallVectorImpl<char> &OutStr) const {
// When the diagnostic string is only "%0", the entire string is being given
diff --git a/clang/lib/Lex/LiteralSupport.cpp b/clang/lib/Lex/LiteralSupport.cpp
index 482146ccf8654..72a65dd156b19 100644
--- a/clang/lib/Lex/LiteralSupport.cpp
+++ b/clang/lib/Lex/LiteralSupport.cpp
@@ -539,6 +539,11 @@ static bool ProcessNumericUCNEscape(const char *ThisTokBegin,
return !HasError;
}
+static bool allowedInCharacterName(char C) {
+ return (C >= 'A' && C <= 'Z') || (C >= '0' && C <= '9') || C == '-' ||
+ C == ' ';
+}
+
static void DiagnoseInvalidUnicodeCharacterName(
DiagnosticsEngine *Diags, const LangOptions &Features, FullSourceLoc Loc,
const char *TokBegin, const char *TokRangeBegin, const char *TokRangeEnd,
@@ -550,6 +555,27 @@ static void DiagnoseInvalidUnicodeCharacterName(
namespace u = llvm::sys::unicode;
+ bool HasIllegalCharacter = false;
+ for (const char *P = Name.begin(), *E = Name.end(); P != E;) {
+ if (allowedInCharacterName(*P)) {
+ ++P;
+ continue;
+ }
+ const auto *Src = reinterpret_cast<const llvm::UTF8 *>(P);
+ const auto *SrcEnd = reinterpret_cast<const llvm::UTF8 *>(E);
+ llvm::UTF32 CodePoint = 0;
+ if (llvm::convertUTF8Sequence(&Src, SrcEnd, &CodePoint,
+ llvm::strictConversion) != llvm::conversionOK)
+ break;
+ SourceLocation CharLoc = Lexer::AdvanceToTokenCharacter(
+ Loc, (TokRangeBegin - TokBegin) + (P - Name.begin()), Loc.getManager(),
+ Features);
+ Diags->Report(CharLoc, diag::note_invalid_ucn_name_character)
+ << DisplayCodePointForDiagnostic(CodePoint);
+ HasIllegalCharacter = true;
+ break;
+ }
+
std::optional<u::LooseMatchingResult> Res =
u::nameToCodepointLooseMatching(Name);
if (Res) {
@@ -562,6 +588,12 @@ static void DiagnoseInvalidUnicodeCharacterName(
return;
}
+ // Providing illegal characters suggests a fundamental misuse of the feature,
+ // like providing emoji in \N{}. Offering alternative suggestions is often
+ // unhelpful in that scenario.
+ if (HasIllegalCharacter)
+ return;
+
unsigned Distance = 0;
SmallVector<u::MatchForCodepointName> Matches =
u::nearestMatchesForCodepointName(Name, 5);
@@ -576,17 +608,9 @@ static void DiagnoseInvalidUnicodeCharacterName(
break;
Distance = Match.Distance;
- std::string Str;
- llvm::UTF32 V = Match.Value;
- bool Converted =
- llvm::convertUTF32ToUTF8String(llvm::ArrayRef<llvm::UTF32>(&V, 1), Str);
- (void)Converted;
- assert(Converted && "Found a match wich is not a unicode character");
-
Diag(Diags, Features, Loc, TokBegin, TokRangeBegin, TokRangeEnd,
diag::note_invalid_ucn_name_candidate)
- << Match.Name << llvm::utohexstr(Match.Value)
- << Str // FIXME: Fix the rendering of non printable characters
+ << Match.Name << DisplayCodePointForDiagnostic(Match.Value)
<< FixItHint::CreateReplacement(
MakeCharSourceRange(Features, Loc, TokBegin, TokRangeBegin,
TokRangeEnd),
diff --git a/clang/test/CXX/drs/cwg26xx.cpp b/clang/test/CXX/drs/cwg26xx.cpp
index 45653743ae574..6a15513879bbd 100644
--- a/clang/test/CXX/drs/cwg26xx.cpp
+++ b/clang/test/CXX/drs/cwg26xx.cpp
@@ -214,11 +214,7 @@ int \N{Λ} = 0;
// expected-error at -2 {{expected unqualified-id}}
const char* emoji = "\N{🤡}";
// expected-error at -1 {{'🤡' is not a valid Unicode character name}}
-// expected-note at -2 {{did you mean OX ('🐂' U+1F402)?}}
-// expected-note at -3 {{did you mean ANT ('🐜' U+1F41C)?}}
-// expected-note at -4 {{did you mean ARC ('⌒' U+2312)?}}
-// expected-note at -5 {{did you mean AXE ('🪓' U+1FA93)?}}
-// expected-note at -6 {{did you mean BAT ('🦇' U+1F987)?}}
+// expected-note at -2 {{character '🤡' U+1F921 cannot appear in a Unicode character name}}
#define z(x) 0
#define cwg2640_a z(
diff --git a/clang/test/Lexer/char-escapes-delimited.c b/clang/test/Lexer/char-escapes-delimited.c
index 7a8986bc5f867..1c20456701d75 100644
--- a/clang/test/Lexer/char-escapes-delimited.c
+++ b/clang/test/Lexer/char-escapes-delimited.c
@@ -82,7 +82,8 @@ void named(void) {
char b = '\N{DOLLAR SIGN}'; // ext-warning {{extension}} cxx23-warning {{C++23}}
char b_ = '\N{ DOL-LAR _SIGN }'; // expected-error {{' DOL-LAR _SIGN ' is not a valid Unicode character name}} \
- // expected-note {{characters names in Unicode escape sequences are sensitive to case and whitespaces}}
+ // expected-note {{character names in Unicode escape sequences are sensitive to case and whitespaces}} \
+ // expected-note {{character '_' U+005F cannot appear in a Unicode character name}}
char c = '\N{NOTATHING}'; // expected-error {{'NOTATHING' is not a valid Unicode character name}} \
// expected-note 5{{did you mean}}
@@ -100,9 +101,12 @@ void named(void) {
unsigned k = u'\N{LOTUS'; // expected-error {{incomplete universal character name}}
const char* emoji = "\N{🤡}"; // expected-error {{'🤡' is not a valid Unicode character name}} \
- // expected-note 5{{did you mean}}
+ // expected-note {{character '🤡' U+1F921 cannot appear in a Unicode character name}}
const char* nested = "\N{\N{SPARKLE}}"; // expected-error {{'\N{SPARKLE' is not a valid Unicode character name}} \
- // expected-note 5{{did you mean}}
+ // expected-note {{cannot appear in a Unicode character name}}
+ const char* line_feed = "\N{LINE FEE}"; // expected-error {{'LINE FEE' is not a valid Unicode character name}} \
+ // expected-note {{did you mean LINE FEED (U+000A)}} \
+ // expected-note 4{{did you mean}}
}
void separators(void) {
diff --git a/clang/test/Lexer/unicode.c b/clang/test/Lexer/unicode.c
index e0489e11b9da9..2d1edc77a1550 100644
--- a/clang/test/Lexer/unicode.c
+++ b/clang/test/Lexer/unicode.c
@@ -49,7 +49,7 @@ extern int \u{16D80}; // CHISOI LETTER A - Added in Unicode 18.0
extern int a\N{TANGSA LETTER GA};
extern int a\N{TANGSALETTERGA}; // expected-error {{'TANGSALETTERGA' is not a valid Unicode character name}} \
// expected-error {{expected ';' after top level declarator}} \
- // expected-note {{characters names in Unicode escape sequences are sensitive to case and whitespace}}
+ // expected-note {{character names in Unicode escape sequences are sensitive to case and whitespace}}
extern int 𝛛; // expected-warning {{mathematical notation character <U+1D6DB> in an identifier is a Clang extension}}
extern int ₉; // expected-error {{character <U+2089> not allowed at the start of an identifier}} \\
diff --git a/clang/test/Preprocessor/ucn-pp-identifier.c b/clang/test/Preprocessor/ucn-pp-identifier.c
index 5efcfe48f638a..ee008a73eb882 100644
--- a/clang/test/Preprocessor/ucn-pp-identifier.c
+++ b/clang/test/Preprocessor/ucn-pp-identifier.c
@@ -131,7 +131,7 @@ C 1
// expected-error {{macro name must be an identifier}}
#define \NN // expected-warning {{incomplete universal character name; treating as '\' followed by identifier}} expected-error {{macro name must be an identifier}}
#define \N{GREEK_SMALL-LETTERALPHA} // expected-error {{'GREEK_SMALL-LETTERALPHA' is not a valid Unicode character name}} \
- // expected-note {{characters names in Unicode escape sequences are sensitive to case and whitespaces}}
+ // expected-note {{character names in Unicode escape sequences are sensitive to case and whitespaces}}
#define \N{🤡} // expected-error {{'🤡' is not a valid Unicode character name}} \
// expected-error {{macro name must be an identifier}}
diff --git a/clang/unittests/Basic/DiagnosticTest.cpp b/clang/unittests/Basic/DiagnosticTest.cpp
index 4ced52c8f715f..5cc0c39e5cb51 100644
--- a/clang/unittests/Basic/DiagnosticTest.cpp
+++ b/clang/unittests/Basic/DiagnosticTest.cpp
@@ -449,4 +449,24 @@ TEST_F(SuppressionMappingTest, CanonicalizesSlashesOnWindows) {
}
#endif
+TEST(DisplayCodePointForDiagnosticTest, printableDisplaysQuoted) {
+ EXPECT_EQ(DisplayCodePointForDiagnostic(U'A'), "'A' U+0041");
+ EXPECT_EQ(DisplayCodePointForDiagnostic(U'🤡'), "'🤡' U+1F921");
+ EXPECT_EQ(DisplayCodePointForDiagnostic(U' '), "' ' U+0020");
+}
+
+TEST(DisplayCodePointForDiagnosticTest, nonPrintableDisplaysNoQuoted) {
+ EXPECT_EQ(DisplayCodePointForDiagnostic(U'\n'), "U+000A");
+ EXPECT_EQ(DisplayCodePointForDiagnostic(U'\0'), "U+0000");
+ EXPECT_EQ(DisplayCodePointForDiagnostic(U'\x1B'), "U+001B");
+}
+
+TEST(DisplayCodePointForDiagnosticTest, nonScalarValues) {
+ // Low and high surrogates:
+ EXPECT_EQ(DisplayCodePointForDiagnostic(0xD800), "U+D800");
+ EXPECT_EQ(DisplayCodePointForDiagnostic(0xDFFF), "U+DFFF");
+ // Overly large values:
+ EXPECT_EQ(DisplayCodePointForDiagnostic(0x110000), "U+110000");
+}
+
} // namespace
More information about the cfe-commits
mailing list