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 | |
9 | #include "clang-tidy/ClangTidyCheck.h" |
10 | #include "clang-tidy/ClangTidyModuleRegistry.h" |
11 | |
12 | #include "clang/Basic/Module.h" |
13 | |
14 | #include "llvm/ADT/ArrayRef.h" |
15 | |
16 | #include "header_exportable_declarations.hpp" |
17 | |
18 | #include <iostream> |
19 | #include <iterator> |
20 | #include <ranges> |
21 | #include <algorithm> |
22 | |
23 | template <> |
24 | struct clang::tidy::OptionEnumMapping<libcpp::header_exportable_declarations::FileType> { |
25 | static llvm::ArrayRef<std::pair<libcpp::header_exportable_declarations::FileType, llvm::StringRef>> getEnumMapping() { |
26 | static constexpr std::pair<libcpp::header_exportable_declarations::FileType, llvm::StringRef> Mapping[] = { |
27 | {libcpp::header_exportable_declarations::FileType::Header, "Header" }, |
28 | {libcpp::header_exportable_declarations::FileType::ModulePartition, "ModulePartition" }, |
29 | {libcpp::header_exportable_declarations::FileType::Module, "Module" }, |
30 | {libcpp::header_exportable_declarations::FileType::CHeader, "CHeader" }, |
31 | {libcpp::header_exportable_declarations::FileType::CompatModulePartition, "CompatModulePartition" }, |
32 | {libcpp::header_exportable_declarations::FileType::CompatModule, "CompatModule" }}; |
33 | return ArrayRef(Mapping); |
34 | } |
35 | }; |
36 | |
37 | namespace libcpp { |
38 | header_exportable_declarations::( |
39 | llvm::StringRef name, clang::tidy::ClangTidyContext* context) |
40 | : clang::tidy::ClangTidyCheck(name, context), |
41 | filename_(Options.get("Filename" , "" )), |
42 | file_type_(Options.get("FileType" , header_exportable_declarations::FileType::Unknown)), |
43 | extra_header_(Options.get("ExtraHeader" , "" )) { |
44 | switch (file_type_) { |
45 | case header_exportable_declarations::FileType::CHeader: |
46 | case header_exportable_declarations::FileType::Header: |
47 | if (filename_.empty()) |
48 | llvm::errs() << "No filename is provided.\n" ; |
49 | if (extra_header_.empty()) |
50 | extra_header_ = "$^" ; // Use a never matching regex to silence an error message. |
51 | break; |
52 | case header_exportable_declarations::FileType::ModulePartition: |
53 | case header_exportable_declarations::FileType::CompatModulePartition: |
54 | if (filename_.empty()) |
55 | llvm::errs() << "No filename is provided.\n" ; |
56 | [[fallthrough]]; |
57 | case header_exportable_declarations::FileType::Module: |
58 | case header_exportable_declarations::FileType::CompatModule: |
59 | if (!extra_header_.empty()) |
60 | llvm::errs() << "Extra headers are not allowed for modules.\n" ; |
61 | if (Options.get("SkipDeclarations" )) |
62 | llvm::errs() << "Modules may not skip declarations.\n" ; |
63 | if (Options.get("ExtraDeclarations" )) |
64 | llvm::errs() << "Modules may not have extra declarations.\n" ; |
65 | break; |
66 | case header_exportable_declarations::FileType::Unknown: |
67 | llvm::errs() << "No file type is provided.\n" ; |
68 | break; |
69 | } |
70 | |
71 | std::optional<llvm::StringRef> list = Options.get("SkipDeclarations" ); |
72 | if (list) |
73 | for (auto decl : std::views::split(*list, ' ')) { |
74 | std::string s; |
75 | std::ranges::copy(decl, std::back_inserter(s)); // use range based constructor |
76 | skip_decls_.emplace(std::move(s)); |
77 | } |
78 | decls_ = skip_decls_; |
79 | |
80 | list = Options.get("ExtraDeclarations" ); |
81 | if (list) |
82 | for (auto decl : std::views::split(*list, ' ')) |
83 | std::cout << "using ::" << std::string_view{decl.data(), decl.size()} << ";\n" ; |
84 | } |
85 | |
86 | header_exportable_declarations::() { |
87 | for (const auto& name : global_decls_) |
88 | if (!skip_decls_.contains("std::" + name) && decls_.contains("std::" + name)) |
89 | std::cout << "using ::" << name << ";\n" ; |
90 | } |
91 | |
92 | void header_exportable_declarations::(clang::ast_matchers::MatchFinder* finder) { |
93 | // there are no public names in the Standard starting with an underscore, so |
94 | // no need to check the strict rules. |
95 | using namespace clang::ast_matchers; |
96 | |
97 | switch (file_type_) { |
98 | case FileType::Header: |
99 | finder->addMatcher( |
100 | namedDecl( |
101 | // Looks at the common locations where headers store their data |
102 | // * header |
103 | // * __header/*.h |
104 | // * __fwd/header.h |
105 | anyOf(isExpansionInFileMatching(("v1/__" + filename_ + "/" ).str()), |
106 | isExpansionInFileMatching(extra_header_), |
107 | isExpansionInFileMatching(("v1/__fwd/" + filename_ + "\\.h$" ).str()), |
108 | isExpansionInFileMatching(("v1/" + filename_ + "$" ).str())), |
109 | unless(hasAncestor(friendDecl()))) |
110 | .bind("header_exportable_declarations" ), |
111 | this); |
112 | break; |
113 | case FileType::CHeader: |
114 | // For C headers of the std.compat two matchers are used |
115 | // - The cheader matcher; in libc++ these are never split in multiple |
116 | // headers so limiting the declarations to that header works. |
117 | // - The header.h; where the declarations of this header are provided |
118 | // is not specified and depends on the libc used. Therefore it is not |
119 | // possible to restrict the location in a portable way. |
120 | finder->addMatcher(namedDecl().bind("cheader_exportable_declarations" ), this); |
121 | |
122 | [[fallthrough]]; |
123 | case FileType::ModulePartition: |
124 | case FileType::CompatModulePartition: |
125 | finder->addMatcher(namedDecl(isExpansionInFileMatching(filename_)).bind("header_exportable_declarations" ), this); |
126 | break; |
127 | case FileType::Module: |
128 | case FileType::CompatModule: |
129 | finder->addMatcher(namedDecl().bind("header_exportable_declarations" ), this); |
130 | break; |
131 | case header_exportable_declarations::FileType::Unknown: |
132 | llvm::errs() << "This should be unreachable.\n" ; |
133 | break; |
134 | } |
135 | } |
136 | |
137 | /// Returns the qualified name of a public declaration. |
138 | /// |
139 | /// There is a small issue with qualified names. Typically the name returned is |
140 | /// in the namespace \c std instead of the namespace \c std::__1. Except when a |
141 | /// name is declared both in the namespace \c std and in the namespace |
142 | /// \c std::__1. In that case the returned value will adjust the name to use |
143 | /// the namespace \c std. |
144 | /// |
145 | /// The reason this happens is due to some parts of libc++ using |
146 | /// \code namespace std \endcode instead of |
147 | /// \code _LIBCPP_BEGIN_NAMESPACE_STD \endcode |
148 | /// Some examples |
149 | /// * cstddef has bitwise operators for the type \c byte |
150 | /// * exception has equality operators for the type \c exception_ptr |
151 | /// * initializer_list has the functions \c begin and \c end |
152 | /// |
153 | /// When the named declaration uses a reserved name the result is an |
154 | /// empty string. |
155 | static std::string get_qualified_name(const clang::NamedDecl& decl) { |
156 | std::string result = decl.getNameAsString(); |
157 | // Reject reserved names (ignoring _ in global namespace). |
158 | if (result.size() >= 2 && result[0] == '_') |
159 | if (result[1] == '_' || std::isupper(result[1])) |
160 | if (result != "_Exit" ) |
161 | return "" ; |
162 | |
163 | for (auto* context = llvm::dyn_cast_or_null<clang::NamespaceDecl>(decl.getDeclContext()); // |
164 | context; |
165 | context = llvm::dyn_cast_or_null<clang::NamespaceDecl>(context->getDeclContext())) { |
166 | std::string ns = std::string(context->getName()); |
167 | |
168 | if (ns.starts_with("__" )) { |
169 | // When the reserved name is an inline namespace the namespace is |
170 | // not added to the qualified name instead of removed. Libc++ uses |
171 | // several inline namespace with reserved names. For example, |
172 | // __1 for every declaration, __cpo in range-based algorithms. |
173 | // |
174 | // Note other inline namespaces are expanded. This resolves |
175 | // ambiguity when two named declarations have the same name but in |
176 | // different inline namespaces. These typically are the literal |
177 | // conversion operators like operator""s which can be a |
178 | // std::string or std::chrono::seconds. |
179 | if (!context->isInline()) |
180 | return "" ; |
181 | } else |
182 | result = ns + "::" + result; |
183 | } |
184 | return result; |
185 | } |
186 | |
187 | static bool is_viable_declaration(const clang::NamedDecl* decl) { |
188 | // Declarations that are a subobject of a friend Declaration are automatically exported with the record itself. |
189 | if (decl->getFriendObjectKind() != clang::Decl::FOK_None) |
190 | return false; |
191 | |
192 | // *** Function declarations *** |
193 | |
194 | if (clang::CXXMethodDecl::classof(decl)) |
195 | return false; |
196 | |
197 | if (clang::CXXDeductionGuideDecl::classof(decl)) |
198 | return false; |
199 | |
200 | if (clang::FunctionDecl::classof(decl)) |
201 | return true; |
202 | |
203 | if (clang::CXXConstructorDecl::classof(decl)) |
204 | return false; |
205 | |
206 | // implicit constructors disallowed |
207 | if (const auto* r = llvm::dyn_cast_or_null<clang::RecordDecl>(decl)) |
208 | return !r->isLambda() && !r->isImplicit(); |
209 | |
210 | // *** Unconditionally accepted declarations *** |
211 | return llvm::isa<clang::EnumDecl, clang::VarDecl, clang::ConceptDecl, clang::TypedefNameDecl, clang::UsingDecl>(decl); |
212 | } |
213 | |
214 | /// Some declarations in the global namespace are exported from the std module. |
215 | static bool is_global_name_exported_by_std_module(std::string_view name) { |
216 | static const std::set<std::string_view> valid{ |
217 | "operator delete" , "operator delete[]" , "operator new" , "operator new[]" }; |
218 | return valid.contains(name); |
219 | } |
220 | |
221 | static bool ( |
222 | const clang::NamedDecl& decl, std::string_view name, header_exportable_declarations::FileType file_type) { |
223 | const clang::DeclContext& context = *decl.getDeclContext(); |
224 | if (context.isNamespace()) |
225 | return true; |
226 | |
227 | if (context.isFunctionOrMethod() || context.isRecord()) |
228 | return false; |
229 | |
230 | if (is_global_name_exported_by_std_module(name)) |
231 | return true; |
232 | |
233 | return file_type != header_exportable_declarations::FileType::Header; |
234 | } |
235 | |
236 | static bool (header_exportable_declarations::FileType file_type) { |
237 | switch (file_type) { |
238 | case header_exportable_declarations::FileType::Module: |
239 | case header_exportable_declarations::FileType::ModulePartition: |
240 | case header_exportable_declarations::FileType::CompatModule: |
241 | case header_exportable_declarations::FileType::CompatModulePartition: |
242 | return true; |
243 | |
244 | case header_exportable_declarations::FileType::Header: |
245 | case header_exportable_declarations::FileType::CHeader: |
246 | return false; |
247 | |
248 | case header_exportable_declarations::FileType::Unknown: |
249 | llvm::errs() << "This should be unreachable.\n" ; |
250 | return false; |
251 | } |
252 | } |
253 | |
254 | void header_exportable_declarations::(const clang::ast_matchers::MatchFinder::MatchResult& result) { |
255 | if (const auto* decl = result.Nodes.getNodeAs<clang::NamedDecl>("header_exportable_declarations" ); decl != nullptr) { |
256 | if (!is_viable_declaration(decl)) |
257 | return; |
258 | |
259 | std::string name = get_qualified_name(*decl); |
260 | if (name.empty()) |
261 | return; |
262 | |
263 | // For modules only take the declarations exported. |
264 | if (is_module(file_type: file_type_)) |
265 | if (decl->getModuleOwnershipKind() != clang::Decl::ModuleOwnershipKind::VisibleWhenImported) |
266 | return; |
267 | |
268 | if (!is_valid_declaration_context(*decl, name, file_type_)) |
269 | return; |
270 | |
271 | if (decls_.contains(name)) { |
272 | // For modules avoid exporting the same named declaration twice. For |
273 | // header files this is common and valid. |
274 | if (file_type_ == FileType::ModulePartition || file_type_ == FileType::CompatModulePartition) |
275 | // After the warning the script continues. |
276 | // The test will fail since modules have duplicated entries and headers not. |
277 | llvm::errs() << "Duplicated export of '" << name << "'.\n" ; |
278 | else |
279 | return; |
280 | } |
281 | |
282 | // For named declarations in std this is valid |
283 | // using std::foo; |
284 | // for named declarations it is invalid to use |
285 | // using bar; |
286 | // Since fully qualifying named declarations in the std namespace is valid |
287 | // using fully qualified names unconditionally. |
288 | std::cout << "using ::" << std::string{name} << ";\n" ; |
289 | decls_.insert(x: name); |
290 | } else if (const auto* decl = result.Nodes.getNodeAs<clang::NamedDecl>("cheader_exportable_declarations" ); |
291 | decl != nullptr) { |
292 | if (decl->getDeclContext()->isNamespace()) |
293 | return; |
294 | |
295 | if (!is_viable_declaration(decl)) |
296 | return; |
297 | |
298 | std::string name = get_qualified_name(*decl); |
299 | if (name.empty()) |
300 | return; |
301 | |
302 | if (global_decls_.contains(name)) |
303 | return; |
304 | |
305 | global_decls_.insert(x: name); |
306 | } |
307 | } |
308 | |
309 | } // namespace libcpp |
310 | |