[clang-tools-extra] [clang-tidy] Add TrailingUnderscore option for naming-check (PR #221452)

Baranov Victor via cfe-commits cfe-commits at lists.llvm.org
Sat Sep 5 07:43:32 PDT 2026


https://github.com/vbvictor created https://github.com/llvm/llvm-project/pull/221452

Fixes https://github.com/llvm/llvm-project/issues/154756.

>From 63c455845db615fc5185c95d088b6867fea5980b Mon Sep 17 00:00:00 2001
From: Victor Baranov <bar.victor.2002 at gmail.com>
Date: Sat, 5 Sep 2026 17:42:31 +0300
Subject: [PATCH] [clang-tidy] Add TrailingUnderscore option for naming-check

---
 .../readability/IdentifierNamingCheck.cpp     | 37 +++++++---
 .../readability/IdentifierNamingCheck.h       | 20 ++++--
 clang-tools-extra/docs/ReleaseNotes.md        |  3 +
 .../checks/readability/identifier-naming.rst  | 30 ++++++++
 ...ifier-naming-allow-trailing-underscore.cpp | 72 +++++++++++++++++++
 5 files changed, 146 insertions(+), 16 deletions(-)
 create mode 100644 clang-tools-extra/test/clang-tidy/checkers/readability/identifier-naming-allow-trailing-underscore.cpp

diff --git a/clang-tools-extra/clang-tidy/readability/IdentifierNamingCheck.cpp b/clang-tools-extra/clang-tidy/readability/IdentifierNamingCheck.cpp
index e3a39d7b99155..c1e6eeab51784 100644
--- a/clang-tools-extra/clang-tidy/readability/IdentifierNamingCheck.cpp
+++ b/clang-tools-extra/clang-tidy/readability/IdentifierNamingCheck.cpp
@@ -293,8 +293,14 @@ IdentifierNamingCheck::FileStyle IdentifierNamingCheck::getFileStyleFromOptions(
       Options.get("CheckAnonFieldInParent", false);
   const bool TypedefInheritAnonTagConfig =
       Options.get("TypedefInheritAnonTagConfig", false);
-  return {std::move(Styles), std::move(HNOption), IgnoreMainLike,
-          CheckAnonFieldInParent, TypedefInheritAnonTagConfig};
+  const bool AllowTrailingUnderscore =
+      Options.get("AllowTrailingUnderscore", false);
+  return {std::move(Styles),
+          std::move(HNOption),
+          IgnoreMainLike,
+          CheckAnonFieldInParent,
+          TypedefInheritAnonTagConfig,
+          AllowTrailingUnderscore};
 }
 
 std::string IdentifierNamingCheck::HungarianNotation::getDeclTypeName(
@@ -866,13 +872,15 @@ void IdentifierNamingCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
                 MainFileStyle->isCheckingAnonFieldInParentScope());
   Options.store(Opts, "TypedefInheritAnonTagConfig",
                 MainFileStyle->isTypedefInheritingAnonTagConfig());
+  Options.store(Opts, "AllowTrailingUnderscore",
+                MainFileStyle->isAllowingTrailingUnderscore());
 }
 
 bool IdentifierNamingCheck::matchesStyle(
     StringRef Type, StringRef Name,
     const IdentifierNamingCheck::NamingStyle &Style,
     const IdentifierNamingCheck::HungarianNotationOption &HNOption,
-    const NamedDecl *Decl) const {
+    const NamedDecl *Decl, bool AllowTrailingUnderscore) const {
   static const llvm::Regex Matchers[] = {
       llvm::Regex("^.*$"),
       llvm::Regex("^[a-z][a-z0-9_]*$"),
@@ -886,6 +894,8 @@ bool IdentifierNamingCheck::matchesStyle(
 
   if (!Name.consume_front(Style.Prefix))
     return false;
+  if (AllowTrailingUnderscore)
+    Name.consume_back("_");
   if (!Name.consume_back(Style.Suffix))
     return false;
   if (IdentifierNamingCheck::HungarianPrefixType::HPT_Off != Style.HPType) {
@@ -1095,8 +1105,10 @@ std::string IdentifierNamingCheck::fixupWithStyle(
     StringRef Type, StringRef Name,
     const IdentifierNamingCheck::NamingStyle &Style,
     const IdentifierNamingCheck::HungarianNotationOption &HNOption,
-    const Decl *D) const {
+    const Decl *D, bool AllowTrailingUnderscore) const {
   Name.consume_front(Style.Prefix);
+  const bool KeepTrailingUnderscore =
+      AllowTrailingUnderscore && Name.consume_back("_");
   Name.consume_back(Style.Suffix);
   std::string Fixed = fixupWithCase(
       Type, Name, D, Style, HNOption,
@@ -1118,7 +1130,9 @@ std::string IdentifierNamingCheck::fixupWithStyle(
   if (Mid.empty())
     Mid = "_";
 
-  return (Style.Prefix + HungarianPrefix + Mid + Style.Suffix).str();
+  return (Style.Prefix + HungarianPrefix + Mid + Style.Suffix +
+          (KeepTrailingUnderscore ? "_" : ""))
+      .str();
 }
 
 /// Returns \c true if \p Style can reject a name. A Hungarian prefix cannot
@@ -1361,7 +1375,8 @@ IdentifierNamingCheck::getFailureInfo(
     SourceLocation Location,
     ArrayRef<std::optional<IdentifierNamingCheck::NamingStyle>> NamingStyles,
     const IdentifierNamingCheck::HungarianNotationOption &HNOption,
-    StyleKind SK, const SourceManager &SM, bool IgnoreFailedSplit) const {
+    StyleKind SK, const SourceManager &SM, bool IgnoreFailedSplit,
+    bool AllowTrailingUnderscore) const {
   if (SK == SK_Invalid)
     return std::nullopt;
 
@@ -1373,7 +1388,7 @@ IdentifierNamingCheck::getFailureInfo(
   if (Style.IgnoredRegexp.isValid() && Style.IgnoredRegexp.match(Name))
     return std::nullopt;
 
-  if (matchesStyle(Type, Name, Style, HNOption, ND))
+  if (matchesStyle(Type, Name, Style, HNOption, ND, AllowTrailingUnderscore))
     return std::nullopt;
 
   std::string KindName =
@@ -1383,7 +1398,8 @@ IdentifierNamingCheck::getFailureInfo(
                           IdentifierNamingCheck::CT_LowerCase);
   llvm::replace(KindName, '_', ' ');
 
-  std::string Fixup = fixupWithStyle(Type, Name, Style, HNOption, ND);
+  std::string Fixup =
+      fixupWithStyle(Type, Name, Style, HNOption, ND, AllowTrailingUnderscore);
   if (StringRef(Fixup) == Name) {
     if (!IgnoreFailedSplit) {
       LLVM_DEBUG(Location.print(llvm::dbgs(), SM);
@@ -1416,7 +1432,7 @@ IdentifierNamingCheck::getDeclFailureInfo(const NamedDecl *Decl,
                     FileStyle.isIgnoringMainLikeFunction(),
                     FileStyle.isCheckingAnonFieldInParentScope(),
                     FileStyle.isTypedefInheritingAnonTagConfig()),
-      SM, IgnoreFailedSplit);
+      SM, IgnoreFailedSplit, FileStyle.isAllowingTrailingUnderscore());
 }
 
 std::optional<RenamerClangTidyCheck::FailureInfo>
@@ -1434,7 +1450,8 @@ IdentifierNamingCheck::getMacroFailureInfo(const Token &MacroNameTok,
 
   return getFailureInfo("", MacroNameTok.getIdentifierInfo()->getName(),
                         nullptr, Loc, Style.getStyles(), Style.getHNOption(),
-                        UsedKind, SM, IgnoreFailedSplit);
+                        UsedKind, SM, IgnoreFailedSplit,
+                        Style.isAllowingTrailingUnderscore());
 }
 
 RenamerClangTidyCheck::DiagInfo
diff --git a/clang-tools-extra/clang-tidy/readability/IdentifierNamingCheck.h b/clang-tools-extra/clang-tidy/readability/IdentifierNamingCheck.h
index 8f20acc6030cb..e66beba64d49b 100644
--- a/clang-tools-extra/clang-tidy/readability/IdentifierNamingCheck.h
+++ b/clang-tools-extra/clang-tidy/readability/IdentifierNamingCheck.h
@@ -127,14 +127,16 @@ class IdentifierNamingCheck final : public RenamerClangTidyCheck {
   struct FileStyle {
     FileStyle()
         : IsActive(false), IgnoreMainLikeFunctions(false),
-          TypedefInheritAnonTagConfig(false) {}
+          TypedefInheritAnonTagConfig(false), AllowTrailingUnderscore(false) {}
     FileStyle(SmallVectorImpl<std::optional<NamingStyle>> &&Styles,
               HungarianNotationOption HNOption, bool IgnoreMainLike,
-              bool CheckAnonFieldInParent, bool TypedefInheritAnonTag)
+              bool CheckAnonFieldInParent, bool TypedefInheritAnonTag,
+              bool AllowTrailingUnderscore)
         : Styles(std::move(Styles)), HNOption(std::move(HNOption)),
           IsActive(true), IgnoreMainLikeFunctions(IgnoreMainLike),
           CheckAnonFieldInParentScope(CheckAnonFieldInParent),
-          TypedefInheritAnonTagConfig(TypedefInheritAnonTag) {}
+          TypedefInheritAnonTagConfig(TypedefInheritAnonTag),
+          AllowTrailingUnderscore(AllowTrailingUnderscore) {}
 
     ArrayRef<std::optional<NamingStyle>> getStyles() const {
       assert(IsActive);
@@ -157,6 +159,10 @@ class IdentifierNamingCheck final : public RenamerClangTidyCheck {
       return TypedefInheritAnonTagConfig;
     }
 
+    bool isAllowingTrailingUnderscore() const {
+      return AllowTrailingUnderscore;
+    }
+
   private:
     SmallVector<std::optional<NamingStyle>, 0> Styles;
     HungarianNotationOption HNOption;
@@ -164,6 +170,7 @@ class IdentifierNamingCheck final : public RenamerClangTidyCheck {
     bool IgnoreMainLikeFunctions;
     bool CheckAnonFieldInParentScope;
     bool TypedefInheritAnonTagConfig;
+    bool AllowTrailingUnderscore;
   };
 
   IdentifierNamingCheck::FileStyle
@@ -173,7 +180,7 @@ class IdentifierNamingCheck final : public RenamerClangTidyCheck {
   matchesStyle(StringRef Type, StringRef Name,
                const IdentifierNamingCheck::NamingStyle &Style,
                const IdentifierNamingCheck::HungarianNotationOption &HNOption,
-               const NamedDecl *Decl) const;
+               const NamedDecl *Decl, bool AllowTrailingUnderscore) const;
 
   std::string
   fixupWithCase(StringRef Type, StringRef Name, const Decl *D,
@@ -185,7 +192,7 @@ class IdentifierNamingCheck final : public RenamerClangTidyCheck {
   fixupWithStyle(StringRef Type, StringRef Name,
                  const IdentifierNamingCheck::NamingStyle &Style,
                  const IdentifierNamingCheck::HungarianNotationOption &HNOption,
-                 const Decl *D) const;
+                 const Decl *D, bool AllowTrailingUnderscore) const;
 
   StyleKind findStyleKind(
       const NamedDecl *D,
@@ -198,7 +205,8 @@ class IdentifierNamingCheck final : public RenamerClangTidyCheck {
       SourceLocation Location,
       ArrayRef<std::optional<IdentifierNamingCheck::NamingStyle>> NamingStyles,
       const IdentifierNamingCheck::HungarianNotationOption &HNOption,
-      StyleKind SK, const SourceManager &SM, bool IgnoreFailedSplit) const;
+      StyleKind SK, const SourceManager &SM, bool IgnoreFailedSplit,
+      bool AllowTrailingUnderscore) const;
 
   bool isParamInMainLikeFunction(const ParmVarDecl &ParmDecl,
                                  bool IncludeMainLike) const;
diff --git a/clang-tools-extra/docs/ReleaseNotes.md b/clang-tools-extra/docs/ReleaseNotes.md
index b32b8a51e0606..3a81a24f3f70f 100644
--- a/clang-tools-extra/docs/ReleaseNotes.md
+++ b/clang-tools-extra/docs/ReleaseNotes.md
@@ -233,6 +233,9 @@ infrastructure are described first, followed by tool-specific sections.
     the new `LambdaCapture` options. Simple, non-init captures continue to follow
     the naming style of the variable they capture.
 
+  - Added the {option}`AllowTrailingUnderscore` option, which permits a
+    single trailing underscore on any identifier.
+
 - Improved {doc}`readability-named-parameter
   <clang-tidy/checks/readability/named-parameter>` check by ignoring
   standard tag types (e.g. `std::in_place_t`, `std::allocator_arg_t`,
diff --git a/clang-tools-extra/docs/clang-tidy/checks/readability/identifier-naming.rst b/clang-tools-extra/docs/clang-tidy/checks/readability/identifier-naming.rst
index b177577df311e..1014d5c6d17a3 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/readability/identifier-naming.rst
+++ b/clang-tools-extra/docs/clang-tidy/checks/readability/identifier-naming.rst
@@ -82,6 +82,7 @@ The available options are summarized below:
 **General options**
 
  - :option:`AggressiveDependentMemberLookup`
+ - :option:`AllowTrailingUnderscore`
  - :option:`CheckAnonFieldInParent`
  - :option:`GetConfigPerFile`
  - :option:`IgnoreMainLikeFunctions`
@@ -396,6 +397,35 @@ After if AggressiveDependentMemberLookup is `true`:
       }
     };
 
+.. option:: AllowTrailingUnderscore
+
+    When set to `true`, a single trailing underscore is allowed on any
+    identifier, in addition to whatever casing, prefix and suffix are
+    otherwise configured for its kind.
+
+For example using values of:
+
+   - AllowTrailingUnderscore of `true`
+   - LocalVariableCase of ``camelBack``
+
+Transforms names as follows:
+
+Before:
+
+.. code-block:: c++
+
+    void f(int value) {
+      int Value_ = value;
+    }
+
+After:
+
+.. code-block:: c++
+
+    void f(int value) {
+      int value_ = value;
+    }
+
 .. option:: CheckAnonFieldInParent
 
     When set to `true`, fields in anonymous records (i.e. anonymous
diff --git a/clang-tools-extra/test/clang-tidy/checkers/readability/identifier-naming-allow-trailing-underscore.cpp b/clang-tools-extra/test/clang-tidy/checkers/readability/identifier-naming-allow-trailing-underscore.cpp
new file mode 100644
index 0000000000000..a36d9ab778a82
--- /dev/null
+++ b/clang-tools-extra/test/clang-tidy/checkers/readability/identifier-naming-allow-trailing-underscore.cpp
@@ -0,0 +1,72 @@
+// RUN: %check_clang_tidy -std=c++14-or-later %s readability-identifier-naming %t -- \
+// RUN:   -config='{CheckOptions: { \
+// RUN:     readability-identifier-naming.LocalVariableCase: lower_case, \
+// RUN:     readability-identifier-naming.ParameterCase: UPPER_CASE, \
+// RUN:     readability-identifier-naming.FunctionCase: CamelCase, \
+// RUN:     readability-identifier-naming.LambdaCaptureCase: camel_Snake_Back, \
+// RUN:     readability-identifier-naming.MemberCase: lower_case, \
+// RUN:     readability-identifier-naming.MemberSuffix: '_impl', \
+// RUN:   }}'
+
+// RUN: %check_clang_tidy -std=c++14-or-later -check-suffixes=ALLOWED %s \
+// RUN:   readability-identifier-naming %t -- \
+// RUN:   -config='{CheckOptions: { \
+// RUN:     readability-identifier-naming.AllowTrailingUnderscore: true, \
+// RUN:     readability-identifier-naming.LocalVariableCase: lower_case, \
+// RUN:     readability-identifier-naming.ParameterCase: UPPER_CASE, \
+// RUN:     readability-identifier-naming.FunctionCase: CamelCase, \
+// RUN:     readability-identifier-naming.LambdaCaptureCase: camel_Snake_Back, \
+// RUN:     readability-identifier-naming.MemberCase: lower_case, \
+// RUN:     readability-identifier-naming.MemberSuffix: '_impl', \
+// RUN:   }}'
+
+void Positive(int TRANSLATOR) {
+  int translator_ = TRANSLATOR;
+  // CHECK-MESSAGES: :[[@LINE-1]]:7: warning: invalid case style for local variable 'translator_' [readability-identifier-naming]
+  // CHECK-FIXES: int translator = TRANSLATOR;
+}
+
+void WrongCaseKeepsUnderscore() {
+  int Bad_Name_;
+  // CHECK-MESSAGES: :[[@LINE-1]]:7: warning: invalid case style for local variable 'Bad_Name_'
+  // CHECK-MESSAGES-ALLOWED: :[[@LINE-2]]:7: warning: invalid case style for local variable 'Bad_Name_'
+  // CHECK-FIXES: int bad_name;
+  // CHECK-FIXES-ALLOWED: int bad_name_;
+}
+
+void ExtraUnderscoresRejected() {
+  int bad__;
+  // CHECK-MESSAGES: :[[@LINE-1]]:7: warning: invalid case style for local variable 'bad__'
+  // CHECK-MESSAGES-ALLOWED: :[[@LINE-2]]:7: warning: invalid case style for local variable 'bad__'
+  // CHECK-FIXES: int bad;
+  // CHECK-FIXES-ALLOWED: int bad_;
+}
+
+void LeadingUnderscoreRejected() {
+  int _bad;
+  // CHECK-MESSAGES: :[[@LINE-1]]:7: warning: invalid case style for local variable '_bad'
+  // CHECK-MESSAGES-ALLOWED: :[[@LINE-2]]:7: warning: invalid case style for local variable '_bad'
+  // CHECK-FIXES: int bad;
+  // CHECK-FIXES-ALLOWED: int bad;
+}
+
+void TakesParam(int VALUE_) {
+  // CHECK-MESSAGES: :[[@LINE-1]]:21: warning: invalid case style for parameter 'VALUE_'
+  // CHECK-FIXES: void TakesParam(int VALUE) {
+}
+
+void Helper_() {}
+// CHECK-MESSAGES: :[[@LINE-1]]:6: warning: invalid case style for function 'Helper_'
+// CHECK-FIXES: void Helper() {}
+
+void LambdaCapture(int VALUE) {
+  auto lambda = [value_Snake_ = VALUE]() { return value_Snake_; };
+  // CHECK-MESSAGES: :[[@LINE-1]]:18: warning: invalid case style for lambda capture 'value_Snake_'
+  // CHECK-FIXES: auto lambda = [value_Snake = VALUE]() { return value_Snake; };
+}
+
+struct MemberSuffix {
+  int foo_impl_;
+  // CHECK-MESSAGES: :[[@LINE-1]]:7: warning: invalid case style for member 'foo_impl_'
+  // CHECK-FIXES: int foo_impl_impl;
+};



More information about the cfe-commits mailing list