1 //===--- HeaderIncludeCycleCheck.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 "HeaderIncludeCycleCheck.h" 10 #include "../utils/OptionsUtils.h" 11 #include "clang/AST/ASTContext.h" 12 #include "clang/ASTMatchers/ASTMatchFinder.h" 13 #include "clang/Lex/PPCallbacks.h" 14 #include "clang/Lex/Preprocessor.h" 15 #include "llvm/ADT/SmallVector.h" 16 #include "llvm/Support/Regex.h" 17 #include <algorithm> 18 #include <deque> 19 #include <optional> 20 #include <string> 21 22 using namespace clang::ast_matchers; 23 24 namespace clang::tidy::misc { 25 26 namespace { 27 28 struct Include { 29 FileID Id; 30 llvm::StringRef Name; 31 SourceLocation Loc; 32 }; 33 34 class CyclicDependencyCallbacks : public PPCallbacks { 35 public: 36 CyclicDependencyCallbacks(HeaderIncludeCycleCheck &Check, 37 const SourceManager &SM, 38 const std::vector<StringRef> &IgnoredFilesList) 39 : Check(Check), SM(SM) { 40 IgnoredFilesRegexes.reserve(IgnoredFilesList.size()); 41 for (const StringRef &It : IgnoredFilesList) { 42 if (!It.empty()) 43 IgnoredFilesRegexes.emplace_back(It); 44 } 45 } 46 47 void FileChanged(SourceLocation Loc, FileChangeReason Reason, 48 SrcMgr::CharacteristicKind FileType, 49 FileID PrevFID) override { 50 if (FileType != clang::SrcMgr::C_User) 51 return; 52 53 if (Reason != EnterFile && Reason != ExitFile) 54 return; 55 56 FileID Id = SM.getFileID(Loc); 57 if (Id.isInvalid()) 58 return; 59 60 if (Reason == ExitFile) { 61 if ((Files.size() > 1U) && (Files.back().Id == PrevFID) && 62 (Files[Files.size() - 2U].Id == Id)) 63 Files.pop_back(); 64 return; 65 } 66 67 if (!Files.empty() && Files.back().Id == Id) 68 return; 69 70 std::optional<llvm::StringRef> FilePath = SM.getNonBuiltinFilenameForID(Id); 71 llvm::StringRef FileName = 72 FilePath ? llvm::sys::path::filename(*FilePath) : llvm::StringRef(); 73 74 if (!NextToEnter) 75 NextToEnter = Include{Id, FileName, SourceLocation()}; 76 77 assert(NextToEnter->Name == FileName); 78 NextToEnter->Id = Id; 79 Files.emplace_back(*NextToEnter); 80 NextToEnter.reset(); 81 } 82 83 void InclusionDirective(SourceLocation, const Token &, StringRef FilePath, 84 bool, CharSourceRange Range, 85 OptionalFileEntryRef File, StringRef, StringRef, 86 const Module *, 87 SrcMgr::CharacteristicKind FileType) override { 88 if (FileType != clang::SrcMgr::C_User) 89 return; 90 91 llvm::StringRef FileName = llvm::sys::path::filename(FilePath); 92 NextToEnter = {FileID(), FileName, Range.getBegin()}; 93 94 if (!File) 95 return; 96 97 FileID Id = SM.translateFile(*File); 98 if (Id.isInvalid()) 99 return; 100 101 checkForDoubleInclude(Id, FileName, Range.getBegin()); 102 } 103 104 void EndOfMainFile() override { 105 if (!Files.empty() && Files.back().Id == SM.getMainFileID()) 106 Files.pop_back(); 107 108 assert(Files.empty()); 109 } 110 111 void checkForDoubleInclude(FileID Id, llvm::StringRef FileName, 112 SourceLocation Loc) { 113 auto It = 114 std::find_if(Files.rbegin(), Files.rend(), 115 [&](const Include &Entry) { return Entry.Id == Id; }); 116 if (It == Files.rend()) 117 return; 118 119 const std::optional<StringRef> FilePath = SM.getNonBuiltinFilenameForID(Id); 120 if (!FilePath || isFileIgnored(*FilePath)) 121 return; 122 123 if (It == Files.rbegin()) { 124 Check.diag(Loc, "direct self-inclusion of header file '%0'") << FileName; 125 return; 126 } 127 128 Check.diag(Loc, "circular header file dependency detected while including " 129 "'%0', please check the include path") 130 << FileName; 131 132 const bool IsIncludePathValid = 133 std::all_of(Files.rbegin(), It, [](const Include &Elem) { 134 return !Elem.Name.empty() && Elem.Loc.isValid(); 135 }); 136 137 if (!IsIncludePathValid) 138 return; 139 140 auto CurrentIt = Files.rbegin(); 141 do { 142 Check.diag(CurrentIt->Loc, "'%0' included from here", DiagnosticIDs::Note) 143 << CurrentIt->Name; 144 } while (CurrentIt++ != It); 145 } 146 147 bool isFileIgnored(StringRef FileName) const { 148 return llvm::any_of(IgnoredFilesRegexes, [&](const llvm::Regex &It) { 149 return It.match(FileName); 150 }); 151 } 152 153 private: 154 std::deque<Include> Files; 155 std::optional<Include> NextToEnter; 156 HeaderIncludeCycleCheck &Check; 157 const SourceManager &SM; 158 std::vector<llvm::Regex> IgnoredFilesRegexes; 159 }; 160 161 } // namespace 162 163 HeaderIncludeCycleCheck::HeaderIncludeCycleCheck(StringRef Name, 164 ClangTidyContext *Context) 165 : ClangTidyCheck(Name, Context), 166 IgnoredFilesList(utils::options::parseStringList( 167 Options.get("IgnoredFilesList", ""))) {} 168 169 void HeaderIncludeCycleCheck::registerPPCallbacks( 170 const SourceManager &SM, Preprocessor *PP, Preprocessor *ModuleExpanderPP) { 171 PP->addPPCallbacks( 172 std::make_unique<CyclicDependencyCallbacks>(*this, SM, IgnoredFilesList)); 173 } 174 175 void HeaderIncludeCycleCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) { 176 Options.store(Opts, "IgnoredFilesList", 177 utils::options::serializeStringList(IgnoredFilesList)); 178 } 179 180 } // namespace clang::tidy::misc 181