xref: /llvm-project/clang-tools-extra/clang-tidy/misc/HeaderIncludeCycleCheck.cpp (revision c5ff983fe4a3180e13c7244a6ce9f5994b4379b4)
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:
CyclicDependencyCallbacks(HeaderIncludeCycleCheck & Check,const SourceManager & SM,const std::vector<StringRef> & IgnoredFilesList)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 
FileChanged(SourceLocation Loc,FileChangeReason Reason,SrcMgr::CharacteristicKind FileType,FileID PrevFID)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 
InclusionDirective(SourceLocation,const Token &,StringRef FilePath,bool,CharSourceRange Range,OptionalFileEntryRef File,StringRef,StringRef,const Module *,bool,SrcMgr::CharacteristicKind FileType)83   void InclusionDirective(SourceLocation, const Token &, StringRef FilePath,
84                           bool, CharSourceRange Range,
85                           OptionalFileEntryRef File, StringRef, StringRef,
86                           const Module *, bool,
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 
EndOfMainFile()104   void EndOfMainFile() override {
105     if (!Files.empty() && Files.back().Id == SM.getMainFileID())
106       Files.pop_back();
107 
108     assert(Files.empty());
109   }
110 
checkForDoubleInclude(FileID Id,llvm::StringRef FileName,SourceLocation Loc)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 + 1, [](const Include &Elem) {
134           return !Elem.Name.empty() && Elem.Loc.isValid();
135         });
136     if (!IsIncludePathValid)
137       return;
138 
139     for (const Include &I : llvm::make_range(Files.rbegin(), It + 1))
140       Check.diag(I.Loc, "'%0' included from here", DiagnosticIDs::Note)
141           << I.Name;
142   }
143 
isFileIgnored(StringRef FileName) const144   bool isFileIgnored(StringRef FileName) const {
145     return llvm::any_of(IgnoredFilesRegexes, [&](const llvm::Regex &It) {
146       return It.match(FileName);
147     });
148   }
149 
150 private:
151   std::deque<Include> Files;
152   std::optional<Include> NextToEnter;
153   HeaderIncludeCycleCheck &Check;
154   const SourceManager &SM;
155   std::vector<llvm::Regex> IgnoredFilesRegexes;
156 };
157 
158 } // namespace
159 
HeaderIncludeCycleCheck(StringRef Name,ClangTidyContext * Context)160 HeaderIncludeCycleCheck::HeaderIncludeCycleCheck(StringRef Name,
161                                                  ClangTidyContext *Context)
162     : ClangTidyCheck(Name, Context),
163       IgnoredFilesList(utils::options::parseStringList(
164           Options.get("IgnoredFilesList", ""))) {}
165 
registerPPCallbacks(const SourceManager & SM,Preprocessor * PP,Preprocessor * ModuleExpanderPP)166 void HeaderIncludeCycleCheck::registerPPCallbacks(
167     const SourceManager &SM, Preprocessor *PP, Preprocessor *ModuleExpanderPP) {
168   PP->addPPCallbacks(
169       std::make_unique<CyclicDependencyCallbacks>(*this, SM, IgnoredFilesList));
170 }
171 
storeOptions(ClangTidyOptions::OptionMap & Opts)172 void HeaderIncludeCycleCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
173   Options.store(Opts, "IgnoredFilesList",
174                 utils::options::serializeStringList(IgnoredFilesList));
175 }
176 
177 } // namespace clang::tidy::misc
178