xref: /llvm-project/libcxx/utils/libcxx/test/modules.py (revision 94e7c0b051c79fd56205f115771980f2e7812306)
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
9from libcxx.header_information import module_headers
10from libcxx.header_information import header_restrictions
11from dataclasses import dataclass
12
13### SkipDeclarations
14
15# Ignore several declarations found in the includes.
16#
17# Part of these items are bugs other are not yet implemented features.
18SkipDeclarations = dict()
19
20# See comment in the header.
21SkipDeclarations["cuchar"] = ["std::mbstate_t", "std::size_t"]
22
23# Not in the synopsis.
24SkipDeclarations["cwchar"] = ["std::FILE"]
25
26# The operators are added for private types like __iom_t10.
27SkipDeclarations["iomanip"] = ["std::operator<<", "std::operator>>"]
28
29# This header also provides declarations in the namespace that might be
30# an error.
31SkipDeclarations["filesystem"] = [
32    "std::filesystem::operator==",
33    "std::filesystem::operator!=",
34]
35
36# This is a specialization for a private type
37SkipDeclarations["iterator"] = ["std::pointer_traits"]
38
39# TODO MODULES
40# This definition is declared in string and defined in istream
41# This declaration should be part of string
42SkipDeclarations["istream"] = ["std::getline"]
43
44# P1614 (at many places) and LWG3519 too.
45SkipDeclarations["random"] = [
46    "std::operator!=",
47    # LWG3519 makes these hidden friends.
48    # Note the older versions had the requirement of these operations but not in
49    # the synopsis.
50    "std::operator<<",
51    "std::operator>>",
52    "std::operator==",
53]
54
55# include/__type_traits/is_swappable.h
56SkipDeclarations["type_traits"] = [
57    "std::swap",
58    # TODO MODULES gotten through __functional/unwrap_ref.h
59    "std::reference_wrapper",
60]
61
62### ExtraDeclarations
63
64# Add declarations in headers.
65#
66# Some headers have their defines in a different header, which may have
67# additional declarations.
68ExtraDeclarations = dict()
69# This declaration is in the ostream header.
70ExtraDeclarations["system_error"] = ["std::operator<<"]
71
72# TODO MODULES avoid this work-around
73# This is a work-around for the special math functions. They are declared in
74# __math/special_functions.h. Adding this as an ExtraHeader works for the std
75# module. However these functions are special; they are not available in the
76# global namespace.
77ExtraDeclarations["cmath"] = ["std::hermite", "std::hermitef", "std::hermitel"]
78
79### ExtraHeader
80
81# Adds extra headers file to scan
82#
83# Some C++ headers in libc++ are stored in multiple physical files. There is a
84# pattern to find these files. However there are some exceptions these are
85# listed here.
86ExtraHeader = dict()
87# locale has a file and not a subdirectory
88ExtraHeader["locale"] = "v1/__locale$"
89ExtraHeader["ranges"] = "v1/__fwd/subrange.h$"
90
91# The extra header is needed since two headers are required to provide the
92# same definition.
93ExtraHeader["functional"] = "v1/__compare/compare_three_way.h$"
94
95# Some C compatibility headers define std::size_t, which is in <__cstddef/size_t.h>
96for header in ("cstdio", "cstdlib", "cstring", "ctime", "cuchar", "cwchar"):
97    ExtraHeader[header] = "v1/__cstddef/size_t.h$"
98
99
100# newline needs to be escaped for the module partition output.
101nl = "\\\\n"
102
103
104@dataclass
105class module_test_generator:
106    tmp_prefix: str
107    module_path: str
108    clang_tidy: str
109    clang_tidy_plugin: str
110    compiler: str
111    compiler_flags: str
112    module: str
113
114    def write_lit_configuration(self):
115        print(
116            f"""\
117// UNSUPPORTED: c++03, c++11, c++14, c++17
118// UNSUPPORTED: clang-modules-build
119
120// REQUIRES: has-clang-tidy
121
122// The GCC compiler flags are not always compatible with clang-tidy.
123// UNSUPPORTED: gcc
124
125// MODULE_DEPENDENCIES: {self.module}
126
127// RUN: echo -n > {self.tmp_prefix}.all_partitions
128"""
129        )
130
131    def process_module_partition(self, header, is_c_header):
132        # Some headers cannot be included when a libc++ feature is disabled.
133        # In that case include the header conditionally. The header __config
134        # ensures the libc++ feature macros are available.
135        if header in header_restrictions:
136            include = (
137                f"#include <__config>{nl}"
138                f"#if {header_restrictions[header]}{nl}"
139                f"#  include <{header}>{nl}"
140                f"#endif{nl}"
141            )
142        else:
143            include = f"#include <{header}>{nl}"
144
145        module_files = f'#include \\"{self.module_path}/std/{header}.inc\\"{nl}'
146        if is_c_header:
147            module_files += (
148                f'#include \\"{self.module_path}/std.compat/{header}.inc\\"{nl}'
149            )
150
151        # Generate a module partition for the header module includes. This
152        # makes it possible to verify that all headers export all their
153        # named declarations.
154        print(
155            '// RUN: echo -e "'
156            f"module;{nl}"
157            f"{include}{nl}"
158            f"{nl}"
159            f"// Use __libcpp_module_<HEADER> to ensure that modules{nl}"
160            f"// are not named as keywords or reserved names.{nl}"
161            f"export module std:__libcpp_module_{header};{nl}"
162            f"{module_files}"
163            f'" > {self.tmp_prefix}.{header}.cppm'
164        )
165
166        # Extract the information of the module partition using lang-tidy
167        print(
168            f"// RUN: {self.clang_tidy} {self.tmp_prefix}.{header}.cppm "
169            "  --checks='-*,libcpp-header-exportable-declarations' "
170            "  -config='{CheckOptions: [ "
171            "    {"
172            "      key: libcpp-header-exportable-declarations.Filename, "
173            f"     value: {header}.inc"
174            "    }, {"
175            "      key: libcpp-header-exportable-declarations.FileType, "
176            f"     value: {'CompatModulePartition' if is_c_header else 'ModulePartition'}"
177            "    }, "
178            "  ]}' "
179            f"--load={self.clang_tidy_plugin} "
180            f"-- {self.compiler_flags} "
181            f"| sort > {self.tmp_prefix}.{header}.module"
182        )
183        print(
184            f"// RUN: cat  {self.tmp_prefix}.{header}.module >> {self.tmp_prefix}.all_partitions"
185        )
186
187        return include
188
189    def process_header(self, header, include, is_c_header):
190        # Dump the information as found in the module by using the header file(s).
191        skip_declarations = " ".join(SkipDeclarations.get(header, []))
192        if skip_declarations:
193            skip_declarations = (
194                "{"
195                "  key: libcpp-header-exportable-declarations.SkipDeclarations, "
196                f' value: "{skip_declarations}" '
197                "}, "
198            )
199
200        extra_declarations = " ".join(ExtraDeclarations.get(header, []))
201        if extra_declarations:
202            extra_declarations = (
203                "{"
204                "  key: libcpp-header-exportable-declarations.ExtraDeclarations, "
205                f' value: "{extra_declarations}" '
206                "}, "
207            )
208
209        extra_header = ExtraHeader.get(header, "")
210        if extra_header:
211            extra_header = (
212                "{"
213                "  key: libcpp-header-exportable-declarations.ExtraHeader, "
214                f' value: "{extra_header}" '
215                "}, "
216            )
217
218        # Clang-tidy needs a file input
219        print(f'// RUN: echo -e "' f"{include}" f'" > {self.tmp_prefix}.{header}.cpp')
220        print(
221            f"// RUN: {self.clang_tidy} {self.tmp_prefix}.{header}.cpp "
222            "  --checks='-*,libcpp-header-exportable-declarations' "
223            "  -config='{CheckOptions: [ "
224            "    {"
225            "      key: libcpp-header-exportable-declarations.Filename, "
226            f"     value: {header}"
227            "    }, {"
228            "      key: libcpp-header-exportable-declarations.FileType, "
229            f"     value: {'CHeader' if is_c_header else 'Header'}"
230            "    }, "
231            f"   {skip_declarations} {extra_declarations} {extra_header}, "
232            "  ]}' "
233            f"--load={self.clang_tidy_plugin} "
234            f"-- {self.compiler_flags} "
235            f"| sort > {self.tmp_prefix}.{header}.include"
236        )
237        print(
238            f"// RUN: diff -u {self.tmp_prefix}.{header}.module {self.tmp_prefix}.{header}.include"
239        )
240
241    def process_module(self, module):
242        # Merge the data of the parts
243        print(
244            f"// RUN: sort -u -o {self.tmp_prefix}.all_partitions {self.tmp_prefix}.all_partitions"
245        )
246
247        # Dump the information as found in top-level module.
248        print(
249            f"// RUN: {self.clang_tidy} {self.module_path}/{module}.cppm "
250            "  --checks='-*,libcpp-header-exportable-declarations' "
251            "  -config='{CheckOptions: [ "
252            "    {"
253            "      key: libcpp-header-exportable-declarations.Header, "
254            f"     value: {module}.cppm"
255            "    }, {"
256            "      key: libcpp-header-exportable-declarations.FileType, "
257            "      value: Module"
258            "    }, "
259            "  ]}' "
260            f"--load={self.clang_tidy_plugin} "
261            f"-- {self.compiler_flags} "
262            f"| sort > {self.tmp_prefix}.module"
263        )
264
265        # Compare the sum of the parts with the top-level module.
266        print(
267            f"// RUN: diff -u {self.tmp_prefix}.all_partitions {self.tmp_prefix}.module"
268        )
269
270    # Basic smoke test. Import a module and try to compile when using all
271    # exported names. This validates the clang-tidy script does not
272    # accidentally add named declarations to the list that are not available.
273    def test_module(self, module):
274        print(
275            f"""\
276// RUN: echo 'import {module};' > {self.tmp_prefix}.compile.pass.cpp
277// RUN: cat {self.tmp_prefix}.all_partitions >> {self.tmp_prefix}.compile.pass.cpp
278// RUN: {self.compiler} {self.compiler_flags} -fsyntax-only {self.tmp_prefix}.compile.pass.cpp
279"""
280        )
281
282    def write_test(self, module, c_headers=[]):
283        self.write_lit_configuration()
284
285        # Validate all module parts.
286        for header in module_headers:
287            is_c_header = header in c_headers
288            include = self.process_module_partition(header, is_c_header)
289            self.process_header(header, include, is_c_header)
290
291        self.process_module(module)
292        self.test_module(module)
293