1 //===--- MacroToEnumCheck.cpp - clang-tidy --------------------------------===//
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
7 //===----------------------------------------------------------------------===//
9 #include "MacroToEnumCheck.h"
10 #include "IntegralLiteralExpressionMatcher.h"
12 #include "clang/AST/ASTContext.h"
13 #include "clang/ASTMatchers/ASTMatchFinder.h"
14 #include "clang/Lex/Preprocessor.h"
15 #include "llvm/ADT/STLExtras.h"
21 namespace clang::tidy::modernize
{
23 static bool hasOnlyComments(SourceLocation Loc
, const LangOptions
&Options
,
25 // Use a lexer to look for tokens; if we find something other than a single
26 // hash, then there were intervening tokens between macro definitions.
27 std::string Buffer
{Text
};
28 Lexer
Lex(Loc
, Options
, Buffer
.c_str(), Buffer
.c_str(),
29 Buffer
.c_str() + Buffer
.size());
31 bool SeenHash
= false;
32 while (!Lex
.LexFromRawLexer(Tok
)) {
33 if (Tok
.getKind() == tok::hash
&& !SeenHash
) {
40 // Everything in between was whitespace, so now just look for two blank lines,
41 // consisting of two consecutive EOL sequences, either '\n', '\r' or '\r\n'.
42 enum class WhiteSpace
{
50 WhiteSpace State
= WhiteSpace::Nothing
;
54 if (State
== WhiteSpace::CR
)
57 State
= State
== WhiteSpace::CRLF
? WhiteSpace::CRLFCR
: WhiteSpace::CR
;
61 if (State
== WhiteSpace::LF
|| State
== WhiteSpace::CRLFCR
)
64 State
= State
== WhiteSpace::CR
? WhiteSpace::CRLF
: WhiteSpace::LF
;
68 State
= WhiteSpace::Nothing
;
76 static StringRef
getTokenName(const Token
&Tok
) {
77 return Tok
.is(tok::raw_identifier
) ? Tok
.getRawIdentifier()
78 : Tok
.getIdentifierInfo()->getName();
84 EnumMacro(Token Name
, const MacroDirective
*Directive
)
85 : Name(Name
), Directive(Directive
) {}
88 const MacroDirective
*Directive
;
91 using MacroList
= SmallVector
<EnumMacro
>;
93 enum class IncludeGuard
{ None
, FileChanged
, IfGuard
, DefineGuard
};
96 FileState() = default;
98 int ConditionScopes
= 0;
99 unsigned int LastLine
= 0;
100 IncludeGuard GuardScanner
= IncludeGuard::None
;
101 SourceLocation LastMacroLocation
;
106 class MacroToEnumCallbacks
: public PPCallbacks
{
108 MacroToEnumCallbacks(MacroToEnumCheck
*Check
, const LangOptions
&LangOptions
,
109 const SourceManager
&SM
)
110 : Check(Check
), LangOpts(LangOptions
), SM(SM
) {}
112 void FileChanged(SourceLocation Loc
, FileChangeReason Reason
,
113 SrcMgr::CharacteristicKind FileType
,
114 FileID PrevFID
) override
;
116 void InclusionDirective(SourceLocation HashLoc
, const Token
&IncludeTok
,
117 StringRef FileName
, bool IsAngled
,
118 CharSourceRange FilenameRange
,
119 OptionalFileEntryRef File
, StringRef SearchPath
,
120 StringRef RelativePath
, const Module
*SuggestedModule
,
122 SrcMgr::CharacteristicKind FileType
) override
{
123 clearCurrentEnum(HashLoc
);
126 // Keep track of macro definitions that look like enums.
127 void MacroDefined(const Token
&MacroNameTok
,
128 const MacroDirective
*MD
) override
;
130 // Undefining an enum-like macro results in the enum set being dropped.
131 void MacroUndefined(const Token
&MacroNameTok
, const MacroDefinition
&MD
,
132 const MacroDirective
*Undef
) override
;
134 // Conditional compilation clears any adjacent enum-like macros.
135 // Macros used in conditional expressions clear any adjacent enum-like
137 // Include guards are either
138 // #if !defined(GUARD)
141 void If(SourceLocation Loc
, SourceRange ConditionRange
,
142 ConditionValueKind ConditionValue
) override
{
144 checkCondition(ConditionRange
);
146 void Ifndef(SourceLocation Loc
, const Token
&MacroNameTok
,
147 const MacroDefinition
&MD
) override
{
149 checkName(MacroNameTok
);
151 void Ifdef(SourceLocation Loc
, const Token
&MacroNameTok
,
152 const MacroDefinition
&MD
) override
{
154 checkName(MacroNameTok
);
156 void Elif(SourceLocation Loc
, SourceRange ConditionRange
,
157 ConditionValueKind ConditionValue
, SourceLocation IfLoc
) override
{
158 checkCondition(ConditionRange
);
160 void Elifdef(SourceLocation Loc
, const Token
&MacroNameTok
,
161 const MacroDefinition
&MD
) override
{
162 checkName(MacroNameTok
);
164 void Elifdef(SourceLocation Loc
, SourceRange ConditionRange
,
165 SourceLocation IfLoc
) override
{
166 PPCallbacks::Elifdef(Loc
, ConditionRange
, IfLoc
);
168 void Elifndef(SourceLocation Loc
, const Token
&MacroNameTok
,
169 const MacroDefinition
&MD
) override
{
170 checkName(MacroNameTok
);
172 void Elifndef(SourceLocation Loc
, SourceRange ConditionRange
,
173 SourceLocation IfLoc
) override
{
174 PPCallbacks::Elifndef(Loc
, ConditionRange
, IfLoc
);
176 void Endif(SourceLocation Loc
, SourceLocation IfLoc
) override
;
177 void PragmaDirective(SourceLocation Loc
,
178 PragmaIntroducerKind Introducer
) override
;
180 // After we've seen everything, issue warnings and fix-its.
181 void EndOfMainFile() override
;
183 void invalidateRange(SourceRange Range
);
187 if (Enums
.empty() || !Enums
.back().empty())
188 Enums
.emplace_back();
190 bool insideConditional() const {
191 return (CurrentFile
->GuardScanner
== IncludeGuard::DefineGuard
&&
192 CurrentFile
->ConditionScopes
> 1) ||
193 (CurrentFile
->GuardScanner
!= IncludeGuard::DefineGuard
&&
194 CurrentFile
->ConditionScopes
> 0);
196 bool isConsecutiveMacro(const MacroDirective
*MD
) const;
197 void rememberLastMacroLocation(const MacroDirective
*MD
) {
198 CurrentFile
->LastLine
= SM
.getSpellingLineNumber(MD
->getLocation());
199 CurrentFile
->LastMacroLocation
= Lexer::getLocForEndOfToken(
200 MD
->getMacroInfo()->getDefinitionEndLoc(), 0, SM
, LangOpts
);
202 void clearLastMacroLocation() {
203 CurrentFile
->LastLine
= 0;
204 CurrentFile
->LastMacroLocation
= SourceLocation
{};
206 void clearCurrentEnum(SourceLocation Loc
);
207 void conditionStart(const SourceLocation
&Loc
);
208 void checkCondition(SourceRange ConditionRange
);
209 void checkName(const Token
&MacroNameTok
);
210 void rememberExpressionName(const Token
&Tok
);
211 void rememberExpressionTokens(ArrayRef
<Token
> MacroTokens
);
212 void invalidateExpressionNames();
213 void issueDiagnostics();
214 void warnMacroEnum(const EnumMacro
&Macro
) const;
215 void fixEnumMacro(const MacroList
&MacroList
) const;
216 bool isInitializer(ArrayRef
<Token
> MacroTokens
);
218 MacroToEnumCheck
*Check
;
219 const LangOptions
&LangOpts
;
220 const SourceManager
&SM
;
221 SmallVector
<MacroList
> Enums
;
222 SmallVector
<FileState
> Files
;
223 std::vector
<std::string
> ExpressionNames
;
224 FileState
*CurrentFile
= nullptr;
227 bool MacroToEnumCallbacks::isConsecutiveMacro(const MacroDirective
*MD
) const {
228 if (CurrentFile
->LastMacroLocation
.isInvalid())
231 SourceLocation Loc
= MD
->getLocation();
232 if (CurrentFile
->LastLine
+ 1 == SM
.getSpellingLineNumber(Loc
))
235 SourceLocation Define
=
236 SM
.translateLineCol(SM
.getFileID(Loc
), SM
.getSpellingLineNumber(Loc
), 1);
237 CharSourceRange BetweenMacros
{
238 SourceRange
{CurrentFile
->LastMacroLocation
, Define
}, true};
239 CharSourceRange CharRange
=
240 Lexer::makeFileCharRange(BetweenMacros
, SM
, LangOpts
);
241 StringRef BetweenText
= Lexer::getSourceText(CharRange
, SM
, LangOpts
);
242 return hasOnlyComments(Define
, LangOpts
, BetweenText
);
245 void MacroToEnumCallbacks::clearCurrentEnum(SourceLocation Loc
) {
246 // Only drop the most recent Enum set if the directive immediately follows.
247 if (!Enums
.empty() && !Enums
.back().empty() &&
248 SM
.getSpellingLineNumber(Loc
) == CurrentFile
->LastLine
+ 1)
251 clearLastMacroLocation();
254 void MacroToEnumCallbacks::conditionStart(const SourceLocation
&Loc
) {
255 ++CurrentFile
->ConditionScopes
;
256 clearCurrentEnum(Loc
);
257 if (CurrentFile
->GuardScanner
== IncludeGuard::FileChanged
)
258 CurrentFile
->GuardScanner
= IncludeGuard::IfGuard
;
261 void MacroToEnumCallbacks::checkCondition(SourceRange Range
) {
262 CharSourceRange CharRange
= Lexer::makeFileCharRange(
263 CharSourceRange::getTokenRange(Range
), SM
, LangOpts
);
264 std::string Text
= Lexer::getSourceText(CharRange
, SM
, LangOpts
).str();
265 Lexer
Lex(CharRange
.getBegin(), LangOpts
, Text
.data(), Text
.data(),
266 Text
.data() + Text
.size());
270 End
= Lex
.LexFromRawLexer(Tok
);
271 if (Tok
.is(tok::raw_identifier
) &&
272 Tok
.getRawIdentifier().str() != "defined")
277 void MacroToEnumCallbacks::checkName(const Token
&MacroNameTok
) {
278 rememberExpressionName(MacroNameTok
);
280 StringRef Id
= getTokenName(MacroNameTok
);
281 llvm::erase_if(Enums
, [&Id
](const MacroList
&MacroList
) {
282 return llvm::any_of(MacroList
, [&Id
](const EnumMacro
&Macro
) {
283 return getTokenName(Macro
.Name
) == Id
;
288 void MacroToEnumCallbacks::rememberExpressionName(const Token
&Tok
) {
289 std::string Id
= getTokenName(Tok
).str();
290 auto Pos
= llvm::lower_bound(ExpressionNames
, Id
);
291 if (Pos
== ExpressionNames
.end() || *Pos
!= Id
) {
292 ExpressionNames
.insert(Pos
, Id
);
296 void MacroToEnumCallbacks::rememberExpressionTokens(
297 ArrayRef
<Token
> MacroTokens
) {
298 for (Token Tok
: MacroTokens
) {
299 if (Tok
.isAnyIdentifier())
300 rememberExpressionName(Tok
);
304 void MacroToEnumCallbacks::FileChanged(SourceLocation Loc
,
305 FileChangeReason Reason
,
306 SrcMgr::CharacteristicKind FileType
,
309 if (Reason
== EnterFile
) {
310 Files
.emplace_back();
311 if (!SM
.isInMainFile(Loc
))
312 Files
.back().GuardScanner
= IncludeGuard::FileChanged
;
313 } else if (Reason
== ExitFile
) {
314 assert(CurrentFile
->ConditionScopes
== 0);
317 CurrentFile
= &Files
.back();
320 bool MacroToEnumCallbacks::isInitializer(ArrayRef
<Token
> MacroTokens
)
322 IntegralLiteralExpressionMatcher
Matcher(MacroTokens
, LangOpts
.C99
== 0);
323 bool Matched
= Matcher
.match();
324 bool isC
= !LangOpts
.CPlusPlus
;
325 if (isC
&& (Matcher
.largestLiteralSize() != LiteralSize::Int
&&
326 Matcher
.largestLiteralSize() != LiteralSize::UnsignedInt
))
333 // Any defined but rejected macro is scanned for identifiers that
334 // are to be excluded as enums.
335 void MacroToEnumCallbacks::MacroDefined(const Token
&MacroNameTok
,
336 const MacroDirective
*MD
) {
337 // Include guards are never candidates for becoming an enum.
338 if (CurrentFile
->GuardScanner
== IncludeGuard::IfGuard
) {
339 CurrentFile
->GuardScanner
= IncludeGuard::DefineGuard
;
343 if (insideConditional())
346 if (SM
.getFilename(MD
->getLocation()).empty())
349 const MacroInfo
*Info
= MD
->getMacroInfo();
350 ArrayRef
<Token
> MacroTokens
= Info
->tokens();
351 if (Info
->isBuiltinMacro() || MacroTokens
.empty())
353 if (Info
->isFunctionLike()) {
354 rememberExpressionTokens(MacroTokens
);
358 if (!isInitializer(MacroTokens
))
361 if (!isConsecutiveMacro(MD
))
363 Enums
.back().emplace_back(MacroNameTok
, MD
);
364 rememberLastMacroLocation(MD
);
367 // Any macro that is undefined removes all adjacent macros from consideration as
368 // an enum and starts a new enum scan.
369 void MacroToEnumCallbacks::MacroUndefined(const Token
&MacroNameTok
,
370 const MacroDefinition
&MD
,
371 const MacroDirective
*Undef
) {
372 rememberExpressionName(MacroNameTok
);
374 auto MatchesToken
= [&MacroNameTok
](const EnumMacro
&Macro
) {
375 return getTokenName(Macro
.Name
) == getTokenName(MacroNameTok
);
378 auto It
= llvm::find_if(Enums
, [MatchesToken
](const MacroList
&MacroList
) {
379 return llvm::any_of(MacroList
, MatchesToken
);
381 if (It
!= Enums
.end())
384 clearLastMacroLocation();
385 CurrentFile
->GuardScanner
= IncludeGuard::None
;
388 void MacroToEnumCallbacks::Endif(SourceLocation Loc
, SourceLocation IfLoc
) {
389 // The if directive for the include guard isn't counted in the
391 if (CurrentFile
->ConditionScopes
== 0 &&
392 CurrentFile
->GuardScanner
== IncludeGuard::DefineGuard
)
395 // We don't need to clear the current enum because the start of the
396 // conditional block already took care of that.
397 assert(CurrentFile
->ConditionScopes
> 0);
398 --CurrentFile
->ConditionScopes
;
404 bool textEquals(const char (&Needle
)[N
], const char *HayStack
) {
405 return StringRef
{HayStack
, N
- 1} == Needle
;
408 template <size_t N
> size_t len(const char (&)[N
]) { return N
- 1; }
412 void MacroToEnumCallbacks::PragmaDirective(SourceLocation Loc
,
413 PragmaIntroducerKind Introducer
) {
414 if (CurrentFile
->GuardScanner
!= IncludeGuard::FileChanged
)
417 bool Invalid
= false;
418 const char *Text
= SM
.getCharacterData(
419 Lexer::getLocForEndOfToken(Loc
, 0, SM
, LangOpts
), &Invalid
);
423 while (*Text
&& std::isspace(*Text
))
426 if (textEquals("pragma", Text
))
429 Text
+= len("pragma");
430 while (*Text
&& std::isspace(*Text
))
433 if (textEquals("once", Text
))
434 CurrentFile
->GuardScanner
= IncludeGuard::IfGuard
;
437 void MacroToEnumCallbacks::invalidateExpressionNames() {
438 for (const std::string
&Id
: ExpressionNames
) {
439 llvm::erase_if(Enums
, [Id
](const MacroList
&MacroList
) {
440 return llvm::any_of(MacroList
, [&Id
](const EnumMacro
&Macro
) {
441 return getTokenName(Macro
.Name
) == Id
;
447 void MacroToEnumCallbacks::EndOfMainFile() {
448 invalidateExpressionNames();
452 void MacroToEnumCallbacks::invalidateRange(SourceRange Range
) {
453 llvm::erase_if(Enums
, [Range
](const MacroList
&MacroList
) {
454 return llvm::any_of(MacroList
, [Range
](const EnumMacro
&Macro
) {
455 return Macro
.Directive
->getLocation() >= Range
.getBegin() &&
456 Macro
.Directive
->getLocation() <= Range
.getEnd();
461 void MacroToEnumCallbacks::issueDiagnostics() {
462 for (const MacroList
&MacroList
: Enums
) {
463 if (MacroList
.empty())
466 for (const EnumMacro
&Macro
: MacroList
)
467 warnMacroEnum(Macro
);
469 fixEnumMacro(MacroList
);
473 void MacroToEnumCallbacks::warnMacroEnum(const EnumMacro
&Macro
) const {
474 Check
->diag(Macro
.Directive
->getLocation(),
475 "macro '%0' defines an integral constant; prefer an enum instead")
476 << getTokenName(Macro
.Name
);
479 void MacroToEnumCallbacks::fixEnumMacro(const MacroList
&MacroList
) const {
480 SourceLocation Begin
=
481 MacroList
.front().Directive
->getMacroInfo()->getDefinitionLoc();
482 Begin
= SM
.translateLineCol(SM
.getFileID(Begin
),
483 SM
.getSpellingLineNumber(Begin
), 1);
484 DiagnosticBuilder Diagnostic
=
485 Check
->diag(Begin
, "replace macro with enum")
486 << FixItHint::CreateInsertion(Begin
, "enum {\n");
488 for (size_t I
= 0U; I
< MacroList
.size(); ++I
) {
489 const EnumMacro
&Macro
= MacroList
[I
];
490 SourceLocation DefineEnd
=
491 Macro
.Directive
->getMacroInfo()->getDefinitionLoc();
492 SourceLocation DefineBegin
= SM
.translateLineCol(
493 SM
.getFileID(DefineEnd
), SM
.getSpellingLineNumber(DefineEnd
), 1);
494 CharSourceRange DefineRange
;
495 DefineRange
.setBegin(DefineBegin
);
496 DefineRange
.setEnd(DefineEnd
);
497 Diagnostic
<< FixItHint::CreateRemoval(DefineRange
);
499 SourceLocation NameEnd
= Lexer::getLocForEndOfToken(
500 Macro
.Directive
->getMacroInfo()->getDefinitionLoc(), 0, SM
, LangOpts
);
501 Diagnostic
<< FixItHint::CreateInsertion(NameEnd
, " =");
503 SourceLocation ValueEnd
= Lexer::getLocForEndOfToken(
504 Macro
.Directive
->getMacroInfo()->getDefinitionEndLoc(), 0, SM
,
506 if (I
< MacroList
.size() - 1)
507 Diagnostic
<< FixItHint::CreateInsertion(ValueEnd
, ",");
510 SourceLocation End
= Lexer::getLocForEndOfToken(
511 MacroList
.back().Directive
->getMacroInfo()->getDefinitionEndLoc(), 0, SM
,
513 End
= SM
.translateLineCol(SM
.getFileID(End
),
514 SM
.getSpellingLineNumber(End
) + 1, 1);
515 Diagnostic
<< FixItHint::CreateInsertion(End
, "};\n");
518 void MacroToEnumCheck::registerPPCallbacks(const SourceManager
&SM
,
520 Preprocessor
*ModuleExpanderPP
) {
521 auto Callback
= std::make_unique
<MacroToEnumCallbacks
>(this, getLangOpts(), SM
);
522 PPCallback
= Callback
.get();
523 PP
->addPPCallbacks(std::move(Callback
));
526 void MacroToEnumCheck::registerMatchers(ast_matchers::MatchFinder
*Finder
) {
527 using namespace ast_matchers
;
528 auto TopLevelDecl
= hasParent(translationUnitDecl());
529 Finder
->addMatcher(decl(TopLevelDecl
).bind("top"), this);
532 static bool isValid(SourceRange Range
) {
533 return Range
.getBegin().isValid() && Range
.getEnd().isValid();
536 static bool empty(SourceRange Range
) {
537 return Range
.getBegin() == Range
.getEnd();
540 void MacroToEnumCheck::check(
541 const ast_matchers::MatchFinder::MatchResult
&Result
) {
542 auto *TLDecl
= Result
.Nodes
.getNodeAs
<Decl
>("top");
543 if (TLDecl
== nullptr)
546 SourceRange Range
= TLDecl
->getSourceRange();
547 if (auto *TemplateFn
= Result
.Nodes
.getNodeAs
<FunctionTemplateDecl
>("top")) {
548 if (TemplateFn
->isThisDeclarationADefinition() && TemplateFn
->hasBody())
549 Range
= SourceRange
{TemplateFn
->getBeginLoc(),
550 TemplateFn
->getUnderlyingDecl()->getBodyRBrace()};
553 if (isValid(Range
) && !empty(Range
))
554 PPCallback
->invalidateRange(Range
);
557 } // namespace clang::tidy::modernize