1//===--- MacroToEnumCheck.cpp - clang-tidy --------------------------------===//
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 "MacroToEnumCheck.h"
10#include "IntegralLiteralExpressionMatcher.h"
11
12#include "clang/AST/ASTContext.h"
13#include "clang/ASTMatchers/ASTMatchFinder.h"
14#include "clang/Lex/Preprocessor.h"
15#include "llvm/ADT/STLExtras.h"
16#include <algorithm>
17#include <cassert>
18#include <cctype>
19#include <string>
20
21namespace clang::tidy::modernize {
22
23static bool hasOnlyComments(SourceLocation Loc, const LangOptions &Options,
24 StringRef Text) {
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());
30 Token Tok;
31 bool SeenHash = false;
32 while (!Lex.LexFromRawLexer(Result&: Tok)) {
33 if (Tok.getKind() == tok::hash && !SeenHash) {
34 SeenHash = true;
35 continue;
36 }
37 return false;
38 }
39
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 {
43 Nothing,
44 CR,
45 LF,
46 CRLF,
47 CRLFCR,
48 };
49
50 WhiteSpace State = WhiteSpace::Nothing;
51 for (char C : Text) {
52 switch (C) {
53 case '\r':
54 if (State == WhiteSpace::CR)
55 return false;
56
57 State = State == WhiteSpace::CRLF ? WhiteSpace::CRLFCR : WhiteSpace::CR;
58 break;
59
60 case '\n':
61 if (State == WhiteSpace::LF || State == WhiteSpace::CRLFCR)
62 return false;
63
64 State = State == WhiteSpace::CR ? WhiteSpace::CRLF : WhiteSpace::LF;
65 break;
66
67 default:
68 State = WhiteSpace::Nothing;
69 break;
70 }
71 }
72
73 return true;
74}
75
76static StringRef getTokenName(const Token &Tok) {
77 return Tok.is(K: tok::raw_identifier) ? Tok.getRawIdentifier()
78 : Tok.getIdentifierInfo()->getName();
79}
80
81namespace {
82
83struct EnumMacro {
84 EnumMacro(Token Name, const MacroDirective *Directive)
85 : Name(Name), Directive(Directive) {}
86
87 Token Name;
88 const MacroDirective *Directive;
89};
90
91using MacroList = SmallVector<EnumMacro>;
92
93enum class IncludeGuard { None, FileChanged, IfGuard, DefineGuard };
94
95struct FileState {
96 FileState() = default;
97
98 int ConditionScopes = 0;
99 unsigned int LastLine = 0;
100 IncludeGuard GuardScanner = IncludeGuard::None;
101 SourceLocation LastMacroLocation;
102};
103
104} // namespace
105
106class MacroToEnumCallbacks : public PPCallbacks {
107public:
108 MacroToEnumCallbacks(MacroToEnumCheck *Check, const LangOptions &LangOptions,
109 const SourceManager &SM)
110 : Check(Check), LangOpts(LangOptions), SM(SM) {}
111
112 void FileChanged(SourceLocation Loc, FileChangeReason Reason,
113 SrcMgr::CharacteristicKind FileType,
114 FileID PrevFID) override;
115
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,
121 bool ModuleImported,
122 SrcMgr::CharacteristicKind FileType) override {
123 clearCurrentEnum(Loc: HashLoc);
124 }
125
126 // Keep track of macro definitions that look like enums.
127 void MacroDefined(const Token &MacroNameTok,
128 const MacroDirective *MD) override;
129
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;
133
134 // Conditional compilation clears any adjacent enum-like macros.
135 // Macros used in conditional expressions clear any adjacent enum-like
136 // macros.
137 // Include guards are either
138 // #if !defined(GUARD)
139 // or
140 // #ifndef GUARD
141 void If(SourceLocation Loc, SourceRange ConditionRange,
142 ConditionValueKind ConditionValue) override {
143 conditionStart(Loc);
144 checkCondition(ConditionRange);
145 }
146 void Ifndef(SourceLocation Loc, const Token &MacroNameTok,
147 const MacroDefinition &MD) override {
148 conditionStart(Loc);
149 checkName(MacroNameTok);
150 }
151 void Ifdef(SourceLocation Loc, const Token &MacroNameTok,
152 const MacroDefinition &MD) override {
153 conditionStart(Loc);
154 checkName(MacroNameTok);
155 }
156 void Elif(SourceLocation Loc, SourceRange ConditionRange,
157 ConditionValueKind ConditionValue, SourceLocation IfLoc) override {
158 checkCondition(ConditionRange);
159 }
160 void Elifdef(SourceLocation Loc, const Token &MacroNameTok,
161 const MacroDefinition &MD) override {
162 checkName(MacroNameTok);
163 }
164 void Elifdef(SourceLocation Loc, SourceRange ConditionRange,
165 SourceLocation IfLoc) override {
166 PPCallbacks::Elifdef(Loc, ConditionRange, IfLoc);
167 }
168 void Elifndef(SourceLocation Loc, const Token &MacroNameTok,
169 const MacroDefinition &MD) override {
170 checkName(MacroNameTok);
171 }
172 void Elifndef(SourceLocation Loc, SourceRange ConditionRange,
173 SourceLocation IfLoc) override {
174 PPCallbacks::Elifndef(Loc, ConditionRange, IfLoc);
175 }
176 void Endif(SourceLocation Loc, SourceLocation IfLoc) override;
177 void PragmaDirective(SourceLocation Loc,
178 PragmaIntroducerKind Introducer) override;
179
180 // After we've seen everything, issue warnings and fix-its.
181 void EndOfMainFile() override;
182
183 void invalidateRange(SourceRange Range);
184
185private:
186 void newEnum() {
187 if (Enums.empty() || !Enums.back().empty())
188 Enums.emplace_back();
189 }
190 bool insideConditional() const {
191 return (CurrentFile->GuardScanner == IncludeGuard::DefineGuard &&
192 CurrentFile->ConditionScopes > 1) ||
193 (CurrentFile->GuardScanner != IncludeGuard::DefineGuard &&
194 CurrentFile->ConditionScopes > 0);
195 }
196 bool isConsecutiveMacro(const MacroDirective *MD) const;
197 void rememberLastMacroLocation(const MacroDirective *MD) {
198 CurrentFile->LastLine = SM.getSpellingLineNumber(Loc: MD->getLocation());
199 CurrentFile->LastMacroLocation = Lexer::getLocForEndOfToken(
200 Loc: MD->getMacroInfo()->getDefinitionEndLoc(), Offset: 0, SM, LangOpts);
201 }
202 void clearLastMacroLocation() {
203 CurrentFile->LastLine = 0;
204 CurrentFile->LastMacroLocation = SourceLocation{};
205 }
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);
217
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;
225};
226
227bool MacroToEnumCallbacks::isConsecutiveMacro(const MacroDirective *MD) const {
228 if (CurrentFile->LastMacroLocation.isInvalid())
229 return false;
230
231 SourceLocation Loc = MD->getLocation();
232 if (CurrentFile->LastLine + 1 == SM.getSpellingLineNumber(Loc))
233 return true;
234
235 SourceLocation Define =
236 SM.translateLineCol(FID: SM.getFileID(SpellingLoc: Loc), Line: SM.getSpellingLineNumber(Loc), Col: 1);
237 CharSourceRange BetweenMacros{
238 SourceRange{CurrentFile->LastMacroLocation, Define}, true};
239 CharSourceRange CharRange =
240 Lexer::makeFileCharRange(Range: BetweenMacros, SM, LangOpts);
241 StringRef BetweenText = Lexer::getSourceText(Range: CharRange, SM, LangOpts);
242 return hasOnlyComments(Loc: Define, Options: LangOpts, Text: BetweenText);
243}
244
245void 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)
249 Enums.pop_back();
250
251 clearLastMacroLocation();
252}
253
254void MacroToEnumCallbacks::conditionStart(const SourceLocation &Loc) {
255 ++CurrentFile->ConditionScopes;
256 clearCurrentEnum(Loc);
257 if (CurrentFile->GuardScanner == IncludeGuard::FileChanged)
258 CurrentFile->GuardScanner = IncludeGuard::IfGuard;
259}
260
261void MacroToEnumCallbacks::checkCondition(SourceRange Range) {
262 CharSourceRange CharRange = Lexer::makeFileCharRange(
263 Range: CharSourceRange::getTokenRange(R: Range), SM, LangOpts);
264 std::string Text = Lexer::getSourceText(Range: CharRange, SM, LangOpts).str();
265 Lexer Lex(CharRange.getBegin(), LangOpts, Text.data(), Text.data(),
266 Text.data() + Text.size());
267 Token Tok;
268 bool End = false;
269 while (!End) {
270 End = Lex.LexFromRawLexer(Result&: Tok);
271 if (Tok.is(K: tok::raw_identifier) &&
272 Tok.getRawIdentifier().str() != "defined")
273 checkName(MacroNameTok: Tok);
274 }
275}
276
277void MacroToEnumCallbacks::checkName(const Token &MacroNameTok) {
278 rememberExpressionName(Tok: MacroNameTok);
279
280 StringRef Id = getTokenName(Tok: MacroNameTok);
281 llvm::erase_if(C&: Enums, P: [&Id](const MacroList &MacroList) {
282 return llvm::any_of(Range: MacroList, P: [&Id](const EnumMacro &Macro) {
283 return getTokenName(Tok: Macro.Name) == Id;
284 });
285 });
286}
287
288void MacroToEnumCallbacks::rememberExpressionName(const Token &Tok) {
289 std::string Id = getTokenName(Tok).str();
290 auto Pos = llvm::lower_bound(Range&: ExpressionNames, Value&: Id);
291 if (Pos == ExpressionNames.end() || *Pos != Id) {
292 ExpressionNames.insert(position: Pos, x: Id);
293 }
294}
295
296void MacroToEnumCallbacks::rememberExpressionTokens(
297 ArrayRef<Token> MacroTokens) {
298 for (Token Tok : MacroTokens) {
299 if (Tok.isAnyIdentifier())
300 rememberExpressionName(Tok);
301 }
302}
303
304void MacroToEnumCallbacks::FileChanged(SourceLocation Loc,
305 FileChangeReason Reason,
306 SrcMgr::CharacteristicKind FileType,
307 FileID PrevFID) {
308 newEnum();
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);
315 Files.pop_back();
316 }
317 CurrentFile = &Files.back();
318}
319
320bool MacroToEnumCallbacks::isInitializer(ArrayRef<Token> MacroTokens)
321{
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))
327 return false;
328
329 return Matched;
330}
331
332
333// Any defined but rejected macro is scanned for identifiers that
334// are to be excluded as enums.
335void 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;
340 return;
341 }
342
343 if (insideConditional())
344 return;
345
346 if (SM.getFilename(SpellingLoc: MD->getLocation()).empty())
347 return;
348
349 const MacroInfo *Info = MD->getMacroInfo();
350 ArrayRef<Token> MacroTokens = Info->tokens();
351 if (Info->isBuiltinMacro() || MacroTokens.empty())
352 return;
353 if (Info->isFunctionLike()) {
354 rememberExpressionTokens(MacroTokens);
355 return;
356 }
357
358 if (!isInitializer(MacroTokens))
359 return;
360
361 if (!isConsecutiveMacro(MD))
362 newEnum();
363 Enums.back().emplace_back(Args: MacroNameTok, Args&: MD);
364 rememberLastMacroLocation(MD);
365}
366
367// Any macro that is undefined removes all adjacent macros from consideration as
368// an enum and starts a new enum scan.
369void MacroToEnumCallbacks::MacroUndefined(const Token &MacroNameTok,
370 const MacroDefinition &MD,
371 const MacroDirective *Undef) {
372 rememberExpressionName(Tok: MacroNameTok);
373
374 auto MatchesToken = [&MacroNameTok](const EnumMacro &Macro) {
375 return getTokenName(Tok: Macro.Name) == getTokenName(Tok: MacroNameTok);
376 };
377
378 auto It = llvm::find_if(Range&: Enums, P: [MatchesToken](const MacroList &MacroList) {
379 return llvm::any_of(Range: MacroList, P: MatchesToken);
380 });
381 if (It != Enums.end())
382 Enums.erase(CI: It);
383
384 clearLastMacroLocation();
385 CurrentFile->GuardScanner = IncludeGuard::None;
386}
387
388void MacroToEnumCallbacks::Endif(SourceLocation Loc, SourceLocation IfLoc) {
389 // The if directive for the include guard isn't counted in the
390 // ConditionScopes.
391 if (CurrentFile->ConditionScopes == 0 &&
392 CurrentFile->GuardScanner == IncludeGuard::DefineGuard)
393 return;
394
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;
399}
400
401namespace {
402
403template <size_t N>
404bool textEquals(const char (&Needle)[N], const char *HayStack) {
405 return StringRef{HayStack, N - 1} == Needle;
406}
407
408template <size_t N> size_t len(const char (&)[N]) { return N - 1; }
409
410} // namespace
411
412void MacroToEnumCallbacks::PragmaDirective(SourceLocation Loc,
413 PragmaIntroducerKind Introducer) {
414 if (CurrentFile->GuardScanner != IncludeGuard::FileChanged)
415 return;
416
417 bool Invalid = false;
418 const char *Text = SM.getCharacterData(
419 SL: Lexer::getLocForEndOfToken(Loc, Offset: 0, SM, LangOpts), Invalid: &Invalid);
420 if (Invalid)
421 return;
422
423 while (*Text && std::isspace(*Text))
424 ++Text;
425
426 if (textEquals(Needle: "pragma", HayStack: Text))
427 return;
428
429 Text += len("pragma");
430 while (*Text && std::isspace(*Text))
431 ++Text;
432
433 if (textEquals(Needle: "once", HayStack: Text))
434 CurrentFile->GuardScanner = IncludeGuard::IfGuard;
435}
436
437void MacroToEnumCallbacks::invalidateExpressionNames() {
438 for (const std::string &Id : ExpressionNames) {
439 llvm::erase_if(C&: Enums, P: [Id](const MacroList &MacroList) {
440 return llvm::any_of(Range: MacroList, P: [&Id](const EnumMacro &Macro) {
441 return getTokenName(Tok: Macro.Name) == Id;
442 });
443 });
444 }
445}
446
447void MacroToEnumCallbacks::EndOfMainFile() {
448 invalidateExpressionNames();
449 issueDiagnostics();
450}
451
452void MacroToEnumCallbacks::invalidateRange(SourceRange Range) {
453 llvm::erase_if(C&: Enums, P: [Range](const MacroList &MacroList) {
454 return llvm::any_of(Range: MacroList, P: [Range](const EnumMacro &Macro) {
455 return Macro.Directive->getLocation() >= Range.getBegin() &&
456 Macro.Directive->getLocation() <= Range.getEnd();
457 });
458 });
459}
460
461void MacroToEnumCallbacks::issueDiagnostics() {
462 for (const MacroList &MacroList : Enums) {
463 if (MacroList.empty())
464 continue;
465
466 for (const EnumMacro &Macro : MacroList)
467 warnMacroEnum(Macro);
468
469 fixEnumMacro(MacroList);
470 }
471}
472
473void MacroToEnumCallbacks::warnMacroEnum(const EnumMacro &Macro) const {
474 Check->diag(Loc: Macro.Directive->getLocation(),
475 Description: "macro '%0' defines an integral constant; prefer an enum instead")
476 << getTokenName(Tok: Macro.Name);
477}
478
479void MacroToEnumCallbacks::fixEnumMacro(const MacroList &MacroList) const {
480 SourceLocation Begin =
481 MacroList.front().Directive->getMacroInfo()->getDefinitionLoc();
482 Begin = SM.translateLineCol(FID: SM.getFileID(SpellingLoc: Begin),
483 Line: SM.getSpellingLineNumber(Loc: Begin), Col: 1);
484 DiagnosticBuilder Diagnostic =
485 Check->diag(Loc: Begin, Description: "replace macro with enum")
486 << FixItHint::CreateInsertion(InsertionLoc: Begin, Code: "enum {\n");
487
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 FID: SM.getFileID(SpellingLoc: DefineEnd), Line: SM.getSpellingLineNumber(Loc: DefineEnd), Col: 1);
494 CharSourceRange DefineRange;
495 DefineRange.setBegin(DefineBegin);
496 DefineRange.setEnd(DefineEnd);
497 Diagnostic << FixItHint::CreateRemoval(RemoveRange: DefineRange);
498
499 SourceLocation NameEnd = Lexer::getLocForEndOfToken(
500 Loc: Macro.Directive->getMacroInfo()->getDefinitionLoc(), Offset: 0, SM, LangOpts);
501 Diagnostic << FixItHint::CreateInsertion(InsertionLoc: NameEnd, Code: " =");
502
503 SourceLocation ValueEnd = Lexer::getLocForEndOfToken(
504 Loc: Macro.Directive->getMacroInfo()->getDefinitionEndLoc(), Offset: 0, SM,
505 LangOpts);
506 if (I < MacroList.size() - 1)
507 Diagnostic << FixItHint::CreateInsertion(InsertionLoc: ValueEnd, Code: ",");
508 }
509
510 SourceLocation End = Lexer::getLocForEndOfToken(
511 Loc: MacroList.back().Directive->getMacroInfo()->getDefinitionEndLoc(), Offset: 0, SM,
512 LangOpts);
513 End = SM.translateLineCol(FID: SM.getFileID(SpellingLoc: End),
514 Line: SM.getSpellingLineNumber(Loc: End) + 1, Col: 1);
515 Diagnostic << FixItHint::CreateInsertion(InsertionLoc: End, Code: "};\n");
516}
517
518void MacroToEnumCheck::registerPPCallbacks(const SourceManager &SM,
519 Preprocessor *PP,
520 Preprocessor *ModuleExpanderPP) {
521 auto Callback = std::make_unique<MacroToEnumCallbacks>(args: this, args: getLangOpts(), args: SM);
522 PPCallback = Callback.get();
523 PP->addPPCallbacks(C: std::move(Callback));
524}
525
526void MacroToEnumCheck::registerMatchers(ast_matchers::MatchFinder *Finder) {
527 using namespace ast_matchers;
528 auto TopLevelDecl = hasParent(translationUnitDecl());
529 Finder->addMatcher(NodeMatch: decl(TopLevelDecl).bind(ID: "top"), Action: this);
530}
531
532static bool isValid(SourceRange Range) {
533 return Range.getBegin().isValid() && Range.getEnd().isValid();
534}
535
536static bool empty(SourceRange Range) {
537 return Range.getBegin() == Range.getEnd();
538}
539
540void MacroToEnumCheck::check(
541 const ast_matchers::MatchFinder::MatchResult &Result) {
542 auto *TLDecl = Result.Nodes.getNodeAs<Decl>(ID: "top");
543 if (TLDecl == nullptr)
544 return;
545
546 SourceRange Range = TLDecl->getSourceRange();
547 if (auto *TemplateFn = Result.Nodes.getNodeAs<FunctionTemplateDecl>(ID: "top")) {
548 if (TemplateFn->isThisDeclarationADefinition() && TemplateFn->hasBody())
549 Range = SourceRange{TemplateFn->getBeginLoc(),
550 TemplateFn->getUnderlyingDecl()->getBodyRBrace()};
551 }
552
553 if (isValid(Range) && !empty(Range))
554 PPCallback->invalidateRange(Range);
555}
556
557} // namespace clang::tidy::modernize
558

source code of clang-tools-extra/clang-tidy/modernize/MacroToEnumCheck.cpp