llvm/libcxx/utils/libcxx/test/modules.py

# ===----------------------------------------------------------------------===##
#
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
# See https://llvm.org/LICENSE.txt for license information.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
#
# ===----------------------------------------------------------------------===##

from libcxx.header_information import module_headers
from libcxx.header_information import header_restrictions
from dataclasses import dataclass

### SkipDeclarations

# Ignore several declarations found in the includes.
#
# Part of these items are bugs other are not yet implemented features.
SkipDeclarations = dict()

# See comment in the header.
SkipDeclarations["cuchar"] = ["std::mbstate_t", "std::size_t"]

# Not in the synopsis.
SkipDeclarations["cwchar"] = ["std::FILE"]

# The operators are added for private types like __iom_t10.
SkipDeclarations["iomanip"] = ["std::operator<<", "std::operator>>"]

# This header also provides declarations in the namespace that might be
# an error.
SkipDeclarations["filesystem"] = [
    "std::filesystem::operator==",
    "std::filesystem::operator!=",
]

# This is a specialization for a private type
SkipDeclarations["iterator"] = ["std::pointer_traits"]

# TODO MODULES
# This definition is declared in string and defined in istream
# This declaration should be part of string
SkipDeclarations["istream"] = ["std::getline"]

# P1614 (at many places) and LWG3519 too.
SkipDeclarations["random"] = [
    "std::operator!=",
    # LWG3519 makes these hidden friends.
    # Note the older versions had the requirement of these operations but not in
    # the synopsis.
    "std::operator<<",
    "std::operator>>",
    "std::operator==",
]

# TODO MODULES remove zombie names
# https://libcxx.llvm.org/Status/Cxx20.html#note-p0619
SkipDeclarations["memory"] = [
    "std::return_temporary_buffer",
    "std::get_temporary_buffer",
]

# include/__type_traits/is_swappable.h
SkipDeclarations["type_traits"] = [
    "std::swap",
    # TODO MODULES gotten through __functional/unwrap_ref.h
    "std::reference_wrapper",
]

### ExtraDeclarations

# Add declarations in headers.
#
# Some headers have their defines in a different header, which may have
# additional declarations.
ExtraDeclarations = dict()
# This declaration is in the ostream header.
ExtraDeclarations["system_error"] = ["std::operator<<"]

# TODO MODULES avoid this work-around
# This is a work-around for the special math functions. They are declared in
# __math/special_functions.h. Adding this as an ExtraHeader works for the std
# module. However these functions are special; they are not available in the
# global namespace.
ExtraDeclarations["cmath"] = ["std::hermite", "std::hermitef", "std::hermitel"]

### ExtraHeader

# Adds extra headers file to scan
#
# Some C++ headers in libc++ are stored in multiple physical files. There is a
# pattern to find these files. However there are some exceptions these are
# listed here.
ExtraHeader = dict()
# locale has a file and not a subdirectory
ExtraHeader["locale"] = "v1/__locale$"
ExtraHeader["ranges"] = "v1/__fwd/subrange.h$"

# The extra header is needed since two headers are required to provide the
# same definition.
ExtraHeader["functional"] = "v1/__compare/compare_three_way.h$"


# newline needs to be escaped for the module partition output.
nl = "\\\\n"


