Skip to content

Commit 975d562

Browse files
authored
[clang-tidy] Add new check readability-trailing-comma (llvm#173669)
clang-format has a couple of similar options: https://clang.llvm.org/docs/ClangFormatStyleOptions.html#enumtrailingcomma - add trailing commas for enum https://clang.llvm.org/docs/ClangFormatStyleOptions.html#inserttrailingcommas - add trailing commas for C++ but generally they are marked with such warning: > Warning > > Setting this option to any value other than Leave could lead to incorrect code formatting due to clang-format’s lack of complete semantic information. As such, extra care should be taken to review code changes made by this option. clang-tidy on the other hand has all semantic information, thus can (hopefully) provide 0 false-positives. Note that we have already overlapping checks in clang-format/clang-tidy like: https://clang.llvm.org/docs/ClangFormatStyleOptions.html#insertbraces vs https://clang.llvm.org/extra/clang-tidy/checks/readability/braces-around-statements.html
1 parent c951d76 commit 975d562

14 files changed

Lines changed: 890 additions & 0 deletions

clang-tools-extra/clang-tidy/readability/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ add_clang_library(clangTidyReadabilityModule STATIC
5858
StaticDefinitionInAnonymousNamespaceCheck.cpp
5959
StringCompareCheck.cpp
6060
SuspiciousCallArgumentCheck.cpp
61+
TrailingCommaCheck.cpp
6162
UniqueptrDeleteReleaseCheck.cpp
6263
UppercaseLiteralSuffixCheck.cpp
6364
UseAnyOfAllOfCheck.cpp

clang-tools-extra/clang-tidy/readability/ReadabilityTidyModule.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
#include "StaticDefinitionInAnonymousNamespaceCheck.h"
6161
#include "StringCompareCheck.h"
6262
#include "SuspiciousCallArgumentCheck.h"
63+
#include "TrailingCommaCheck.h"
6364
#include "UniqueptrDeleteReleaseCheck.h"
6465
#include "UppercaseLiteralSuffixCheck.h"
6566
#include "UseAnyOfAllOfCheck.h"
@@ -177,6 +178,8 @@ class ReadabilityModule : public ClangTidyModule {
177178
"readability-simplify-boolean-expr");
178179
CheckFactories.registerCheck<SuspiciousCallArgumentCheck>(
179180
"readability-suspicious-call-argument");
181+
CheckFactories.registerCheck<TrailingCommaCheck>(
182+
"readability-trailing-comma");
180183
CheckFactories.registerCheck<UniqueptrDeleteReleaseCheck>(
181184
"readability-uniqueptr-delete-release");
182185
CheckFactories.registerCheck<UppercaseLiteralSuffixCheck>(
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
#include "TrailingCommaCheck.h"
10+
#include "../utils/LexerUtils.h"
11+
#include "clang/AST/ASTContext.h"
12+
#include "clang/ASTMatchers/ASTMatchFinder.h"
13+
#include "clang/Lex/Lexer.h"
14+
15+
using namespace clang::ast_matchers;
16+
17+
namespace clang::tidy {
18+
19+
template <>
20+
struct OptionEnumMapping<readability::TrailingCommaCheck::CommaPolicyKind> {
21+
static llvm::ArrayRef<
22+
std::pair<readability::TrailingCommaCheck::CommaPolicyKind, StringRef>>
23+
getEnumMapping() {
24+
static constexpr std::pair<readability::TrailingCommaCheck::CommaPolicyKind,
25+
StringRef>
26+
Mapping[] = {
27+
{readability::TrailingCommaCheck::CommaPolicyKind::Append,
28+
"Append"},
29+
{readability::TrailingCommaCheck::CommaPolicyKind::Remove,
30+
"Remove"},
31+
{readability::TrailingCommaCheck::CommaPolicyKind::Ignore,
32+
"Ignore"},
33+
};
34+
return {Mapping};
35+
}
36+
};
37+
38+
} // namespace clang::tidy
39+
40+
namespace clang::tidy::readability {
41+
42+
static bool isSingleLine(SourceRange Range, const SourceManager &SM) {
43+
return SM.getExpansionLineNumber(Range.getBegin()) ==
44+
SM.getExpansionLineNumber(Range.getEnd());
45+
}
46+
47+
namespace {
48+
49+
AST_POLYMORPHIC_MATCHER(isMacro,
50+
AST_POLYMORPHIC_SUPPORTED_TYPES(EnumDecl,
51+
InitListExpr)) {
52+
return Node.getBeginLoc().isMacroID() || Node.getEndLoc().isMacroID();
53+
}
54+
55+
AST_MATCHER(EnumDecl, isEmptyEnum) { return Node.enumerators().empty(); }
56+
57+
AST_MATCHER(InitListExpr, isEmptyInitList) { return Node.getNumInits() == 0; }
58+
59+
} // namespace
60+
61+
TrailingCommaCheck::TrailingCommaCheck(StringRef Name,
62+
ClangTidyContext *Context)
63+
: ClangTidyCheck(Name, Context),
64+
SingleLineCommaPolicy(
65+
Options.get("SingleLineCommaPolicy", CommaPolicyKind::Remove)),
66+
MultiLineCommaPolicy(
67+
Options.get("MultiLineCommaPolicy", CommaPolicyKind::Append)) {
68+
if (SingleLineCommaPolicy == CommaPolicyKind::Ignore &&
69+
MultiLineCommaPolicy == CommaPolicyKind::Ignore)
70+
configurationDiag("The check '%0' will not perform any analysis because "
71+
"'SingleLineCommaPolicy' and 'MultiLineCommaPolicy' are "
72+
"both set to 'Ignore'.")
73+
<< Name;
74+
}
75+
76+
void TrailingCommaCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
77+
Options.store(Opts, "SingleLineCommaPolicy", SingleLineCommaPolicy);
78+
Options.store(Opts, "MultiLineCommaPolicy", MultiLineCommaPolicy);
79+
}
80+
81+
void TrailingCommaCheck::registerMatchers(MatchFinder *Finder) {
82+
Finder->addMatcher(
83+
enumDecl(isDefinition(), unless(isEmptyEnum()), unless(isMacro()))
84+
.bind("enum"),
85+
this);
86+
87+
Finder->addMatcher(initListExpr(unless(isEmptyInitList()), unless(isMacro()))
88+
.bind("initlist"),
89+
this);
90+
}
91+
92+
void TrailingCommaCheck::check(const MatchFinder::MatchResult &Result) {
93+
if (const auto *Enum = Result.Nodes.getNodeAs<EnumDecl>("enum"))
94+
checkEnumDecl(Enum, Result);
95+
else if (const auto *InitList =
96+
Result.Nodes.getNodeAs<InitListExpr>("initlist"))
97+
checkInitListExpr(InitList, Result);
98+
else
99+
llvm_unreachable("No matches found");
100+
}
101+
102+
void TrailingCommaCheck::checkEnumDecl(const EnumDecl *Enum,
103+
const MatchFinder::MatchResult &Result) {
104+
const bool IsSingleLine = isSingleLine(
105+
{Enum->getBeginLoc(), Enum->getEndLoc()}, *Result.SourceManager);
106+
const CommaPolicyKind Policy =
107+
IsSingleLine ? SingleLineCommaPolicy : MultiLineCommaPolicy;
108+
109+
if (Policy == CommaPolicyKind::Ignore)
110+
return;
111+
112+
const std::optional<Token> LastTok =
113+
Lexer::findPreviousToken(Enum->getBraceRange().getEnd(),
114+
*Result.SourceManager, getLangOpts(), false);
115+
if (!LastTok)
116+
return;
117+
118+
emitDiag(LastTok->getLocation(), LastTok, DiagKind::Enum, Result, Policy);
119+
}
120+
121+
void TrailingCommaCheck::checkInitListExpr(
122+
const InitListExpr *InitList, const MatchFinder::MatchResult &Result) {
123+
// We need to use non-empty syntactic form for correct source locations.
124+
if (const InitListExpr *SynInitInitList = InitList->getSyntacticForm();
125+
SynInitInitList && SynInitInitList->getNumInits() > 0)
126+
InitList = SynInitInitList;
127+
128+
const bool IsSingleLine = isSingleLine(
129+
{InitList->getBeginLoc(), InitList->getEndLoc()}, *Result.SourceManager);
130+
const CommaPolicyKind Policy =
131+
IsSingleLine ? SingleLineCommaPolicy : MultiLineCommaPolicy;
132+
133+
if (Policy == CommaPolicyKind::Ignore)
134+
return;
135+
136+
const Expr *LastInit = InitList->inits().back();
137+
assert(LastInit);
138+
139+
// Skip pack expansions - they already have special syntax with '...'
140+
if (isa<PackExpansionExpr>(LastInit))
141+
return;
142+
143+
const std::optional<Token> NextTok =
144+
utils::lexer::findNextTokenSkippingComments(
145+
LastInit->getEndLoc(), *Result.SourceManager, getLangOpts());
146+
147+
// If the next token is neither a comma nor closing brace, there might be
148+
// a macro (e.g., #define COMMA ,) that we can't safely analyze.
149+
if (NextTok && !NextTok->isOneOf(tok::comma, tok::r_brace))
150+
return;
151+
152+
emitDiag(LastInit->getEndLoc(), NextTok, DiagKind::InitList, Result, Policy);
153+
}
154+
155+
void TrailingCommaCheck::emitDiag(
156+
SourceLocation LastLoc, std::optional<Token> Token, DiagKind Kind,
157+
const ast_matchers::MatchFinder::MatchResult &Result,
158+
CommaPolicyKind Policy) {
159+
if (LastLoc.isInvalid() || !Token)
160+
return;
161+
162+
const bool HasTrailingComma = Token->is(tok::comma);
163+
if (Policy == CommaPolicyKind::Append && !HasTrailingComma) {
164+
const SourceLocation InsertLoc = Lexer::getLocForEndOfToken(
165+
LastLoc, 0, *Result.SourceManager, getLangOpts());
166+
diag(InsertLoc, "%select{initializer list|enum}0 should have "
167+
"a trailing comma")
168+
<< Kind << FixItHint::CreateInsertion(InsertLoc, ",");
169+
} else if (Policy == CommaPolicyKind::Remove && HasTrailingComma) {
170+
const SourceLocation CommaLoc = Token->getLocation();
171+
if (CommaLoc.isInvalid())
172+
return;
173+
diag(CommaLoc, "%select{initializer list|enum}0 should not have "
174+
"a trailing comma")
175+
<< Kind << FixItHint::CreateRemoval(CommaLoc);
176+
}
177+
}
178+
179+
} // namespace clang::tidy::readability
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_READABILITY_TRAILINGCOMMACHECK_H
10+
#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_READABILITY_TRAILINGCOMMACHECK_H
11+
12+
#include "../ClangTidyCheck.h"
13+
14+
namespace clang::tidy::readability {
15+
16+
/// Checks for presence or absence of trailing commas in enum definitions
17+
/// and initializer lists.
18+
///
19+
/// For the user-facing documentation see:
20+
/// https://clang.llvm.org/extra/clang-tidy/checks/readability/trailing-comma.html
21+
class TrailingCommaCheck : public ClangTidyCheck {
22+
public:
23+
TrailingCommaCheck(StringRef Name, ClangTidyContext *Context);
24+
void storeOptions(ClangTidyOptions::OptionMap &Opts) override;
25+
void registerMatchers(ast_matchers::MatchFinder *Finder) override;
26+
void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
27+
bool isLanguageVersionSupported(const LangOptions &LangOpts) const override {
28+
return LangOpts.CPlusPlus || LangOpts.C99;
29+
}
30+
std::optional<TraversalKind> getCheckTraversalKind() const override {
31+
return TK_IgnoreUnlessSpelledInSource;
32+
}
33+
34+
enum class CommaPolicyKind { Append, Remove, Ignore };
35+
36+
private:
37+
const CommaPolicyKind SingleLineCommaPolicy;
38+
const CommaPolicyKind MultiLineCommaPolicy;
39+
40+
void checkEnumDecl(const EnumDecl *Enum,
41+
const ast_matchers::MatchFinder::MatchResult &Result);
42+
void checkInitListExpr(const InitListExpr *InitList,
43+
const ast_matchers::MatchFinder::MatchResult &Result);
44+
45+
// Values correspond to %select{initializer list|enum}0 indices
46+
enum DiagKind { InitList = 0, Enum = 1 };
47+
void emitDiag(SourceLocation LastLoc, std::optional<Token> Token,
48+
DiagKind Kind,
49+
const ast_matchers::MatchFinder::MatchResult &Result,
50+
CommaPolicyKind Policy);
51+
};
52+
53+
} // namespace clang::tidy::readability
54+
55+
#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_READABILITY_TRAILINGCOMMACHECK_H

clang-tools-extra/docs/ReleaseNotes.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,12 @@ New checks
128128
Finds and removes redundant conversions from ``std::[w|u8|u16|u32]string_view`` to
129129
``std::[...]string`` in call expressions expecting ``std::[...]string_view``.
130130

131+
- New :doc:`readability-trailing-comma
132+
<clang-tidy/checks/readability/trailing-comma>` check.
133+
134+
Checks for presence or absence of trailing commas in enum definitions and
135+
initializer lists.
136+
131137
New check aliases
132138
^^^^^^^^^^^^^^^^^
133139

clang-tools-extra/docs/clang-tidy/checks/list.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,7 @@ Clang-Tidy Checks
429429
:doc:`readability-static-definition-in-anonymous-namespace <readability/static-definition-in-anonymous-namespace>`, "Yes"
430430
:doc:`readability-string-compare <readability/string-compare>`, "Yes"
431431
:doc:`readability-suspicious-call-argument <readability/suspicious-call-argument>`,
432+
:doc:`readability-trailing-comma <readability/trailing-comma>`, "Yes"
432433
:doc:`readability-uniqueptr-delete-release <readability/uniqueptr-delete-release>`, "Yes"
433434
:doc:`readability-uppercase-literal-suffix <readability/uppercase-literal-suffix>`, "Yes"
434435
:doc:`readability-use-anyofallof <readability/use-anyofallof>`,
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
.. title:: clang-tidy - readability-trailing-comma
2+
3+
readability-trailing-comma
4+
==========================
5+
6+
Checks for presence or absence of trailing commas in enum definitions and
7+
initializer lists.
8+
9+
The check supports separate policies for single-line and multi-line constructs,
10+
allowing different styles for each. By default, the check enforces trailing
11+
commas in multi-line constructs and removes them from single-line constructs.
12+
13+
Trailing commas in multi-line constructs offer several benefits:
14+
15+
- Adding or removing elements at the end only changes a single line, making
16+
diffs smaller and easier to read.
17+
- Formatters may change code to a more desired style.
18+
- Code generators avoid the need for special handling of the last element.
19+
20+
.. code-block:: c++
21+
22+
// Without trailing commas - adding "Yellow" requires modifying the "Blue" line
23+
enum Color {
24+
Red,
25+
Green,
26+
Blue
27+
};
28+
29+
// With trailing commas - adding "Yellow" is a clean, single-line change
30+
enum Color {
31+
Red,
32+
Green,
33+
Blue,
34+
};
35+
36+
37+
Limitations
38+
-----------
39+
40+
The check currently doesn't analyze code inside macros.
41+
42+
43+
Options
44+
-------
45+
46+
.. option:: SingleLineCommaPolicy
47+
48+
Controls whether to add, remove, or ignore trailing commas in single-line
49+
enum definitions and initializer lists.
50+
Valid values are:
51+
52+
- `Append`: Add trailing commas where missing.
53+
- `Remove`: Remove trailing commas where present.
54+
- `Ignore`: Do not check single-line constructs.
55+
56+
Default is `Remove`.
57+
58+
.. option:: MultiLineCommaPolicy
59+
60+
Controls whether to add, remove, or ignore trailing commas in multi-line
61+
enum definitions and initializer lists.
62+
Valid values are:
63+
64+
- `Append`: Add trailing commas where missing.
65+
- `Remove`: Remove trailing commas where present.
66+
- `Ignore`: Do not check multi-line constructs.
67+
68+
Default is `Append`.

0 commit comments

Comments
 (0)