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
16namespace clang::tidy::utils {
17
18/// canonicalize a path by removing ./ and ../ components.
19static std::string cleanPath(StringRef Path) {
20 SmallString<256> Result = Path;
21 llvm::sys::path::remove_dots(path&: Result, remove_dot_dot: true);
22 return std::string(Result);
23}
24
25namespace {
26class HeaderGuardPPCallbacks : public PPCallbacks {
27public:
28 HeaderGuardPPCallbacks(Preprocessor *PP, HeaderGuardCheck *Check)
29 : PP(PP), Check(Check) {}
30
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(FID: SM.getFileID(SpellingLoc: Loc))) {
40 std::string FileName = cleanPath(Path: FE->getName());
41 Files[FileName] = *FE;
42 }
43 }
44 }
45
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(x&: Loc, y: MacroNameTok.getLocation());
54 }
55
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(args: MacroNameTok, args: MD->getMacroInfo());
61 }
62
63 void Endif(SourceLocation Loc, SourceLocation IfLoc) override {
64 // Record all #endif and the corresponding #ifs (including #ifndefs).
65 EndIfs[IfLoc] = Loc;
66 }
67
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(FID: SM.getFileID(SpellingLoc: MI->getDefinitionLoc()));
83 std::string FileName = cleanPath(Path: FE->getName());
84 Files.erase(Key: FileName);
85
86 // See if we should check and fix this header guard.
87 if (!Check->shouldFixHeaderGuard(Filename: 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, HeaderGuard: 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(Loc: Ifndef, Description: "header guard does not follow preferred style")
114 << FixIts;
115 } else {
116 Check->diag(Loc: EndIf, Description: "#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
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(SL: EndIf);
134 size_t EndIfLen = std::strcspn(s: EndIfData, reject: "\r\n");
135 if (EndIfLenPtr)
136 *EndIfLenPtr = EndIfLen;
137
138 StringRef EndIfStr(EndIfData, EndIfLen);
139 EndIfStr = EndIfStr.substr(Start: EndIfStr.find_first_not_of(Chars: "#endif \t"));
140
141 // Give up if there's an escaped newline.
142 size_t FindEscapedNewline = EndIfStr.find_last_not_of(C: ' ');
143 if (FindEscapedNewline != StringRef::npos &&
144 EndIfStr[FindEscapedNewline] == '\\')
145 return false;
146
147 bool IsLineComment =
148 EndIfStr.consume_front(Prefix: "//") ||
149 (EndIfStr.consume_front(Prefix: "/*") && EndIfStr.consume_back(Suffix: "*/"));
150 if (!IsLineComment)
151 return Check->shouldSuggestEndifComment(Filename: 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.
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: FileName, OldGuard: CurHeaderGuard);
166 CPPVar = Check->sanitizeHeaderGuard(Guard: 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, HeaderGuard: CurHeaderGuard))) {
174 FixIts.push_back(x: FixItHint::CreateReplacement(
175 RemoveRange: CharSourceRange::getTokenRange(
176 B: Ifndef, E: Ifndef.getLocWithOffset(Offset: CurHeaderGuard.size())),
177 Code: CPPVar));
178 FixIts.push_back(x: FixItHint::CreateReplacement(
179 RemoveRange: CharSourceRange::getTokenRange(
180 B: Define, E: Define.getLocWithOffset(Offset: CurHeaderGuard.size())),
181 Code: 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.
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, EndIfLenPtr: &EndIfLen)) {
194 FixIts.push_back(x: FixItHint::CreateReplacement(
195 RemoveRange: CharSourceRange::getCharRange(B: EndIf,
196 E: EndIf.getLocWithOffset(Offset: EndIfLen)),
197 Code: 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.
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: FileName))
210 continue;
211
212 SourceManager &SM = PP->getSourceManager();
213 FileID FID = SM.translateFile(SourceFile: FE.getValue());
214 SourceLocation StartLoc = SM.getLocForStartOfFile(FID);
215 if (StartLoc.isInvalid())
216 continue;
217
218 std::string CPPVar = Check->getHeaderGuard(Filename: FileName);
219 CPPVar = Check->sanitizeHeaderGuard(Guard: 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(Loc1: StartLoc, Loc2: DefineLoc)) {
232 Check->diag(Loc: DefineLoc, Description: "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(Loc: StartLoc, Description: "header is missing header guard")
243 << FixItHint::CreateInsertion(
244 InsertionLoc: StartLoc, Code: "#ifndef " + CPPVar + "\n#define " + CPPVar + "\n\n")
245 << FixItHint::CreateInsertion(
246 InsertionLoc: SM.getLocForEndOfFile(FID),
247 Code: Check->shouldSuggestEndifComment(Filename: FileName)
248 ? "\n#" + Check->formatEndIf(HeaderGuard: CPPVar) + "\n"
249 : "\n#endif\n");
250 }
251 }
252
253private:
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
272void HeaderGuardCheck::registerPPCallbacks(const SourceManager &SM,
273 Preprocessor *PP,
274 Preprocessor *ModuleExpanderPP) {
275 PP->addPPCallbacks(C: std::make_unique<HeaderGuardPPCallbacks>(args&: PP, args: this));
276}
277
278std::string HeaderGuardCheck::sanitizeHeaderGuard(StringRef Guard) {
279 // Only reserved identifiers are allowed to start with an '_'.
280 return Guard.ltrim(Char: '_').str();
281}
282
283bool HeaderGuardCheck::shouldSuggestEndifComment(StringRef FileName) {
284 return utils::isFileExtension(FileName, FileExtensions: HeaderFileExtensions);
285}
286
287bool HeaderGuardCheck::shouldFixHeaderGuard(StringRef FileName) { return true; }
288
289bool HeaderGuardCheck::shouldSuggestToAddHeaderGuard(StringRef FileName) {
290 return utils::isFileExtension(FileName, FileExtensions: HeaderFileExtensions);
291}
292
293std::string HeaderGuardCheck::formatEndIf(StringRef HeaderGuard) {
294 return "endif // " + HeaderGuard.str();
295}
296} // namespace clang::tidy::utils
297

source code of clang-tools-extra/clang-tidy/utils/HeaderGuard.cpp