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