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