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