From: Jakub Jelinek Date: Wed, 29 Apr 2026 05:55:02 +0000 (+0200) Subject: testsuite: Add plugin to verify bits/std.cc exports X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=5d657f6bbb1853350fa29f037fe04ab8fefcea92;p=thirdparty%2Fgcc.git testsuite: Add plugin to verify bits/std.cc exports The following patch adds another g++.dg/plugin/ testsuite plugin, this time to verify whether some std.cc exports aren't mistakenly omitted. The patch is a reworked version of the https://gcc.gnu.org/pipermail/libstdc++/2025-August/thread.html#62859 proof of concept. That version just dumped out everything it saw in the std namespace and its child namespaces (excluding non-inline subnamespaces with identifiers starting with underscore) and then I've used sed&grep to form a list of omissions. This patch keeps the previous walk of std namespace and namespaces children of it, but it only reports (in this version using error_at instead of inform previously) what it finds if it isn't exported from the module and is not deprecated (deprecated attribute is used usually for zombie.names in the standard). I've been strugling with the detection of what is and what isn't exported, had to try several different methods. What is DECL_MODULE_EXPORT_P is ignored, but that is not set on everything actually exported. In other cases there is OVL_EXPORT_P flag on OVERLOAD (but OVL_HIDDEN_P at the start doesn't have it). Another case are inline namespaces, e.g. for std::filesystem::__cxx11::begin or std::filesystem::__cxx11::directory_iterator. In the latter case, there is no sign of the above flags in __cxx11 binding entry, but there is a USING_DECL with the same name directly in std::filesystem. And for begin there is OVERLOAD with OVL_EXPORT_P in std::filesystem but not in std::filesystem::__cxx11. 2026-04-29 Jakub Jelinek * g++.dg/plugin/plugin.exp: Set PLUGIN_DEFAULT_REPO. Add set*module*exports* to plugin_test_list. Remove *.gcm files at the start and end. * g++.dg/plugin/std_module_exports_plugin.cc: New file. * g++.dg/plugin/std-module-exports-c++20.C: New test. * g++.dg/plugin/std-module-exports-c++23.C: New test. * g++.dg/plugin/std-module-exports-c++26.C: New test. Reviewed-by: Andrew Pinski --- diff --git a/gcc/testsuite/g++.dg/plugin/plugin.exp b/gcc/testsuite/g++.dg/plugin/plugin.exp index 26df19b1b5a..ab04ce10834 100644 --- a/gcc/testsuite/g++.dg/plugin/plugin.exp +++ b/gcc/testsuite/g++.dg/plugin/plugin.exp @@ -51,6 +51,8 @@ if ![gcc_parallel_test_run_p plugin] { } gcc_parallel_test_enable 0 +set PLUGIN_DEFAULT_REPO "gcm.cache" + # Specify the plugin source file and the associated test files in a list. # plugin_test_list={ {plugin1 test1 test2 ...} {plugin2 test1 ...} ... } set plugin_test_list [list \ @@ -87,9 +89,17 @@ set plugin_test_list [list \ uglification-c++20.C \ uglification-c++23.C \ uglification-c++26.C } \ + { std_module_exports_plugin.cc \ + std-module-exports-c++20.C \ + std-module-exports-c++23.C \ + std-module-exports-c++26.C } \ { comment_plugin.cc comments-1.C } \ ] +foreach file [find $PLUGIN_DEFAULT_REPO *.gcm] { + file_on_host delete $file +} + foreach plugin_test $plugin_test_list { # Replace each source file with its full-path name for {set i 0} {$i < [llength $plugin_test]} {incr i} { @@ -105,4 +115,8 @@ foreach plugin_test $plugin_test_list { plugin-test-execute $plugin_src $plugin_input_tests } +foreach file [find $PLUGIN_DEFAULT_REPO *.gcm] { + file_on_host delete $file +} + gcc_parallel_test_enable 1 diff --git a/gcc/testsuite/g++.dg/plugin/std-module-exports-c++20.C b/gcc/testsuite/g++.dg/plugin/std-module-exports-c++20.C new file mode 100644 index 00000000000..0640eeeb995 --- /dev/null +++ b/gcc/testsuite/g++.dg/plugin/std-module-exports-c++20.C @@ -0,0 +1,11 @@ +// Test bits/std.cc exports everything it should. +// This is done using a plugin to walk std namespace and +// its child namespaces, looking for decls with non-uglified +// names which aren't deprecated and are not exported. +// { dg-do link } +// { dg-options "-O0 -std=c++20 -fmodules -x c++-system-module bits/std.cc -x none" } + +int +main () +{ +} diff --git a/gcc/testsuite/g++.dg/plugin/std-module-exports-c++23.C b/gcc/testsuite/g++.dg/plugin/std-module-exports-c++23.C new file mode 100644 index 00000000000..d5745f16b11 --- /dev/null +++ b/gcc/testsuite/g++.dg/plugin/std-module-exports-c++23.C @@ -0,0 +1,11 @@ +// Test bits/std.cc exports everything it should. +// This is done using a plugin to walk std namespace and +// its child namespaces, looking for decls with non-uglified +// names which aren't deprecated and are not exported. +// { dg-do link } +// { dg-options "-O0 -std=c++23 -fmodules -x c++-system-module bits/std.cc -x none" } + +int +main () +{ +} diff --git a/gcc/testsuite/g++.dg/plugin/std-module-exports-c++26.C b/gcc/testsuite/g++.dg/plugin/std-module-exports-c++26.C new file mode 100644 index 00000000000..7e8f5e2e426 --- /dev/null +++ b/gcc/testsuite/g++.dg/plugin/std-module-exports-c++26.C @@ -0,0 +1,11 @@ +// Test bits/std.cc exports everything it should. +// This is done using a plugin to walk std namespace and +// its child namespaces, looking for decls with non-uglified +// names which aren't deprecated and are not exported. +// { dg-do link } +// { dg-options "-O0 -std=c++26 -fmodules -freflection -fcontracts -x c++-system-module bits/std.cc -x none" } + +int +main () +{ +} diff --git a/gcc/testsuite/g++.dg/plugin/std_module_exports_plugin.cc b/gcc/testsuite/g++.dg/plugin/std_module_exports_plugin.cc new file mode 100644 index 00000000000..e81eb6d3fb6 --- /dev/null +++ b/gcc/testsuite/g++.dg/plugin/std_module_exports_plugin.cc @@ -0,0 +1,219 @@ +#include "gcc-plugin.h" +#include +#include "config.h" +#include "system.h" +#include "coretypes.h" +#include "tree.h" +#include "intl.h" +#include "cp/cp-tree.h" +#include "cp/name-lookup.h" +#include "diagnostic.h" +#include "stringpool.h" +#include "attribs.h" + +int plugin_is_GPL_compatible; + +void plugin_dump_ns (tree, char *, hash_set **); + +enum class whitelist_std { + cxx20 = 1 << 0, + cxx23 = 1 << 1, + cxx26 = 1 << 2, + all = (1 << 3) - 1 +}; +struct { + const char *name; + whitelist_std enabled_in; +} whitelist[] = { + // [zombie.names] in C++20-C++26, supported as an extension. + { "std::result_of", whitelist_std::all }, + { "std::result_of_t", whitelist_std::all }, + // [zombie.names] in C++20-C++26, supported in C++20 as an extension. + { "std::unexpected_handler", whitelist_std::cxx20 }, + // [zombie.names] in C++23-C++26, not exported in C++20. + { "std::declare_no_pointers", whitelist_std::cxx20 }, + { "std::declare_reachable", whitelist_std::cxx20 }, + { "std::get_pointer_safety", whitelist_std::cxx20 }, + { "std::pointer_safety", whitelist_std::cxx20 }, + { "std::undeclare_no_pointers", whitelist_std::cxx20 }, + { "std::undeclare_reachable", whitelist_std::cxx20 } +}; + +void +plugin_dump_decl (tree decl, char *scope, hash_set **exported_usings) +{ + if (VAR_P (decl) && DECL_ARTIFICIAL (decl)) + return; + + tree name = DECL_NAME (decl); + if (!name) + return; + + if (IDENTIFIER_ANON_P (name)) + return; + + if (TREE_CODE (decl) == CONST_DECL) + return; + + if (TREE_CODE (decl) == NAMESPACE_DECL && DECL_NAMESPACE_INLINE_P (decl)) + { + plugin_dump_ns (decl, scope, exported_usings); + return; + } + + if (IDENTIFIER_POINTER (name)[0] == '_' + || strchr (IDENTIFIER_POINTER (name), ' ')) + return; + + if (TREE_CODE (decl) == NAMESPACE_DECL) + { + char *p = strchr (scope, '\0'); + strcpy (p, IDENTIFIER_POINTER (name)); + strcat (p, "::"); + plugin_dump_ns (decl, scope, exported_usings); + *p = '\0'; + return; + } + + if (DECL_MODULE_EXPORT_P (decl)) + return; + if (TREE_DEPRECATED (decl) + || lookup_attribute ("deprecated", DECL_ATTRIBUTES (decl))) + return; + if (TREE_CODE (decl) == TEMPLATE_DECL + && DECL_TEMPLATE_RESULT (decl) + && (TREE_DEPRECATED (DECL_TEMPLATE_RESULT (decl)) + || lookup_attribute ("deprecated", + DECL_ATTRIBUTES (DECL_TEMPLATE_RESULT (decl))))) + return; + if (*exported_usings + && (*exported_usings)->contains (DECL_NAME (decl))) + return; + + size_t scope_len = strlen (scope); + whitelist_std this_std; + if (cxx_dialect == cxx20) + this_std = whitelist_std::cxx20; + else if (cxx_dialect == cxx23) + this_std = whitelist_std::cxx23; + else if (cxx_dialect == cxx26) + this_std = whitelist_std::cxx26; + for (int i = 0; i < ARRAY_SIZE (whitelist); ++i) + if (strncmp (whitelist[i].name, scope, scope_len) == 0 + && strcmp (whitelist[i].name + scope_len, + IDENTIFIER_POINTER (name)) == 0) + { + if (((int) whitelist[i].enabled_in & (int) this_std) != 0) + { + inform (DECL_SOURCE_LOCATION (decl), + "missing using %s%D; whitelisted", scope, name); + return; + } + break; + } + + error_at (DECL_SOURCE_LOCATION (decl), "missing using %s%D;", scope, name); +} + +void +plugin_dump_binding (tree binding, char *scope, + hash_set **exported_usings) +{ + tree value = NULL_TREE; + + if (TREE_CODE (binding) == OVERLOAD) + { + tree ovl = ovl_skip_hidden (binding); + if (ovl && TREE_CODE (ovl) == OVERLOAD && OVL_EXPORT_P (ovl)) + return; + } + + if (STAT_HACK_P (binding)) + { + if (!STAT_TYPE_HIDDEN_P (binding) + && STAT_TYPE (binding)) + return plugin_dump_decl (STAT_TYPE (binding), scope, exported_usings); + else if (!STAT_DECL_HIDDEN_P (binding)) + value = STAT_DECL (binding); + } + else + value = binding; + + value = ovl_skip_hidden (value); + if (value) + { + value = OVL_FIRST (value); + return plugin_dump_decl (value, scope, exported_usings); + } +} + +void +plugin_dump_ns (tree ns, char *scope, hash_set **exported_usings) +{ + using itert = hash_table::iterator; + itert end (DECL_NAMESPACE_BINDINGS (ns)->end ()); + hash_set *my_exported_usings = NULL; + if (DECL_NAMESPACE_INLINE_P (ns)) + { + if (*exported_usings == NULL) + { + *exported_usings = new hash_set; + tree parent = DECL_CONTEXT (ns); + itert pend (DECL_NAMESPACE_BINDINGS (parent)->end ()); + for (itert iter (DECL_NAMESPACE_BINDINGS (parent)->begin ()); + iter != pend; ++iter) + { + tree b = *iter; + if (TREE_CODE (b) == USING_DECL + && DECL_MODULE_EXPORT_P (b) + && DECL_NAME (b)) + (*exported_usings)->add (DECL_NAME (b)); + else if (TREE_CODE (b) == OVERLOAD) + { + tree ovl = ovl_skip_hidden (b); + if (ovl && TREE_CODE (ovl) == OVERLOAD && OVL_EXPORT_P (ovl)) + { + b = OVL_FIRST (ovl); + if (DECL_NAME (b)) + (*exported_usings)->add (DECL_NAME (b)); + } + } + } + } + } + else + exported_usings = &my_exported_usings; + for (itert iter (DECL_NAMESPACE_BINDINGS (ns)->begin ()); + iter != end; ++iter) + { + tree b = *iter; + gcc_assert (TREE_CODE (b) != BINDING_VECTOR); + plugin_dump_binding (b, scope, exported_usings); + } + delete my_exported_usings; +} + +void +plugin_finish_unit (void *, void *) +{ + if (!main_input_filename + || strstr (main_input_filename, "/std.cc") == NULL) + return; + + char buf[4096]; + strcpy (buf, "std::"); + hash_set *exported_usings = NULL; + plugin_dump_ns (std_node, buf, &exported_usings); + delete exported_usings; +} + +int +plugin_init (struct plugin_name_args *plugin_info, + struct plugin_gcc_version *version) +{ + const char *plugin_name = plugin_info->base_name; + + register_callback (plugin_name, PLUGIN_FINISH_UNIT, + plugin_finish_unit, NULL); + return 0; +}