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.
cleanPath(StringRef Path)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:
HeaderGuardPPCallbacks(Preprocessor * PP,HeaderGuardCheck * Check)30   HeaderGuardPPCallbacks(Preprocessor *PP, HeaderGuardCheck *Check)
31       : PP(PP), Check(Check) {}
32 
FileChanged(SourceLocation Loc,FileChangeReason Reason,SrcMgr::CharacteristicKind FileType,FileID PrevFID)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 
Ifndef(SourceLocation Loc,const Token & MacroNameTok,const MacroDefinition & MD)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 
MacroDefined(const Token & MacroNameTok,const MacroDirective * MD)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 
Endif(SourceLocation Loc,SourceLocation IfLoc)64   void Endif(SourceLocation Loc, SourceLocation IfLoc) override {
65     // Record all #endif and the corresponding #ifs (including #ifndefs).
66     EndIfs[IfLoc] = Loc;
67   }
68 
EndOfMainFile()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 
wouldFixEndifComment(StringRef FileName,SourceLocation EndIf,StringRef HeaderGuard,size_t * EndIfLenPtr=nullptr)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     bool IsLineComment =
149         EndIfStr.consume_front("//") ||
150         (EndIfStr.consume_front("/*") && EndIfStr.consume_back("*/"));
151     if (!IsLineComment)
152       return Check->shouldSuggestEndifComment(FileName);
153 
154     return EndIfStr.trim() != HeaderGuard;
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.
checkHeaderGuardDefinition(SourceLocation Ifndef,SourceLocation Define,SourceLocation EndIf,StringRef FileName,StringRef CurHeaderGuard,std::vector<FixItHint> & FixIts)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.
checkEndifComment(StringRef FileName,SourceLocation EndIf,StringRef HeaderGuard,std::vector<FixItHint> & FixIts)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.
checkGuardlessHeaders()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:
clearAllState()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 
storeOptions(ClangTidyOptions::OptionMap & Opts)273 void HeaderGuardCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
274   Options.store(Opts, "HeaderFileExtensions", RawStringHeaderFileExtensions);
275 }
276 
registerPPCallbacks(const SourceManager & SM,Preprocessor * PP,Preprocessor * ModuleExpanderPP)277 void HeaderGuardCheck::registerPPCallbacks(const SourceManager &SM,
278                                            Preprocessor *PP,
279                                            Preprocessor *ModuleExpanderPP) {
280   PP->addPPCallbacks(std::make_unique<HeaderGuardPPCallbacks>(PP, this));
281 }
282 
sanitizeHeaderGuard(StringRef Guard)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 
shouldSuggestEndifComment(StringRef FileName)288 bool HeaderGuardCheck::shouldSuggestEndifComment(StringRef FileName) {
289   return utils::isFileExtension(FileName, HeaderFileExtensions);
290 }
291 
shouldFixHeaderGuard(StringRef FileName)292 bool HeaderGuardCheck::shouldFixHeaderGuard(StringRef FileName) { return true; }
293 
shouldSuggestToAddHeaderGuard(StringRef FileName)294 bool HeaderGuardCheck::shouldSuggestToAddHeaderGuard(StringRef FileName) {
295   return utils::isFileExtension(FileName, HeaderFileExtensions);
296 }
297 
formatEndIf(StringRef HeaderGuard)298 std::string HeaderGuardCheck::formatEndIf(StringRef HeaderGuard) {
299   return "endif // " + HeaderGuard.str();
300 }
301 } // namespace utils
302 } // namespace tidy
303 } // namespace clang
304