1 #include "HeaderGuardCheck.h"
2
3 #include <unordered_set>
4
5 #include <clang/Frontend/CompilerInstance.h>
6
7 #if !defined(_MSC_VER)
8 #include <sys/stat.h>
9 #include <unistd.h>
10 #endif
11
12 namespace clang
13 {
14 namespace tidy
15 {
16 namespace cata
17 {
18
CataHeaderGuardCheck(StringRef Name,ClangTidyContext * Context)19 CataHeaderGuardCheck::CataHeaderGuardCheck( StringRef Name,
20 ClangTidyContext *Context )
21 : ClangTidyCheck( Name, Context ) {}
22
23 /// \brief canonicalize a path by removing ./ and ../ components.
cleanPath(StringRef Path)24 static std::string cleanPath( StringRef Path )
25 {
26 SmallString<256> Result = Path;
27 llvm::sys::path::remove_dots( Result, true );
28 return Result.str();
29 }
30
pathExists(const std::string & path)31 static bool pathExists( const std::string &path )
32 {
33 struct stat buffer;
34 return ( stat( path.c_str(), &buffer ) == 0 );
35 }
36
isHeaderFileName(StringRef FileName)37 static bool isHeaderFileName( StringRef FileName )
38 {
39 return FileName.contains( ".h" );
40 }
41
getHeaderGuard(StringRef Filename)42 static std::string getHeaderGuard( StringRef Filename )
43 {
44 std::string Guard = tooling::getAbsolutePath( Filename );
45
46 // Look for the top-level directory
47 std::string TopDir = Guard;
48 size_t LastSlash;
49 bool Found = false;
50 while( std::string::npos != ( LastSlash = TopDir.find_last_of( "/\\" ) ) ) {
51 TopDir = TopDir.substr( 0, LastSlash );
52 // Either the root source dir (containing .travis.yml) or the root build
53 // dir (containing CMakeCache.txt)
54 if( pathExists( TopDir + "/.travis.yml" ) || pathExists( TopDir + "/CMakeCache.txt" ) ) {
55 Found = true;
56 break;
57 }
58 }
59
60 if( !Found ) {
61 return {};
62 }
63
64 Guard = Guard.substr( TopDir.length() + 1 );
65
66 std::replace( Guard.begin(), Guard.end(), '/', '_' );
67 std::replace( Guard.begin(), Guard.end(), '\\', '_' );
68 std::replace( Guard.begin(), Guard.end(), '.', '_' );
69 std::replace( Guard.begin(), Guard.end(), '-', '_' );
70
71 Guard = "CATA_" + Guard;
72
73 return StringRef( Guard ).upper();
74 }
75
formatEndIf(StringRef HeaderGuard)76 static std::string formatEndIf( StringRef HeaderGuard )
77 {
78 return "endif // " + HeaderGuard.str();
79 }
80
81 struct MacroInfo_ {
82 Token Tok;
83 const MacroInfo *Info;
84 };
85
86 struct IfndefInfo {
87 const IdentifierInfo *MacroId;
88 SourceLocation Loc;
89 SourceLocation MacroNameLoc;
90 };
91
92 struct FileInfo {
93 const FileEntry *Entry;
94 std::vector<MacroInfo_> Macros;
95 std::vector<IfndefInfo> Ifndefs;
96 std::map<SourceLocation, SourceLocation> EndIfs;
97 SourceLocation PragmaOnce;
98 };
99
100 class HeaderGuardPPCallbacks : public PPCallbacks
101 {
102 public:
HeaderGuardPPCallbacks(Preprocessor * PP,CataHeaderGuardCheck * Check)103 HeaderGuardPPCallbacks( Preprocessor *PP, CataHeaderGuardCheck *Check )
104 : PP( PP ), Check( Check ) {}
105
GetFileName(SourceLocation Loc)106 std::string GetFileName( SourceLocation Loc ) {
107 SourceManager &SM = PP->getSourceManager();
108 FileID Id = SM.getFileID( Loc );
109 if( const FileEntry *Entry = SM.getFileEntryForID( Id ) ) {
110 return cleanPath( Entry->getName() );
111 } else {
112 return {};
113 }
114 }
115
FileChanged(SourceLocation Loc,FileChangeReason Reason,SrcMgr::CharacteristicKind FileType,FileID)116 void FileChanged( SourceLocation Loc, FileChangeReason Reason,
117 SrcMgr::CharacteristicKind FileType,
118 FileID ) override {
119 if( Reason == EnterFile && FileType == SrcMgr::C_User ) {
120 std::string FileName = GetFileName( Loc );
121 Files.insert( FileName );
122 SourceManager &SM = PP->getSourceManager();
123 FileID Id = SM.getFileID( Loc );
124 FileInfos[FileName].Entry = SM.getFileEntryForID( Id );
125 }
126 }
127
Ifndef(SourceLocation Loc,const Token & MacroNameTok,const MacroDefinition & MD)128 void Ifndef( SourceLocation Loc, const Token &MacroNameTok,
129 const MacroDefinition &MD ) override {
130 if( MD ) {
131 return;
132 }
133
134 // Record #ifndefs that succeeded. We also need the Location of the Name.
135 FileInfos[GetFileName( Loc )].Ifndefs.push_back(
136 IfndefInfo{ MacroNameTok.getIdentifierInfo(), Loc, MacroNameTok.getLocation() } );
137 }
138
MacroDefined(const Token & MacroNameTok,const MacroDirective * MD)139 void MacroDefined( const Token &MacroNameTok,
140 const MacroDirective *MD ) override {
141 // Record all defined macros. We store the whole token to get info on the
142 // name later.
143 SourceLocation Loc = MD->getLocation();
144 FileInfos[GetFileName( Loc )].Macros.push_back(
145 MacroInfo_{ MacroNameTok, MD->getMacroInfo() } );
146 }
147
Endif(SourceLocation Loc,SourceLocation IfLoc)148 void Endif( SourceLocation Loc, SourceLocation IfLoc ) override {
149 // Record all #endif and the corresponding #ifs (including #ifndefs).
150 FileInfos[GetFileName( Loc )].EndIfs[IfLoc] = Loc;
151 }
152
PragmaDirective(SourceLocation Loc,PragmaIntroducerKind)153 void PragmaDirective( SourceLocation Loc, PragmaIntroducerKind ) override {
154 const char *PragmaData = PP->getSourceManager().getCharacterData( Loc );
155 static constexpr const char *PragmaOnce = "#pragma once";
156 if( 0 == strncmp( PragmaData, PragmaOnce, strlen( PragmaOnce ) ) ) {
157 FileInfos[GetFileName( Loc )].PragmaOnce = Loc;
158 }
159 }
160
EndOfMainFile()161 void EndOfMainFile() override {
162 // Now that we have all this information from the preprocessor, use it!
163 std::unordered_set<std::string> GuardlessHeaders = Files;
164
165 for( const std::string &FileName : Files ) {
166 if( !isHeaderFileName( FileName ) ) {
167 continue;
168 }
169
170 const FileInfo &Info = FileInfos[FileName];
171
172 if( Info.Macros.empty() || Info.Ifndefs.empty() || Info.EndIfs.empty() ) {
173 continue;
174 }
175
176 const IfndefInfo &Ifndef = Info.Ifndefs.front();
177 const MacroInfo_ &Macro = Info.Macros.front();
178 StringRef CurHeaderGuard = Macro.Tok.getIdentifierInfo()->getName();
179
180 if( Ifndef.MacroId->getName() != CurHeaderGuard ) {
181 // If the #ifndef and #define don't match, then it's
182 // probably not a header guard we're looking at. Abort.
183 continue;
184 }
185
186 // Look up Locations for this guard.
187 SourceLocation DefineLoc = Macro.Tok.getLocation();
188 auto EndifIt = Info.EndIfs.find( Ifndef.Loc );
189
190 if( EndifIt == Info.EndIfs.end() ) {
191 continue;
192 }
193
194 SourceLocation EndIfLoc = EndifIt->second;
195
196 GuardlessHeaders.erase( FileName );
197
198 // If the macro Name is not equal to what we can compute, correct it in
199 // the #ifndef and #define.
200 std::vector<FixItHint> FixIts;
201 std::string NewGuard =
202 checkHeaderGuardDefinition( Ifndef.MacroNameLoc, DefineLoc, FileName,
203 CurHeaderGuard, FixIts );
204
205 // Now look at the #endif. We want a comment with the header guard. Fix it
206 // at the slightest deviation.
207 checkEndifComment( EndIfLoc, NewGuard, FixIts );
208
209 // Bundle all fix-its into one warning. The message depends on whether we
210 // changed the header guard or not.
211 if( !FixIts.empty() ) {
212 if( CurHeaderGuard != NewGuard ) {
213 Check->diag( Ifndef.Loc, "Header guard does not follow preferred style." )
214 << FixIts;
215 } else {
216 Check->diag( EndIfLoc, "#endif for a header guard should reference the "
217 "guard macro in a comment." )
218 << FixIts;
219 }
220 }
221 }
222
223 // Emit warnings for headers that are missing guards.
224 checkGuardlessHeaders( GuardlessHeaders );
225
226 // Clear all state.
227 Files.clear();
228 FileInfos.clear();
229 }
230
wouldFixEndifComment(SourceLocation EndIf,StringRef HeaderGuard,size_t * EndIfLenPtr=nullptr)231 bool wouldFixEndifComment( SourceLocation EndIf, StringRef HeaderGuard,
232 size_t *EndIfLenPtr = nullptr ) {
233 if( !EndIf.isValid() ) {
234 return false;
235 }
236 const char *EndIfData = PP->getSourceManager().getCharacterData( EndIf );
237 // NOLINTNEXTLINE(cata-text-style)
238 size_t EndIfLen = std::strcspn( EndIfData, "\r\n" );
239 if( EndIfLenPtr ) {
240 *EndIfLenPtr = EndIfLen;
241 }
242
243 StringRef EndIfStr( EndIfData, EndIfLen );
244 // NOLINTNEXTLINE(cata-text-style)
245 EndIfStr = EndIfStr.substr( EndIfStr.find_first_not_of( "#endif \t" ) );
246
247 // Give up if there's an escaped newline.
248 size_t FindEscapedNewline = EndIfStr.find_last_not_of( ' ' );
249 if( FindEscapedNewline != StringRef::npos &&
250 EndIfStr[FindEscapedNewline] == '\\' ) {
251 return false;
252 }
253
254 return EndIfStr != "// " + HeaderGuard.str();
255 }
256
257 /// \brief Look for header guards that don't match the preferred style. Emit
258 /// fix-its and return the suggested header guard (or the original if no
259 /// change was made.
checkHeaderGuardDefinition(SourceLocation Ifndef,SourceLocation Define,StringRef FileName,StringRef CurHeaderGuard,std::vector<FixItHint> & FixIts)260 static std::string checkHeaderGuardDefinition( SourceLocation Ifndef,
261 SourceLocation Define,
262 StringRef FileName,
263 StringRef CurHeaderGuard,
264 std::vector<FixItHint> &FixIts ) {
265 std::string CPPVar = getHeaderGuard( FileName );
266
267 if( CPPVar.empty() ) {
268 return CurHeaderGuard;
269 }
270
271 if( Ifndef.isValid() && CurHeaderGuard != CPPVar ) {
272 FixIts.push_back( FixItHint::CreateReplacement(
273 CharSourceRange::getTokenRange(
274 Ifndef, Ifndef.getLocWithOffset( CurHeaderGuard.size() ) ),
275 CPPVar ) );
276 FixIts.push_back( FixItHint::CreateReplacement(
277 CharSourceRange::getTokenRange(
278 Define, Define.getLocWithOffset( CurHeaderGuard.size() ) ),
279 CPPVar ) );
280 return CPPVar;
281 }
282 return CurHeaderGuard;
283 }
284
285 /// \brief Checks the comment after the #endif of a header guard and fixes it
286 /// if it doesn't match \c HeaderGuard.
checkEndifComment(SourceLocation EndIf,StringRef HeaderGuard,std::vector<FixItHint> & FixIts)287 void checkEndifComment( SourceLocation EndIf, StringRef HeaderGuard,
288 std::vector<FixItHint> &FixIts ) {
289 size_t EndIfLen;
290 if( wouldFixEndifComment( EndIf, HeaderGuard, &EndIfLen ) ) {
291 FixIts.push_back( FixItHint::CreateReplacement(
292 CharSourceRange::getCharRange( EndIf,
293 EndIf.getLocWithOffset( EndIfLen ) ),
294 formatEndIf( HeaderGuard ) ) );
295 }
296 }
297
298 /// \brief Looks for files that were visited but didn't have a header guard.
299 /// Emits a warning with fixits suggesting adding one.
checkGuardlessHeaders(std::unordered_set<std::string> GuardlessHeaders)300 void checkGuardlessHeaders( std::unordered_set<std::string> GuardlessHeaders ) {
301 // Look for header files that didn't have a header guard. Emit a warning and
302 // fix-its to add the guard.
303 for( const std::string &FileName : GuardlessHeaders ) {
304 if( !isHeaderFileName( FileName ) ) {
305 continue;
306 }
307
308 SourceManager &SM = PP->getSourceManager();
309 const FileInfo &Info = FileInfos.at( FileName );
310 const FileEntry *FE = Info.Entry;
311 if( !FE ) {
312 fprintf( stderr, "No FileEntry for %s\n", FileName.c_str() );
313 continue;
314 }
315 FileID FID = SM.translateFile( FE );
316 SourceLocation StartLoc = SM.getLocForStartOfFile( FID );
317 if( StartLoc.isInvalid() ) {
318 continue;
319 }
320
321 std::string CPPVar = getHeaderGuard( FileName );
322 if( CPPVar.empty() ) {
323 continue;
324 }
325 // If there's a macro with a name that follows the header guard convention
326 // but was not recognized by the preprocessor as a header guard there must
327 // be code outside of the guarded area. Emit a plain warning without
328 // fix-its.
329 bool SeenMacro = false;
330 for( const auto &MacroEntry : Info.Macros ) {
331 StringRef Name = MacroEntry.Tok.getIdentifierInfo()->getName();
332 SourceLocation DefineLoc = MacroEntry.Tok.getLocation();
333 if( Name == CPPVar &&
334 SM.isWrittenInSameFile( StartLoc, DefineLoc ) ) {
335 Check->diag( DefineLoc, "Code/includes outside of area guarded by "
336 "header guard; consider moving it." );
337 SeenMacro = true;
338 break;
339 }
340 }
341
342 if( SeenMacro ) {
343 continue;
344 }
345
346 SourceLocation InsertLoc = StartLoc;
347
348 if( Info.PragmaOnce.isValid() ) {
349 const char *PragmaData =
350 PP->getSourceManager().getCharacterData( Info.PragmaOnce );
351 const char *Newline = strchr( PragmaData, '\n' );
352 if( Newline ) {
353 InsertLoc = Info.PragmaOnce.getLocWithOffset( Newline + 1 - PragmaData );
354 }
355 }
356
357 const char *InsertData = PP->getSourceManager().getCharacterData( InsertLoc );
358 const char *Newlines = "\n\n";
359 if( InsertData[0] == '\n' || InsertData[0] == '\r' ) {
360 Newlines = "\n";
361 }
362
363 Check->diag( InsertLoc, "Header is missing header guard." )
364 << FixItHint::CreateInsertion(
365 InsertLoc, "#ifndef " + CPPVar + "\n#define " + CPPVar + Newlines )
366 << FixItHint::CreateInsertion(
367 SM.getLocForEndOfFile( FID ),
368 "\n#" + formatEndIf( CPPVar ) + "\n" );
369 }
370 }
371 private:
372 std::unordered_set<std::string> Files;
373 std::unordered_map<std::string, FileInfo> FileInfos;
374
375 Preprocessor *PP;
376 CataHeaderGuardCheck *Check;
377 };
378
registerPPCallbacks(CompilerInstance & Compiler)379 void CataHeaderGuardCheck::registerPPCallbacks( CompilerInstance &Compiler )
380 {
381 Compiler.getPreprocessor().addPPCallbacks(
382 llvm::make_unique<HeaderGuardPPCallbacks>( &Compiler.getPreprocessor(),
383 this ) );
384 }
385
386 } // namespace cata
387 } // namespace tidy
388 } // namespace clang
389