clang-tools 22.0.0git
HeaderIncludeCycleCheck.cpp
Go to the documentation of this file.
1//===----------------------------------------------------------------------===//
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
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 <optional>
17#include <vector>
18
19using namespace clang::ast_matchers;
20
21namespace clang::tidy::misc {
22
23namespace {
24
25struct Include {
26 const FileEntry *File;
27 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(IgnoredFilesList.size());
38 for (const StringRef &It : IgnoredFilesList) {
39 if (!It.empty())
40 IgnoredFilesRegexes.emplace_back(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 const FileID Id = SM.getFileID(Loc);
54 if (Id.isInvalid())
55 return;
56
57 const FileEntry *NewFile = SM.getFileEntryForID(Id);
58 const FileEntry *PrevFile = SM.getFileEntryForID(PrevFID);
59
60 if (Reason == ExitFile) {
61 if ((Files.size() > 1U) && (Files.back().File == PrevFile) &&
62 (Files[Files.size() - 2U].File == NewFile))
63 Files.pop_back();
64 return;
65 }
66
67 if (!Files.empty() && Files.back().File == NewFile)
68 return;
69
70 const std::optional<StringRef> FilePath = SM.getNonBuiltinFilenameForID(Id);
71 const StringRef FileName =
72 FilePath ? llvm::sys::path::filename(*FilePath) : StringRef();
73 Files.push_back({NewFile, FileName, std::exchange(NextToEnter, {})});
74 }
75
76 void InclusionDirective(SourceLocation, const Token &, StringRef FilePath,
77 bool, CharSourceRange Range,
78 OptionalFileEntryRef File, StringRef, StringRef,
79 const Module *, bool,
80 SrcMgr::CharacteristicKind FileType) override {
81 if (FileType != clang::SrcMgr::C_User)
82 return;
83
84 NextToEnter = Range.getBegin();
85
86 if (!File)
87 return;
88
89 checkForDoubleInclude(&File->getFileEntry(),
90 llvm::sys::path::filename(FilePath),
91 Range.getBegin());
92 }
93
94 void checkForDoubleInclude(const FileEntry *File, StringRef FileName,
95 SourceLocation Loc) {
96 const auto It =
97 llvm::find_if(llvm::reverse(Files),
98 [&](const Include &Entry) { return Entry.File == File; });
99 if (It == Files.rend())
100 return;
101
102 const StringRef FilePath = File->tryGetRealPathName();
103 if (FilePath.empty() || isFileIgnored(FilePath))
104 return;
105
106 if (It == Files.rbegin()) {
107 Check.diag(Loc, "direct self-inclusion of header file '%0'") << FileName;
108 return;
109 }
110
111 Check.diag(Loc, "circular header file dependency detected while including "
112 "'%0', please check the include path")
113 << FileName;
114
115 const bool IsIncludePathValid =
116 std::all_of(Files.rbegin(), It + 1, [](const Include &Elem) {
117 return !Elem.Name.empty() && Elem.Loc.isValid();
118 });
119 if (!IsIncludePathValid)
120 return;
121
122 for (const Include &I : llvm::make_range(Files.rbegin(), It + 1))
123 Check.diag(I.Loc, "'%0' included from here", DiagnosticIDs::Note)
124 << I.Name;
125 }
126
127 bool isFileIgnored(StringRef FileName) const {
128 return llvm::any_of(IgnoredFilesRegexes, [&](const llvm::Regex &It) {
129 return It.match(FileName);
130 });
131 }
132
133#ifndef NDEBUG
134 void EndOfMainFile() override {
135 if (!Files.empty() &&
136 Files.back().File == SM.getFileEntryForID(SM.getMainFileID()))
137 Files.pop_back();
138
139 assert(Files.empty());
140 }
141#endif
142
143private:
144 std::vector<Include> Files;
145 SourceLocation NextToEnter;
146 HeaderIncludeCycleCheck &Check;
147 const SourceManager &SM;
148 std::vector<llvm::Regex> IgnoredFilesRegexes;
149};
150
151} // namespace
152
154 ClangTidyContext *Context)
155 : ClangTidyCheck(Name, Context),
156 IgnoredFilesList(utils::options::parseStringList(
157 Options.get("IgnoredFilesList", ""))) {}
158
160 const SourceManager &SM, Preprocessor *PP, Preprocessor *ModuleExpanderPP) {
161 PP->addPPCallbacks(
162 std::make_unique<CyclicDependencyCallbacks>(*this, SM, IgnoredFilesList));
163}
164
166 Options.store(Opts, "IgnoredFilesList",
167 utils::options::serializeStringList(IgnoredFilesList));
168}
169
170} // namespace clang::tidy::misc
Every ClangTidyCheck reports errors through a DiagnosticsEngine provided by this context.
void registerPPCallbacks(const SourceManager &SM, Preprocessor *PP, Preprocessor *ModuleExpanderPP) override
HeaderIncludeCycleCheck(StringRef Name, ClangTidyContext *Context)
void storeOptions(ClangTidyOptions::OptionMap &Opts) override
std::string serializeStringList(ArrayRef< StringRef > Strings)
Serialize a sequence of names that can be parsed by parseStringList.
llvm::StringMap< ClangTidyValue > OptionMap