298 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 "HeaderGuard.h"10#include "clang/Frontend/CompilerInstance.h"11#include "clang/Lex/PPCallbacks.h"12#include "clang/Lex/Preprocessor.h"13#include "llvm/Support/Path.h"14 15namespace clang::tidy::utils {16 17/// canonicalize a path by removing ./ and ../ components.18static std::string cleanPath(StringRef Path) {19 SmallString<256> Result = Path;20 llvm::sys::path::remove_dots(Result, true);21 return std::string(Result);22}23 24namespace {25class HeaderGuardPPCallbacks : public PPCallbacks {26public:27 HeaderGuardPPCallbacks(Preprocessor *PP, HeaderGuardCheck *Check)28 : PP(PP), Check(Check) {}29 30 void FileChanged(SourceLocation Loc, FileChangeReason Reason,31 SrcMgr::CharacteristicKind FileType,32 FileID PrevFID) override {33 // Record all files we enter. We'll need them to diagnose headers without34 // guards.35 const SourceManager &SM = PP->getSourceManager();36 if (Reason == EnterFile && FileType == SrcMgr::C_User) {37 if (OptionalFileEntryRef FE =38 SM.getFileEntryRefForID(SM.getFileID(Loc))) {39 const std::string FileName = cleanPath(FE->getName());40 Files[FileName] = *FE;41 }42 }43 }44 45 void Ifndef(SourceLocation Loc, const Token &MacroNameTok,46 const MacroDefinition &MD) override {47 if (MD)48 return;49 50 // Record #ifndefs that succeeded. We also need the Location of the Name.51 Ifndefs[MacroNameTok.getIdentifierInfo()] =52 std::make_pair(Loc, MacroNameTok.getLocation());53 }54 55 void MacroDefined(const Token &MacroNameTok,56 const MacroDirective *MD) override {57 // Record all defined macros. We store the whole token to get info on the58 // name later.59 Macros.emplace_back(MacroNameTok, MD->getMacroInfo());60 }61 62 void Endif(SourceLocation Loc, SourceLocation IfLoc) override {63 // Record all #endif and the corresponding #ifs (including #ifndefs).64 EndIfs[IfLoc] = Loc;65 }66 67 void EndOfMainFile() override {68 // Now that we have all this information from the preprocessor, use it!69 const SourceManager &SM = PP->getSourceManager();70 71 for (const auto &MacroEntry : Macros) {72 const MacroInfo *MI = MacroEntry.second;73 74 // We use clang's header guard detection. This has the advantage of also75 // emitting a warning for cases where a pseudo header guard is found but76 // preceded by something blocking the header guard optimization.77 if (!MI->isUsedForHeaderGuard())78 continue;79 80 OptionalFileEntryRef FE =81 SM.getFileEntryRefForID(SM.getFileID(MI->getDefinitionLoc()));82 const std::string FileName = cleanPath(FE->getName());83 Files.erase(FileName);84 85 // See if we should check and fix this header guard.86 if (!Check->shouldFixHeaderGuard(FileName))87 continue;88 89 // Look up Locations for this guard.90 const auto &Locs = Ifndefs[MacroEntry.first.getIdentifierInfo()];91 const SourceLocation Ifndef = Locs.second;92 const SourceLocation Define = MacroEntry.first.getLocation();93 const SourceLocation EndIf = EndIfs[Locs.first];94 95 // If the macro Name is not equal to what we can compute, correct it in96 // the #ifndef and #define.97 const StringRef CurHeaderGuard =98 MacroEntry.first.getIdentifierInfo()->getName();99 std::vector<FixItHint> FixIts;100 const std::string NewGuard = checkHeaderGuardDefinition(101 Ifndef, Define, EndIf, FileName, CurHeaderGuard, FixIts);102 103 // Now look at the #endif. We want a comment with the header guard. Fix it104 // at the slightest deviation.105 checkEndifComment(FileName, EndIf, NewGuard, FixIts);106 107 // Bundle all fix-its into one warning. The message depends on whether we108 // changed the header guard or not.109 if (!FixIts.empty()) {110 if (CurHeaderGuard != NewGuard) {111 Check->diag(Ifndef, "header guard does not follow preferred style")112 << FixIts;113 } else {114 Check->diag(EndIf, "#endif for a header guard should reference the "115 "guard macro in a comment")116 << FixIts;117 }118 }119 }120 121 // Emit warnings for headers that are missing guards.122 checkGuardlessHeaders();123 clearAllState();124 }125 126 bool wouldFixEndifComment(StringRef FileName, SourceLocation EndIf,127 StringRef HeaderGuard,128 size_t *EndIfLenPtr = nullptr) {129 if (!EndIf.isValid())130 return false;131 const char *EndIfData = PP->getSourceManager().getCharacterData(EndIf);132 const size_t EndIfLen = std::strcspn(EndIfData, "\r\n");133 if (EndIfLenPtr)134 *EndIfLenPtr = EndIfLen;135 136 StringRef EndIfStr(EndIfData, EndIfLen);137 EndIfStr = EndIfStr.substr(EndIfStr.find_first_not_of("#endif \t"));138 139 // Give up if there's an escaped newline.140 const size_t FindEscapedNewline = EndIfStr.find_last_not_of(' ');141 if (FindEscapedNewline != StringRef::npos &&142 EndIfStr[FindEscapedNewline] == '\\')143 return false;144 145 const bool IsLineComment =146 EndIfStr.consume_front("//") ||147 (EndIfStr.consume_front("/*") && EndIfStr.consume_back("*/"));148 if (!IsLineComment)149 return Check->shouldSuggestEndifComment(FileName);150 151 return EndIfStr.trim() != HeaderGuard;152 }153 154 /// Look for header guards that don't match the preferred style. Emit155 /// fix-its and return the suggested header guard (or the original if no156 /// change was made.157 std::string checkHeaderGuardDefinition(SourceLocation Ifndef,158 SourceLocation Define,159 SourceLocation EndIf,160 StringRef FileName,161 StringRef CurHeaderGuard,162 std::vector<FixItHint> &FixIts) {163 std::string CPPVar = Check->getHeaderGuard(FileName, CurHeaderGuard);164 CPPVar = Check->sanitizeHeaderGuard(CPPVar);165 const std::string CPPVarUnder = CPPVar + '_';166 167 // Allow a trailing underscore if and only if we don't have to change the168 // endif comment too.169 if (Ifndef.isValid() && CurHeaderGuard != CPPVar &&170 (CurHeaderGuard != CPPVarUnder ||171 wouldFixEndifComment(FileName, EndIf, CurHeaderGuard))) {172 FixIts.push_back(FixItHint::CreateReplacement(173 CharSourceRange::getTokenRange(174 Ifndef, Ifndef.getLocWithOffset(CurHeaderGuard.size())),175 CPPVar));176 FixIts.push_back(FixItHint::CreateReplacement(177 CharSourceRange::getTokenRange(178 Define, Define.getLocWithOffset(CurHeaderGuard.size())),179 CPPVar));180 return CPPVar;181 }182 return std::string(CurHeaderGuard);183 }184 185 /// Checks the comment after the #endif of a header guard and fixes it186 /// if it doesn't match \c HeaderGuard.187 void checkEndifComment(StringRef FileName, SourceLocation EndIf,188 StringRef HeaderGuard,189 std::vector<FixItHint> &FixIts) {190 size_t EndIfLen = 0;191 if (wouldFixEndifComment(FileName, EndIf, HeaderGuard, &EndIfLen)) {192 FixIts.push_back(FixItHint::CreateReplacement(193 CharSourceRange::getCharRange(EndIf,194 EndIf.getLocWithOffset(EndIfLen)),195 Check->formatEndIf(HeaderGuard)));196 }197 }198 199 /// Looks for files that were visited but didn't have a header guard.200 /// Emits a warning with fixits suggesting adding one.201 void checkGuardlessHeaders() {202 // Look for header files that didn't have a header guard. Emit a warning and203 // fix-its to add the guard.204 // TODO: Insert the guard after top comments.205 for (const auto &FE : Files) {206 const StringRef FileName = FE.getKey();207 if (!Check->shouldSuggestToAddHeaderGuard(FileName))208 continue;209 210 const SourceManager &SM = PP->getSourceManager();211 const FileID FID = SM.translateFile(FE.getValue());212 const SourceLocation StartLoc = SM.getLocForStartOfFile(FID);213 if (StartLoc.isInvalid())214 continue;215 216 std::string CPPVar = Check->getHeaderGuard(FileName);217 CPPVar = Check->sanitizeHeaderGuard(CPPVar);218 const std::string CPPVarUnder =219 CPPVar + '_'; // Allow a trailing underscore.220 // If there's a macro with a name that follows the header guard convention221 // but was not recognized by the preprocessor as a header guard there must222 // be code outside of the guarded area. Emit a plain warning without223 // fix-its.224 // FIXME: Can we move it into the right spot?225 bool SeenMacro = false;226 for (const auto &MacroEntry : Macros) {227 const StringRef Name = MacroEntry.first.getIdentifierInfo()->getName();228 const SourceLocation DefineLoc = MacroEntry.first.getLocation();229 if ((Name == CPPVar || Name == CPPVarUnder) &&230 SM.isWrittenInSameFile(StartLoc, DefineLoc)) {231 Check->diag(DefineLoc, "code/includes outside of area guarded by "232 "header guard; consider moving it");233 SeenMacro = true;234 break;235 }236 }237 238 if (SeenMacro)239 continue;240 241 Check->diag(StartLoc, "header is missing header guard")242 << FixItHint::CreateInsertion(243 StartLoc,244 (Twine("#ifndef ") + CPPVar + "\n#define " + CPPVar + "\n\n")245 .str())246 << FixItHint::CreateInsertion(247 SM.getLocForEndOfFile(FID),248 Check->shouldSuggestEndifComment(FileName)249 ? "\n#" + Check->formatEndIf(CPPVar) + "\n"250 : "\n#endif\n");251 }252 }253 254private:255 void clearAllState() {256 Macros.clear();257 Files.clear();258 Ifndefs.clear();259 EndIfs.clear();260 }261 262 std::vector<std::pair<Token, const MacroInfo *>> Macros;263 llvm::StringMap<const FileEntry *> Files;264 std::map<const IdentifierInfo *, std::pair<SourceLocation, SourceLocation>>265 Ifndefs;266 std::map<SourceLocation, SourceLocation> EndIfs;267 268 Preprocessor *PP;269 HeaderGuardCheck *Check;270};271} // namespace272 273void HeaderGuardCheck::registerPPCallbacks(const SourceManager &SM,274 Preprocessor *PP,275 Preprocessor *ModuleExpanderPP) {276 PP->addPPCallbacks(std::make_unique<HeaderGuardPPCallbacks>(PP, this));277}278 279std::string HeaderGuardCheck::sanitizeHeaderGuard(StringRef Guard) {280 // Only reserved identifiers are allowed to start with an '_'.281 return Guard.ltrim('_').str();282}283 284bool HeaderGuardCheck::shouldSuggestEndifComment(StringRef FileName) {285 return utils::isFileExtension(FileName, HeaderFileExtensions);286}287 288bool HeaderGuardCheck::shouldFixHeaderGuard(StringRef FileName) { return true; }289 290bool HeaderGuardCheck::shouldSuggestToAddHeaderGuard(StringRef FileName) {291 return utils::isFileExtension(FileName, HeaderFileExtensions);292}293 294std::string HeaderGuardCheck::formatEndIf(StringRef HeaderGuard) {295 return "endif // " + HeaderGuard.str();296}297} // namespace clang::tidy::utils298