xref: /llvm-project/clang-tools-extra/clang-tidy/misc/HeaderIncludeCycleCheck.cpp (revision 7f6e0052a97f13a5f595f3fd0c135c2c4db119d4)
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