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
19using namespace clang::ast_matchers;
20
21namespace clang::tidy::misc {
22
23namespace {
24
25struct Include {
26 FileID Id;
27 llvm::StringRef Name;
28 SourceLocation Loc;
29};
30
31class CyclicDependencyCallbacks : public PPCallbacks {
32public:
33 CyclicDependencyCallbacks(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
147private:
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
157HeaderIncludeCycleCheck::HeaderIncludeCycleCheck(StringRef Name,
158 ClangTidyContext *Context)
159 : ClangTidyCheck(Name, Context),
160 IgnoredFilesList(utils::options::parseStringList(
161 Option: Options.get(LocalName: "IgnoredFilesList", Default: ""))) {}
162
163void HeaderIncludeCycleCheck::registerPPCallbacks(
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
169void HeaderIncludeCycleCheck::storeOptions(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

source code of clang-tools-extra/clang-tidy/misc/HeaderIncludeCycleCheck.cpp