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 | (HeaderIncludeCycleCheck &Check, |
37 | const SourceManager &SM, |
38 | const std::vector<StringRef> &IgnoredFilesList) |
39 | : Check(Check), SM(SM) { |
40 | IgnoredFilesRegexes.reserve(n: IgnoredFilesList.size()); |
41 | for (const StringRef &It : IgnoredFilesList) { |
42 | if (!It.empty()) |
43 | IgnoredFilesRegexes.emplace_back(args: 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(SpellingLoc: 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(FID: Id); |
71 | llvm::StringRef FileName = |
72 | FilePath ? llvm::sys::path::filename(path: *FilePath) : llvm::StringRef(); |
73 | |
74 | if (!NextToEnter) |
75 | NextToEnter = Include{.Id: Id, .Name: FileName, .Loc: SourceLocation()}; |
76 | |
77 | assert(NextToEnter->Name == FileName); |
78 | NextToEnter->Id = Id; |
79 | Files.emplace_back(args&: *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 *, bool, |
87 | SrcMgr::CharacteristicKind FileType) override { |
88 | if (FileType != clang::SrcMgr::C_User) |
89 | return; |
90 | |
91 | llvm::StringRef FileName = llvm::sys::path::filename(path: FilePath); |
92 | NextToEnter = {.Id: FileID(), .Name: FileName, .Loc: Range.getBegin()}; |
93 | |
94 | if (!File) |
95 | return; |
96 | |
97 | FileID Id = SM.translateFile(SourceFile: *File); |
98 | if (Id.isInvalid()) |
99 | return; |
100 | |
101 | checkForDoubleInclude(Id, FileName, Loc: 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(first: Files.rbegin(), last: Files.rend(), |
115 | pred: [&](const Include &Entry) { return Entry.Id == Id; }); |
116 | if (It == Files.rend()) |
117 | return; |
118 | |
119 | const std::optional<StringRef> FilePath = SM.getNonBuiltinFilenameForID(FID: Id); |
120 | if (!FilePath || isFileIgnored(FileName: *FilePath)) |
121 | return; |
122 | |
123 | if (It == Files.rbegin()) { |
124 | Check.diag(Loc, Description: "direct self-inclusion of header file '%0'" ) << FileName; |
125 | return; |
126 | } |
127 | |
128 | Check.diag(Loc, Description: "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(first: Files.rbegin(), last: It, pred: [](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(Loc: CurrentIt->Loc, Description: "'%0' included from here" , Level: DiagnosticIDs::Note) |
143 | << CurrentIt->Name; |
144 | } while (CurrentIt++ != It); |
145 | } |
146 | |
147 | bool isFileIgnored(StringRef FileName) const { |
148 | return llvm::any_of(Range: IgnoredFilesRegexes, P: [&](const llvm::Regex &It) { |
149 | return It.match(String: 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::(StringRef Name, |
164 | ClangTidyContext *Context) |
165 | : ClangTidyCheck(Name, Context), |
166 | IgnoredFilesList(utils::options::parseStringList( |
167 | Option: Options.get(LocalName: "IgnoredFilesList" , Default: "" ))) {} |
168 | |
169 | void HeaderIncludeCycleCheck::( |
170 | const SourceManager &SM, Preprocessor *PP, Preprocessor *ModuleExpanderPP) { |
171 | PP->addPPCallbacks( |
172 | C: std::make_unique<CyclicDependencyCallbacks>(args&: *this, args: SM, args: IgnoredFilesList)); |
173 | } |
174 | |
175 | void HeaderIncludeCycleCheck::(ClangTidyOptions::OptionMap &Opts) { |
176 | Options.store(Options&: Opts, LocalName: "IgnoredFilesList" , |
177 | Value: utils::options::serializeStringList(Strings: IgnoredFilesList)); |
178 | } |
179 | |
180 | } // namespace clang::tidy::misc |
181 | |