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
22using namespace clang::ast_matchers;
23
24namespace clang::tidy::misc {
25
26namespace {
27
28struct Include {
29 FileID Id;
30 llvm::StringRef Name;
31 SourceLocation Loc;
32};
33
34class CyclicDependencyCallbacks : public PPCallbacks {
35public:
36 CyclicDependencyCallbacks(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
153private:
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
163HeaderIncludeCycleCheck::HeaderIncludeCycleCheck(StringRef Name,
164 ClangTidyContext *Context)
165 : ClangTidyCheck(Name, Context),
166 IgnoredFilesList(utils::options::parseStringList(
167 Option: Options.get(LocalName: "IgnoredFilesList", Default: ""))) {}
168
169void HeaderIncludeCycleCheck::registerPPCallbacks(
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
175void HeaderIncludeCycleCheck::storeOptions(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

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