@dataclass
class module_test_generator:
    tmp_prefix: str
    module_path: str
    clang_tidy: str
    clang_tidy_plugin: str
    compiler: str
    compiler_flags: str
    module: str

    def write_lit_configuration(self):
        print(
            f"""\
// UNSUPPORTED: c++03, c++11, c++14, c++17
// UNSUPPORTED: clang-modules-build

// REQUIRES: has-clang-tidy

// The GCC compiler flags are not always compatible with clang-tidy.
// UNSUPPORTED: gcc

// MODULE_DEPENDENCIES: {self.module}

// RUN: echo -n > {self.tmp_prefix}.all_partitions
"""
        )

    def process_module_partition(self, header, is_c_header):
        # Some headers cannot be included when a libc++ feature is disabled.
        # In that case include the header conditionally. The header __config
        # ensures the libc++ feature macros are available.
        if header in header_restrictions:
            include = (
                f"#include <__config>{nl}"
                f"#if {header_restrictions[header]}{nl}"
                f"#  include <{header}>{nl}"
                f"#endif{nl}"
            )
        else:
            include = f"#include <{header}>{nl}"

        module_files = f'#include \\"{self.module_path}/std/{header}.inc\\"{nl}'
        if is_c_header:
            module_files += (
                f'#include \\"{self.module_path}/std.compat/{header}.inc\\"{nl}'
            )

        # Generate a module partition for the header module includes. This
        # makes it possible to verify that all headers export all their
        # named declarations.
        print(
            '// RUN: echo -e "'
            f"module;{nl}"
            f"{include}{nl}"
            f"{nl}"
            f"// Use __libcpp_module_<HEADER> to ensure that modules{nl}"
            f"// are not named as keywords or reserved names.{nl}"
            f"export module std:__libcpp_module_{header};{nl}"
            f"{module_files}"
            f'" > {self.tmp_prefix}.{header}.cppm'
        )

        # Extract the information of the module partition using lang-tidy
        print(
            f"// RUN: {self.clang_tidy} {self.tmp_prefix}.{header}.cppm "
            "  --checks='-*,libcpp-header-exportable-declarations' "
            "  -config='{CheckOptions: [ "
            "    {"
            "      key: libcpp-header-exportable-declarations.Filename, "
            f"     value: {header}.inc"
            "    }, {"
            "      key: libcpp-header-exportable-declarations.FileType, "
            f"     value: {'CompatModulePartition' if is_c_header else 'ModulePartition'}"
            "    }, "
            "  ]}' "
            f"--load={self.clang_tidy_plugin} "
            f"-- {self.compiler_flags} "
            f"| sort > {self.tmp_prefix}.{header}.module"
        )
        print(
            f"// RUN: cat  {self.tmp_prefix}.{header}.module >> {self.tmp_prefix}.all_partitions"
        )

        return include

    def process_header(self, header, include, is_c_header):
        # Dump the information as found in the module by using the header file(s).
        skip_declarations = " ".join(SkipDeclarations.get(header, []))
        if skip_declarations:
            skip_declarations = (
                "{"
                "  key: libcpp-header-exportable-declarations.SkipDeclarations, "
                f' value: "{skip_declarations}" '
                "}, "
            )

        extra_declarations = " ".join(ExtraDeclarations.get(header, []))
        if extra_declarations:
            extra_declarations = (
                "{"
                "  key: libcpp-header-exportable-declarations.ExtraDeclarations, "
                f' value: "{extra_declarations}" '
                "}, "
            )

        extra_header = ExtraHeader.get(header, "")
        if extra_header:
            extra_header = (
                "{"
                "  key: libcpp-header-exportable-declarations.ExtraHeader, "
                f' value: "{extra_header}" '
                "}, "
            )

        # Clang-tidy needs a file input
        print(f'// RUN: echo -e "' f"{include}" f'" > {self.tmp_prefix}.{header}.cpp')
        print(
            f"// RUN: {self.clang_tidy} {self.tmp_prefix}.{header}.cpp "
            "  --checks='-*,libcpp-header-exportable-declarations' "
            "  -config='{CheckOptions: [ "
            "    {"
            "      key: libcpp-header-exportable-declarations.Filename, "
            f"     value: {header}"
            "    }, {"
            "      key: libcpp-header-exportable-declarations.FileType, "
            f"     value: {'CHeader' if is_c_header else 'Header'}"
            "    }, "
            f"   {skip_declarations} {extra_declarations} {extra_header}, "
            "  ]}' "
            f"--load={self.clang_tidy_plugin} "
            f"-- {self.compiler_flags} "
            f"| sort > {self.tmp_prefix}.{header}.include"
        )
        print(
            f"// RUN: diff -u {self.tmp_prefix}.{header}.module {self.tmp_prefix}.{header}.include"
        )

    def process_module(self, module):
        # Merge the data of the parts
        print(
            f"// RUN: sort -u -o {self.tmp_prefix}.all_partitions {self.tmp_prefix}.all_partitions"
        )

        # Dump the information as found in top-level module.
        print(
            f"// RUN: {self.clang_tidy} {self.module_path}/{module}.cppm "
            "  --checks='-*,libcpp-header-exportable-declarations' "
            "  -config='{CheckOptions: [ "
            "    {"
            "      key: libcpp-header-exportable-declarations.Header, "
            f"     value: {module}.cppm"
            "    }, {"
            "      key: libcpp-header-exportable-declarations.FileType, "
            "      value: Module"
            "    }, "
            "  ]}' "
            f"--load={self.clang_tidy_plugin} "
            f"-- {self.compiler_flags} "
            f"| sort > {self.tmp_prefix}.module"
        )

        # Compare the sum of the parts with the top-level module.
        print(
            f"// RUN: diff -u {self.tmp_prefix}.all_partitions {self.tmp_prefix}.module"
        )

    # Basic smoke test. Import a module and try to compile when using all
    # exported names. This validates the clang-tidy script does not
    # accidentally add named declarations to the list that are not available.
    def test_module(self, module):
        print(
            f"""\
// RUN: echo 'import {module};' > {self.tmp_prefix}.compile.pass.cpp
// RUN: cat {self.tmp_prefix}.all_partitions >> {self.tmp_prefix}.compile.pass.cpp
// RUN: {self.compiler} {self.compiler_flags} -fsyntax-only {self.tmp_prefix}.compile.pass.cpp
"""
        )

    def write_test(self, module, c_headers=[]):
        self.write_lit_configuration()

        # Validate all module parts.
        for header in module_headers:
            is_c_header = header in c_headers
            include = self.process_module_partition(header, is_c_header)
            self.process_header(header, include, is_c_header)

        self.process_module(module)
        self.test_module(module)