Skip to content

Commit 5baddd8

Browse files
committed
[clang-tidy] add option to readability-use-concise-preprocessor-directives to take consistency into account
1 parent 4eec28c commit 5baddd8

File tree

5 files changed

+293
-33
lines changed

5 files changed

+293
-33
lines changed

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

Lines changed: 135 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,51 +11,124 @@
1111
#include "clang/Lex/Lexer.h"
1212
#include "clang/Lex/PPCallbacks.h"
1313
#include "clang/Lex/Preprocessor.h"
14+
#include "llvm/ADT/STLForwardCompat.h"
1415

1516
#include <array>
1617

1718
namespace clang::tidy::readability {
1819

1920
namespace {
2021

21-
class IfPreprocessorCallbacks final : public PPCallbacks {
22+
enum class DirectiveKind : bool { If, Elif };
23+
24+
struct Rewrite {
25+
SourceLocation DirectiveLoc;
26+
SourceRange ConditionRange;
27+
StringRef Macro;
28+
bool Negated;
29+
DirectiveKind Directive;
30+
};
31+
32+
struct StackEntry {
33+
SmallVector<Rewrite, 2> Rewrites;
34+
bool ApplyingRewritesWouldBreakConsistency = false;
35+
};
36+
37+
class UseConciseDirectivesPPCallbacks final : public PPCallbacks {
2238
public:
23-
IfPreprocessorCallbacks(ClangTidyCheck &Check, const Preprocessor &PP)
24-
: Check(Check), PP(PP) {}
39+
UseConciseDirectivesPPCallbacks(UseConcisePreprocessorDirectivesCheck &Check,
40+
const Preprocessor &PP,
41+
bool PreserveConsistency)
42+
: Check(Check), PP(PP), PreserveConsistency(PreserveConsistency) {}
43+
44+
void Ifdef(SourceLocation, const Token &, const MacroDefinition &) override {
45+
Stack.emplace_back();
46+
}
47+
48+
void Ifndef(SourceLocation, const Token &, const MacroDefinition &) override {
49+
Stack.emplace_back();
50+
}
2551

2652
void If(SourceLocation Loc, SourceRange ConditionRange,
2753
ConditionValueKind) override {
28-
impl(Loc, ConditionRange, {"ifdef", "ifndef"});
54+
Stack.emplace_back();
55+
handleCondition(Loc, ConditionRange, DirectiveKind::If);
2956
}
3057

3158
void Elif(SourceLocation Loc, SourceRange ConditionRange, ConditionValueKind,
3259
SourceLocation) override {
33-
if (PP.getLangOpts().C23 || PP.getLangOpts().CPlusPlus23)
34-
impl(Loc, ConditionRange, {"elifdef", "elifndef"});
60+
handleCondition(Loc, ConditionRange, DirectiveKind::Elif);
61+
}
62+
63+
void Endif(SourceLocation, SourceLocation) override {
64+
static constexpr std::array<std::array<StringRef, 2>, 2> Replacements = {{
65+
{"ifdef", "ifndef"},
66+
{"elifdef", "elifndef"},
67+
}};
68+
69+
const auto &[Rewrites, ApplyingRewritesWouldBreakConsistency] =
70+
Stack.back();
71+
if (!(PreserveConsistency && ApplyingRewritesWouldBreakConsistency)) {
72+
for (const auto &[DirectiveLoc, ConditionRange, Macro, Negated,
73+
Directive] : Rewrites) {
74+
const StringRef Replacement =
75+
Replacements[llvm::to_underlying(Directive)][Negated];
76+
Check.diag(
77+
DirectiveLoc,
78+
"preprocessor condition can be written more concisely using '#%0'")
79+
<< Replacement
80+
<< FixItHint::CreateReplacement(DirectiveLoc, Replacement)
81+
<< FixItHint::CreateReplacement(ConditionRange, Macro);
82+
}
83+
}
84+
85+
Stack.pop_back();
3586
}
3687

3788
private:
38-
void impl(SourceLocation DirectiveLoc, SourceRange ConditionRange,
39-
const std::array<llvm::StringLiteral, 2> &Replacements) {
89+
struct RewriteInfo {
90+
StringRef Macro;
91+
bool Negated;
92+
};
93+
94+
void handleCondition(SourceLocation DirectiveLoc, SourceRange ConditionRange,
95+
DirectiveKind Directive) {
96+
if (Directive != DirectiveKind::Elif || PP.getLangOpts().C23 ||
97+
PP.getLangOpts().CPlusPlus23)
98+
if (const std::optional<RewriteInfo> Rewrite =
99+
tryRewrite(ConditionRange)) {
100+
Stack.back().Rewrites.push_back({DirectiveLoc, ConditionRange,
101+
Rewrite->Macro, Rewrite->Negated,
102+
Directive});
103+
return;
104+
}
105+
106+
if (!Stack.back().ApplyingRewritesWouldBreakConsistency)
107+
Stack.back().ApplyingRewritesWouldBreakConsistency =
108+
conditionContainsDefinedOperator(ConditionRange);
109+
}
110+
111+
std::optional<RewriteInfo> tryRewrite(SourceRange ConditionRange) {
40112
// Lexer requires its input range to be null-terminated.
41-
SmallString<128> Condition =
113+
const StringRef SourceText =
42114
Lexer::getSourceText(CharSourceRange::getTokenRange(ConditionRange),
43115
PP.getSourceManager(), PP.getLangOpts());
116+
SmallString<128> Condition = SourceText;
44117
Condition.push_back('\0');
45-
Lexer Lex(DirectiveLoc, PP.getLangOpts(), Condition.data(),
46-
Condition.data(), Condition.data() + Condition.size() - 1);
118+
Lexer Lex({}, PP.getLangOpts(), Condition.data(), Condition.data(),
119+
Condition.data() + Condition.size() - 1);
47120
Token Tok;
48-
bool Inverted = false; // The inverted form of #*def is #*ndef.
121+
bool Negated = false;
49122
std::size_t ParensNestingDepth = 0;
50123
for (;;) {
51124
if (Lex.LexFromRawLexer(Tok))
52-
return;
125+
return {};
53126

54127
if (Tok.is(tok::TokenKind::exclaim) ||
55128
(PP.getLangOpts().CPlusPlus &&
56129
Tok.is(tok::TokenKind::raw_identifier) &&
57130
Tok.getRawIdentifier() == "not"))
58-
Inverted = !Inverted;
131+
Negated = !Negated;
59132
else if (Tok.is(tok::TokenKind::l_paren))
60133
++ParensNestingDepth;
61134
else
@@ -64,47 +137,80 @@ class IfPreprocessorCallbacks final : public PPCallbacks {
64137

65138
if (Tok.isNot(tok::TokenKind::raw_identifier) ||
66139
Tok.getRawIdentifier() != "defined")
67-
return;
140+
return {};
68141

69142
bool NoMoreTokens = Lex.LexFromRawLexer(Tok);
70143
if (Tok.is(tok::TokenKind::l_paren)) {
71144
if (NoMoreTokens)
72-
return;
145+
return {};
73146
++ParensNestingDepth;
74147
NoMoreTokens = Lex.LexFromRawLexer(Tok);
75148
}
76149

77150
if (Tok.isNot(tok::TokenKind::raw_identifier))
78-
return;
79-
const StringRef Macro = Tok.getRawIdentifier();
151+
return {};
152+
153+
// We need a stable StringRef into the ConditionRange, but because Lexer
154+
// forces us to work on a temporary null-terminated copy of the
155+
// ConditionRange, we need to do this ugly translation.
156+
const StringRef Macro = {
157+
SourceText.data() + (Tok.getRawIdentifier().data() - Condition.data()),
158+
Tok.getRawIdentifier().size()};
80159

81160
while (!NoMoreTokens) {
82161
NoMoreTokens = Lex.LexFromRawLexer(Tok);
83162
if (Tok.isNot(tok::TokenKind::r_paren))
84-
return;
163+
return {};
85164
--ParensNestingDepth;
86165
}
87166

88167
if (ParensNestingDepth != 0)
89-
return;
90-
91-
Check.diag(
92-
DirectiveLoc,
93-
"preprocessor condition can be written more concisely using '#%0'")
94-
<< FixItHint::CreateReplacement(DirectiveLoc, Replacements[Inverted])
95-
<< FixItHint::CreateReplacement(ConditionRange, Macro)
96-
<< Replacements[Inverted];
168+
return {};
169+
170+
return {{Macro, Negated}};
97171
}
98172

99-
ClangTidyCheck &Check;
173+
bool conditionContainsDefinedOperator(SourceRange ConditionRange) {
174+
// Lexer requires its input range to be null-terminated.
175+
SmallString<128> Condition =
176+
Lexer::getSourceText(CharSourceRange::getTokenRange(ConditionRange),
177+
PP.getSourceManager(), PP.getLangOpts());
178+
Condition.push_back('\0');
179+
Lexer Lex({}, PP.getLangOpts(), Condition.data(), Condition.data(),
180+
Condition.data() + Condition.size() - 1);
181+
182+
for (Token Tok;;) {
183+
const bool NoMoreTokens = Lex.LexFromRawLexer(Tok);
184+
if (Tok.is(tok::TokenKind::raw_identifier) &&
185+
Tok.getRawIdentifier() == "defined")
186+
return true;
187+
if (NoMoreTokens)
188+
return false;
189+
}
190+
}
191+
192+
UseConcisePreprocessorDirectivesCheck &Check;
100193
const Preprocessor &PP;
194+
const bool PreserveConsistency;
195+
SmallVector<StackEntry, 4> Stack;
101196
};
102197

103198
} // namespace
104199

200+
UseConcisePreprocessorDirectivesCheck::UseConcisePreprocessorDirectivesCheck(
201+
StringRef Name, ClangTidyContext *Context)
202+
: ClangTidyCheck(Name, Context),
203+
PreserveConsistency(Options.get("PreserveConsistency", false)) {}
204+
205+
void UseConcisePreprocessorDirectivesCheck::storeOptions(
206+
ClangTidyOptions::OptionMap &Opts) {
207+
Options.store(Opts, "PreserveConsistency", PreserveConsistency);
208+
}
209+
105210
void UseConcisePreprocessorDirectivesCheck::registerPPCallbacks(
106211
const SourceManager &, Preprocessor *PP, Preprocessor *) {
107-
PP->addPPCallbacks(std::make_unique<IfPreprocessorCallbacks>(*this, *PP));
212+
PP->addPPCallbacks(std::make_unique<UseConciseDirectivesPPCallbacks>(
213+
*this, *PP, PreserveConsistency));
108214
}
109215

110216
} // namespace clang::tidy::readability

clang-tools-extra/clang-tidy/readability/UseConcisePreprocessorDirectivesCheck.h

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@ namespace clang::tidy::readability {
2121
/// https://clang.llvm.org/extra/clang-tidy/checks/readability/use-concise-preprocessor-directives.html
2222
class UseConcisePreprocessorDirectivesCheck : public ClangTidyCheck {
2323
public:
24-
using ClangTidyCheck::ClangTidyCheck;
24+
UseConcisePreprocessorDirectivesCheck(StringRef Name,
25+
ClangTidyContext *Context);
2526
void registerPPCallbacks(const SourceManager &SM, Preprocessor *PP,
2627
Preprocessor *ModuleExpanderPP) override;
27-
bool isLanguageVersionSupported(const LangOptions &LangOpts) const override {
28-
return true;
29-
}
28+
void storeOptions(ClangTidyOptions::OptionMap &Opts) override;
29+
30+
private:
31+
const bool PreserveConsistency;
3032
};
3133

3234
} // namespace clang::tidy::readability

clang-tools-extra/docs/ReleaseNotes.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,12 @@ Changes in existing checks
243243
<clang-tidy/checks/readability/qualified-auto>` check by adding the option
244244
`IgnoreAliasing`, that allows not looking at underlying types of type aliases.
245245

246+
- Improved :doc:`readability-use-concise-preprocessor-directives
247+
<clang-tidy/checks/readability/use-concise-preprocessor-directives>` check by
248+
adding the option `PreserveConsistency`, which prevents shortening
249+
directives if it would break visual consistency with other directives in the
250+
``#if`` chain.
251+
246252
Removed checks
247253
^^^^^^^^^^^^^^
248254

clang-tools-extra/docs/clang-tidy/checks/readability/use-concise-preprocessor-directives.rst

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,41 @@ Since C23 and C++23:
2828

2929
#elifdef MEOW
3030
#elifndef MEOW
31+
32+
Options
33+
-------
34+
35+
.. option:: PreserveConsistency
36+
37+
If `true`, directives will not be simplified if doing so would break
38+
consistency with other directives in a chain. Default is `false`.
39+
40+
Example
41+
^^^^^^^
42+
43+
.. code-block:: c++
44+
45+
// Not simplified.
46+
#if defined(FOO)
47+
#elif defined(BAR) || defined(BAZ)
48+
#endif
49+
50+
// Only simplified in C23 or C++23.
51+
#if defined(FOO)
52+
#elif defined(BAR)
53+
#endif
54+
55+
// Consistency among *different* chains is not taken into account.
56+
#if defined(FOO)
57+
#if defined(BAR) || defined(BAZ)
58+
#endif
59+
#elif defined(HAZ)
60+
#endif
61+
62+
// becomes
63+
64+
#ifdef FOO
65+
#if defined(BAR) || defined(BAZ)
66+
#endif
67+
#elifdef HAZ
68+
#endif

0 commit comments

Comments
 (0)