blob: 428bfb13064eeaf777d840d9c4dddf6bb85e98a4 [file] [log] [blame]
//===--- MacroToEnumCheck.cpp - clang-tidy --------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
#include "MacroToEnumCheck.h"
#include "IntegralLiteralExpressionMatcher.h"
#include "clang/AST/ASTContext.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/Lex/Preprocessor.h"
#include "llvm/ADT/STLExtras.h"
#include <algorithm>
#include <cassert>
#include <cctype>
#include <string>
namespace clang::tidy::modernize {
static bool hasOnlyComments(SourceLocation Loc, const LangOptions &Options,
StringRef Text) {
// Use a lexer to look for tokens; if we find something other than a single
// hash, then there were intervening tokens between macro definitions.
std::string Buffer{Text};
Lexer Lex(Loc, Options, Buffer.c_str(), Buffer.c_str(),
Buffer.c_str() + Buffer.size());
Token Tok;
bool SeenHash = false;
while (!Lex.LexFromRawLexer(Tok)) {
if (Tok.getKind() == tok::hash && !SeenHash) {
SeenHash = true;
continue;
}
return false;
}
// Everything in between was whitespace, so now just look for two blank lines,
// consisting of two consecutive EOL sequences, either '\n', '\r' or '\r\n'.
enum class WhiteSpace {
Nothing,
CR,
LF,
CRLF,
CRLFCR,
};
WhiteSpace State = WhiteSpace::Nothing;
for (char C : Text) {
switch (C) {
case '\r':
if (State == WhiteSpace::CR)
return false;
State = State == WhiteSpace::CRLF ? WhiteSpace::CRLFCR : WhiteSpace::CR;
break;
case '\n':
if (State == WhiteSpace::LF || State == WhiteSpace::CRLFCR)
return false;
State = State == WhiteSpace::CR ? WhiteSpace::CRLF : WhiteSpace::LF;
break;
default:
State = WhiteSpace::Nothing;
break;
}
}
return true;
}
static StringRef getTokenName(const Token &Tok) {
return Tok.is(tok::raw_identifier) ? Tok.getRawIdentifier()
: Tok.getIdentifierInfo()->getName();
}
namespace {
struct EnumMacro {
EnumMacro(Token Name, const MacroDirective *Directive)
: Name(Name), Directive(Directive) {}
Token Name;
const MacroDirective *Directive;
};
using MacroList = SmallVector<EnumMacro>;
enum class IncludeGuard { None, FileChanged, IfGuard, DefineGuard };
struct FileState {
FileState()
: ConditionScopes(0), LastLine(0), GuardScanner(IncludeGuard::None) {}
int ConditionScopes;
unsigned int LastLine;
IncludeGuard GuardScanner;
SourceLocation LastMacroLocation;
};
} // namespace
class MacroToEnumCallbacks : public PPCallbacks {
public:
MacroToEnumCallbacks(MacroToEnumCheck *Check, const LangOptions &LangOptions,
const SourceManager &SM)
: Check(Check), LangOpts(LangOptions), SM(SM) {}
void FileChanged(SourceLocation Loc, FileChangeReason Reason,
SrcMgr::CharacteristicKind FileType,
FileID PrevFID) override;
void InclusionDirective(SourceLocation HashLoc, const Token &IncludeTok,
StringRef FileName, bool IsAngled,
CharSourceRange FilenameRange,
OptionalFileEntryRef File, StringRef SearchPath,
StringRef RelativePath, const Module *Imported,
SrcMgr::CharacteristicKind FileType) override {
clearCurrentEnum(HashLoc);
}
// Keep track of macro definitions that look like enums.
void MacroDefined(const Token &MacroNameTok,
const MacroDirective *MD) override;
// Undefining an enum-like macro results in the enum set being dropped.
void MacroUndefined(const Token &MacroNameTok, const MacroDefinition &MD,
const MacroDirective *Undef) override;
// Conditional compilation clears any adjacent enum-like macros.
// Macros used in conditional expressions clear any adjacent enum-like
// macros.
// Include guards are either
// #if !defined(GUARD)
// or
// #ifndef GUARD
void If(SourceLocation Loc, SourceRange ConditionRange,
ConditionValueKind ConditionValue) override {
conditionStart(Loc);
checkCondition(ConditionRange);
}
void Ifndef(SourceLocation Loc, const Token &MacroNameTok,
const MacroDefinition &MD) override {
conditionStart(Loc);
checkName(MacroNameTok);
}
void Ifdef(SourceLocation Loc, const Token &MacroNameTok,
const MacroDefinition &MD) override {
conditionStart(Loc);
checkName(MacroNameTok);
}
void Elif(SourceLocation Loc, SourceRange ConditionRange,
ConditionValueKind ConditionValue, SourceLocation IfLoc) override {
checkCondition(ConditionRange);
}
void Elifdef(SourceLocation Loc, const Token &MacroNameTok,
const MacroDefinition &MD) override {
checkName(MacroNameTok);
}
void Elifdef(SourceLocation Loc, SourceRange ConditionRange,
SourceLocation IfLoc) override {
PPCallbacks::Elifdef(Loc, ConditionRange, IfLoc);
}
void Elifndef(SourceLocation Loc, const Token &MacroNameTok,
const MacroDefinition &MD) override {
checkName(MacroNameTok);
}
void Elifndef(SourceLocation Loc, SourceRange ConditionRange,
SourceLocation IfLoc) override {
PPCallbacks::Elifndef(Loc, ConditionRange, IfLoc);
}
void Endif(SourceLocation Loc, SourceLocation IfLoc) override;
void PragmaDirective(SourceLocation Loc,
PragmaIntroducerKind Introducer) override;
// After we've seen everything, issue warnings and fix-its.
void EndOfMainFile() override;
void invalidateRange(SourceRange Range);
private:
void newEnum() {
if (Enums.empty() || !Enums.back().empty())
Enums.emplace_back();
}
bool insideConditional() const {
return (CurrentFile->GuardScanner == IncludeGuard::DefineGuard &&
CurrentFile->ConditionScopes > 1) ||
(CurrentFile->GuardScanner != IncludeGuard::DefineGuard &&
CurrentFile->ConditionScopes > 0);
}
bool isConsecutiveMacro(const MacroDirective *MD) const;
void rememberLastMacroLocation(const MacroDirective *MD) {
CurrentFile->LastLine = SM.getSpellingLineNumber(MD->getLocation());
CurrentFile->LastMacroLocation = Lexer::getLocForEndOfToken(
MD->getMacroInfo()->getDefinitionEndLoc(), 0, SM, LangOpts);
}
void clearLastMacroLocation() {
CurrentFile->LastLine = 0;
CurrentFile->LastMacroLocation = SourceLocation{};
}
void clearCurrentEnum(SourceLocation Loc);
void conditionStart(const SourceLocation &Loc);
void checkCondition(SourceRange ConditionRange);
void checkName(const Token &MacroNameTok);
void rememberExpressionName(const Token &Tok);
void rememberExpressionTokens(ArrayRef<Token> MacroTokens);
void invalidateExpressionNames();
void issueDiagnostics();
void warnMacroEnum(const EnumMacro &Macro) const;
void fixEnumMacro(const MacroList &MacroList) const;
bool isInitializer(ArrayRef<Token> MacroTokens);
MacroToEnumCheck *Check;
const LangOptions &LangOpts;
const SourceManager &SM;
SmallVector<MacroList> Enums;
SmallVector<FileState> Files;
std::vector<std::string> ExpressionNames;
FileState *CurrentFile = nullptr;
};
bool MacroToEnumCallbacks::isConsecutiveMacro(const MacroDirective *MD) const {
if (CurrentFile->LastMacroLocation.isInvalid())
return false;
SourceLocation Loc = MD->getLocation();
if (CurrentFile->LastLine + 1 == SM.getSpellingLineNumber(Loc))
return true;
SourceLocation Define =
SM.translateLineCol(SM.getFileID(Loc), SM.getSpellingLineNumber(Loc), 1);
CharSourceRange BetweenMacros{
SourceRange{CurrentFile->LastMacroLocation, Define}, true};
CharSourceRange CharRange =
Lexer::makeFileCharRange(BetweenMacros, SM, LangOpts);
StringRef BetweenText = Lexer::getSourceText(CharRange, SM, LangOpts);
return hasOnlyComments(Define, LangOpts, BetweenText);
}
void MacroToEnumCallbacks::clearCurrentEnum(SourceLocation Loc) {
// Only drop the most recent Enum set if the directive immediately follows.
if (!Enums.empty() && !Enums.back().empty() &&
SM.getSpellingLineNumber(Loc) == CurrentFile->LastLine + 1)
Enums.pop_back();
clearLastMacroLocation();
}
void MacroToEnumCallbacks::conditionStart(const SourceLocation &Loc) {
++CurrentFile->ConditionScopes;
clearCurrentEnum(Loc);
if (CurrentFile->GuardScanner == IncludeGuard::FileChanged)
CurrentFile->GuardScanner = IncludeGuard::IfGuard;
}
void MacroToEnumCallbacks::checkCondition(SourceRange Range) {
CharSourceRange CharRange = Lexer::makeFileCharRange(
CharSourceRange::getTokenRange(Range), SM, LangOpts);
std::string Text = Lexer::getSourceText(CharRange, SM, LangOpts).str();
Lexer Lex(CharRange.getBegin(), LangOpts, Text.data(), Text.data(),
Text.data() + Text.size());
Token Tok;
bool End = false;
while (!End) {
End = Lex.LexFromRawLexer(Tok);
if (Tok.is(tok::raw_identifier) &&
Tok.getRawIdentifier().str() != "defined")
checkName(Tok);
}
}
void MacroToEnumCallbacks::checkName(const Token &MacroNameTok) {
rememberExpressionName(MacroNameTok);
StringRef Id = getTokenName(MacroNameTok);
llvm::erase_if(Enums, [&Id](const MacroList &MacroList) {
return llvm::any_of(MacroList, [&Id](const EnumMacro &Macro) {
return getTokenName(Macro.Name) == Id;
});
});
}
void MacroToEnumCallbacks::rememberExpressionName(const Token &Tok) {
std::string Id = getTokenName(Tok).str();
auto Pos = llvm::lower_bound(ExpressionNames, Id);
if (Pos == ExpressionNames.end() || *Pos != Id) {
ExpressionNames.insert(Pos, Id);
}
}
void MacroToEnumCallbacks::rememberExpressionTokens(
ArrayRef<Token> MacroTokens) {
for (Token Tok : MacroTokens) {
if (Tok.isAnyIdentifier())
rememberExpressionName(Tok);
}
}
void MacroToEnumCallbacks::FileChanged(SourceLocation Loc,
FileChangeReason Reason,
SrcMgr::CharacteristicKind FileType,
FileID PrevFID) {
newEnum();
if (Reason == EnterFile) {
Files.emplace_back();
if (!SM.isInMainFile(Loc))
Files.back().GuardScanner = IncludeGuard::FileChanged;
} else if (Reason == ExitFile) {
assert(CurrentFile->ConditionScopes == 0);
Files.pop_back();
}
CurrentFile = &Files.back();
}
bool MacroToEnumCallbacks::isInitializer(ArrayRef<Token> MacroTokens)
{
IntegralLiteralExpressionMatcher Matcher(MacroTokens, LangOpts.C99 == 0);
bool Matched = Matcher.match();
bool isC = !LangOpts.CPlusPlus;
if (isC && (Matcher.largestLiteralSize() != LiteralSize::Int &&
Matcher.largestLiteralSize() != LiteralSize::UnsignedInt))
return false;
return Matched;
}
// Any defined but rejected macro is scanned for identifiers that
// are to be excluded as enums.
void MacroToEnumCallbacks::MacroDefined(const Token &MacroNameTok,
const MacroDirective *MD) {
// Include guards are never candidates for becoming an enum.
if (CurrentFile->GuardScanner == IncludeGuard::IfGuard) {
CurrentFile->GuardScanner = IncludeGuard::DefineGuard;
return;
}
if (insideConditional())
return;
if (SM.getFilename(MD->getLocation()).empty())
return;
const MacroInfo *Info = MD->getMacroInfo();
ArrayRef<Token> MacroTokens = Info->tokens();
if (Info->isBuiltinMacro() || MacroTokens.empty())
return;
if (Info->isFunctionLike()) {
rememberExpressionTokens(MacroTokens);
return;
}
if (!isInitializer(MacroTokens))
return;
if (!isConsecutiveMacro(MD))
newEnum();
Enums.back().emplace_back(MacroNameTok, MD);
rememberLastMacroLocation(MD);
}
// Any macro that is undefined removes all adjacent macros from consideration as
// an enum and starts a new enum scan.
void MacroToEnumCallbacks::MacroUndefined(const Token &MacroNameTok,
const MacroDefinition &MD,
const MacroDirective *Undef) {
rememberExpressionName(MacroNameTok);
auto MatchesToken = [&MacroNameTok](const EnumMacro &Macro) {
return getTokenName(Macro.Name) == getTokenName(MacroNameTok);
};
auto It = llvm::find_if(Enums, [MatchesToken](const MacroList &MacroList) {
return llvm::any_of(MacroList, MatchesToken);
});
if (It != Enums.end())
Enums.erase(It);
clearLastMacroLocation();
CurrentFile->GuardScanner = IncludeGuard::None;
}
void MacroToEnumCallbacks::Endif(SourceLocation Loc, SourceLocation IfLoc) {
// The if directive for the include guard isn't counted in the
// ConditionScopes.
if (CurrentFile->ConditionScopes == 0 &&
CurrentFile->GuardScanner == IncludeGuard::DefineGuard)
return;
// We don't need to clear the current enum because the start of the
// conditional block already took care of that.
assert(CurrentFile->ConditionScopes > 0);
--CurrentFile->ConditionScopes;
}
namespace {
template <size_t N>
bool textEquals(const char (&Needle)[N], const char *HayStack) {
return StringRef{HayStack, N - 1} == Needle;
}
template <size_t N> size_t len(const char (&)[N]) { return N - 1; }
} // namespace
void MacroToEnumCallbacks::PragmaDirective(SourceLocation Loc,
PragmaIntroducerKind Introducer) {
if (CurrentFile->GuardScanner != IncludeGuard::FileChanged)
return;
bool Invalid = false;
const char *Text = SM.getCharacterData(
Lexer::getLocForEndOfToken(Loc, 0, SM, LangOpts), &Invalid);
if (Invalid)
return;
while (*Text && std::isspace(*Text))
++Text;
if (textEquals("pragma", Text))
return;
Text += len("pragma");
while (*Text && std::isspace(*Text))
++Text;
if (textEquals("once", Text))
CurrentFile->GuardScanner = IncludeGuard::IfGuard;
}
void MacroToEnumCallbacks::invalidateExpressionNames() {
for (const std::string &Id : ExpressionNames) {
llvm::erase_if(Enums, [Id](const MacroList &MacroList) {
return llvm::any_of(MacroList, [&Id](const EnumMacro &Macro) {
return getTokenName(Macro.Name) == Id;
});
});
}
}
void MacroToEnumCallbacks::EndOfMainFile() {
invalidateExpressionNames();
issueDiagnostics();
}
void MacroToEnumCallbacks::invalidateRange(SourceRange Range) {
llvm::erase_if(Enums, [Range](const MacroList &MacroList) {
return llvm::any_of(MacroList, [Range](const EnumMacro &Macro) {
return Macro.Directive->getLocation() >= Range.getBegin() &&
Macro.Directive->getLocation() <= Range.getEnd();
});
});
}
void MacroToEnumCallbacks::issueDiagnostics() {
for (const MacroList &MacroList : Enums) {
if (MacroList.empty())
continue;
for (const EnumMacro &Macro : MacroList)
warnMacroEnum(Macro);
fixEnumMacro(MacroList);
}
}
void MacroToEnumCallbacks::warnMacroEnum(const EnumMacro &Macro) const {
Check->diag(Macro.Directive->getLocation(),
"macro '%0' defines an integral constant; prefer an enum instead")
<< getTokenName(Macro.Name);
}
void MacroToEnumCallbacks::fixEnumMacro(const MacroList &MacroList) const {
SourceLocation Begin =
MacroList.front().Directive->getMacroInfo()->getDefinitionLoc();
Begin = SM.translateLineCol(SM.getFileID(Begin),
SM.getSpellingLineNumber(Begin), 1);
DiagnosticBuilder Diagnostic =
Check->diag(Begin, "replace macro with enum")
<< FixItHint::CreateInsertion(Begin, "enum {\n");
for (size_t I = 0u; I < MacroList.size(); ++I) {
const EnumMacro &Macro = MacroList[I];
SourceLocation DefineEnd =
Macro.Directive->getMacroInfo()->getDefinitionLoc();
SourceLocation DefineBegin = SM.translateLineCol(
SM.getFileID(DefineEnd), SM.getSpellingLineNumber(DefineEnd), 1);
CharSourceRange DefineRange;
DefineRange.setBegin(DefineBegin);
DefineRange.setEnd(DefineEnd);
Diagnostic << FixItHint::CreateRemoval(DefineRange);
SourceLocation NameEnd = Lexer::getLocForEndOfToken(
Macro.Directive->getMacroInfo()->getDefinitionLoc(), 0, SM, LangOpts);
Diagnostic << FixItHint::CreateInsertion(NameEnd, " =");
SourceLocation ValueEnd = Lexer::getLocForEndOfToken(
Macro.Directive->getMacroInfo()->getDefinitionEndLoc(), 0, SM,
LangOpts);
if (I < MacroList.size() - 1)
Diagnostic << FixItHint::CreateInsertion(ValueEnd, ",");
}
SourceLocation End = Lexer::getLocForEndOfToken(
MacroList.back().Directive->getMacroInfo()->getDefinitionEndLoc(), 0, SM,
LangOpts);
End = SM.translateLineCol(SM.getFileID(End),
SM.getSpellingLineNumber(End) + 1, 1);
Diagnostic << FixItHint::CreateInsertion(End, "};\n");
}
void MacroToEnumCheck::registerPPCallbacks(const SourceManager &SM,
Preprocessor *PP,
Preprocessor *ModuleExpanderPP) {
auto Callback = std::make_unique<MacroToEnumCallbacks>(this, getLangOpts(), SM);
PPCallback = Callback.get();
PP->addPPCallbacks(std::move(Callback));
}
void MacroToEnumCheck::registerMatchers(ast_matchers::MatchFinder *Finder) {
using namespace ast_matchers;
auto TopLevelDecl = hasParent(translationUnitDecl());
Finder->addMatcher(decl(TopLevelDecl).bind("top"), this);
}
static bool isValid(SourceRange Range) {
return Range.getBegin().isValid() && Range.getEnd().isValid();
}
static bool empty(SourceRange Range) {
return Range.getBegin() == Range.getEnd();
}
void MacroToEnumCheck::check(
const ast_matchers::MatchFinder::MatchResult &Result) {
auto *TLDecl = Result.Nodes.getNodeAs<Decl>("top");
if (TLDecl == nullptr)
return;
SourceRange Range = TLDecl->getSourceRange();
if (auto *TemplateFn = Result.Nodes.getNodeAs<FunctionTemplateDecl>("top")) {
if (TemplateFn->isThisDeclarationADefinition() && TemplateFn->hasBody())
Range = SourceRange{TemplateFn->getBeginLoc(),
TemplateFn->getUnderlyingDecl()->getBodyRBrace()};
}
if (isValid(Range) && !empty(Range))
PPCallback->invalidateRange(Range);
}
} // namespace clang::tidy::modernize