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