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 void FileChanged(SourceLocation Loc, FileChangeReason Reason,
44 SrcMgr::CharacteristicKind FileType,
45 FileID PrevFID) override {
46 if (FileType != clang::SrcMgr::C_User)
47 return;
48
49 if (Reason != EnterFile && Reason != ExitFile)
50 return;
51
52 const FileID Id = SM.getFileID(Loc);
53 if (Id.isInvalid())
54 return;
55
56 const FileEntry *NewFile = SM.getFileEntryForID(Id);
57 const FileEntry *PrevFile = SM.getFileEntryForID(PrevFID);
58
59 if (Reason == ExitFile) {
60 if ((Files.size() > 1U) && (Files.back().File == PrevFile) &&
61 (Files[Files.size() - 2U].File == NewFile))
62 Files.pop_back();
63 return;
64 }
65
66 if (!Files.empty() && Files.back().File == NewFile)
67 return;
68
69 const std::optional<StringRef> FilePath = SM.getNonBuiltinFilenameForID(Id);
70 const StringRef FileName =
71 FilePath ? llvm::sys::path::filename(*FilePath) : StringRef();
72 Files.push_back({NewFile, FileName, std::exchange(NextToEnter, {})});
73 }
74
75 void InclusionDirective(SourceLocation, const Token &, StringRef FilePath,
76 bool, CharSourceRange Range,
77 OptionalFileEntryRef File, StringRef, StringRef,
78 const Module *, bool,
79 SrcMgr::CharacteristicKind FileType) override {
80 if (FileType != clang::SrcMgr::C_User)
81 return;
82
83 NextToEnter = Range.getBegin();
84
85 if (!File)
86 return;
87
88 checkForDoubleInclude(&File->getFileEntry(),
89 llvm::sys::path::filename(FilePath),
90 Range.getBegin());
91 }
92
93 void checkForDoubleInclude(const FileEntry *File, StringRef FileName,
94 SourceLocation Loc) {
95 const auto It =
96 llvm::find_if(llvm::reverse(Files),
97 [&](const Include &Entry) { return Entry.File == File; });
98 if (It == Files.rend())
99 return;
100
101 const StringRef FilePath = File->tryGetRealPathName();
102 if (FilePath.empty() || isFileIgnored(FilePath))
103 return;
104
105 if (It == Files.rbegin()) {
106 Check.diag(Loc, "direct self-inclusion of header file '%0'") << FileName;
107 return;
108 }
109
110 Check.diag(Loc, "circular header file dependency detected while including "
111 "'%0', please check the include path")
112 << FileName;
113
114 const bool IsIncludePathValid =
115 std::all_of(Files.rbegin(), It + 1, [](const Include &Elem) {
116 return !Elem.Name.empty() && Elem.Loc.isValid();
117 });
118 if (!IsIncludePathValid)
119 return;
120
121 for (const Include &I : llvm::make_range(Files.rbegin(), It + 1))
122 Check.diag(I.Loc, "'%0' included from here", DiagnosticIDs::Note)
123 << I.Name;
124 }
125
126 bool isFileIgnored(StringRef FileName) const {
127 return llvm::any_of(IgnoredFilesRegexes, [&](const llvm::Regex &It) {
128 return It.match(FileName);
129 });
130 }
131
132#ifndef NDEBUG
133 void EndOfMainFile() override {
134 if (!Files.empty() &&
135 Files.back().File == SM.getFileEntryForID(SM.getMainFileID()))
136 Files.pop_back();
137
138 assert(Files.empty());
139 }
140#endif
141
142private:
143 std::vector<Include> Files;
144 SourceLocation NextToEnter;
145 HeaderIncludeCycleCheck &Check;
146 const SourceManager &SM;
147 std::vector<llvm::Regex> IgnoredFilesRegexes;
148};
149
150} // namespace
151
153 ClangTidyContext *Context)
154 : ClangTidyCheck(Name, Context),
155 IgnoredFilesList(utils::options::parseStringList(
156 Options.get("IgnoredFilesList", ""))) {}
157
159 const SourceManager &SM, Preprocessor *PP, Preprocessor *ModuleExpanderPP) {
160 PP->addPPCallbacks(
161 std::make_unique<CyclicDependencyCallbacks>(*this, SM, IgnoredFilesList));
162}
163
165 Options.store(Opts, "IgnoredFilesList",
166 utils::options::serializeStringList(IgnoredFilesList));
167}
168
169} // 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