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