[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