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