diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 8602daaf..4247cdc1 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -2,9 +2,9 @@ name: CMake on: push: - branches: [ "master" ] + branches: [ "master", "dev" ] pull_request: - branches: [ "master" ] + branches: [ "master", "dev" ] jobs: build: @@ -20,6 +20,10 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Check Clang warning hygiene + if: runner.os != 'Windows' + run: python3 scripts/check-warning-hygiene.py --compiler clang++ + - name: Configure CMake # Configure CMake in a 'build' subdirectory. run: bash ${{github.workspace}}/scripts/build-all.sh config ${{github.workspace}} diff --git a/include/zeroerr/fuzztest.h b/include/zeroerr/fuzztest.h index 6546335b..02b06e00 100644 --- a/include/zeroerr/fuzztest.h +++ b/include/zeroerr/fuzztest.h @@ -21,15 +21,15 @@ ZEROERR_SUPPRESS_COMMON_WARNINGS_PUSH #define ZEROERR_CREATE_FUZZ_TEST_FUNC(function, name, ...) \ + ZEROERR_SUPPRESS_COMMON_WARNINGS_PUSH \ static void function(zeroerr::TestContext*); \ static zeroerr::detail::regTest ZEROERR_NAMEGEN(_zeroerr_reg)( \ {name, __FILE__, __LINE__, function, {__VA_ARGS__}}, zeroerr::TestType::fuzz_test); \ + ZEROERR_SUPPRESS_COMMON_WARNINGS_POP \ static void function(ZEROERR_UNUSED(zeroerr::TestContext* _ZEROERR_TEST_CONTEXT)) #define FUZZ_TEST_CASE(...) \ - ZEROERR_SUPPRESS_COMMON_WARNINGS_PUSH \ - ZEROERR_CREATE_FUZZ_TEST_FUNC(ZEROERR_NAMEGEN(_zeroerr_testcase), __VA_ARGS__) \ - ZEROERR_SUPPRESS_COMMON_WARNINGS_POP + ZEROERR_CREATE_FUZZ_TEST_FUNC(ZEROERR_NAMEGEN(_zeroerr_testcase), __VA_ARGS__) #define FUZZ_FUNC(func) zeroerr::FuzzFunction(func, _ZEROERR_TEST_CONTEXT) @@ -234,4 +234,4 @@ std::vector ReadCorpusFromDir(std::string dir); } // namespace zeroerr -ZEROERR_SUPPRESS_COMMON_WARNINGS_POP \ No newline at end of file +ZEROERR_SUPPRESS_COMMON_WARNINGS_POP diff --git a/include/zeroerr/internal/config.h b/include/zeroerr/internal/config.h index 7c4276e1..eb5dd6fa 100644 --- a/include/zeroerr/internal/config.h +++ b/include/zeroerr/internal/config.h @@ -223,6 +223,17 @@ // == COMPILER WARNINGS ============================================================================ // ================================================================================================= +// Clang warning groups vary across upstream, Apple and Android releases. +// Test support instead of emitting an unknown diagnostic from the suppression itself. +#define ZEROERR_CLANG_SUPPRESS_VARIADIC_OMITTED +#if ZEROERR_CLANG && !ZEROERR_ICC +#if __has_warning("-Wvariadic-macro-arguments-omitted") +#undef ZEROERR_CLANG_SUPPRESS_VARIADIC_OMITTED +#define ZEROERR_CLANG_SUPPRESS_VARIADIC_OMITTED \ + ZEROERR_CLANG_SUPPRESS_WARNING("-Wvariadic-macro-arguments-omitted") +#endif +#endif + // both the header and the implementation suppress all of these, // so it only makes sense to aggregate them like so #define ZEROERR_SUPPRESS_COMMON_WARNINGS_PUSH \ @@ -233,7 +244,7 @@ ZEROERR_CLANG_SUPPRESS_WARNING("-Wmissing-prototypes") \ ZEROERR_CLANG_SUPPRESS_WARNING("-Wc++98-compat") \ ZEROERR_CLANG_SUPPRESS_WARNING("-Wc++98-compat-pedantic") \ - ZEROERR_CLANG_SUPPRESS_WARNING("-Wvariadic-macro-arguments-omitted") \ + ZEROERR_CLANG_SUPPRESS_VARIADIC_OMITTED \ \ ZEROERR_GCC_SUPPRESS_WARNING_PUSH \ ZEROERR_GCC_SUPPRESS_WARNING("-Wunknown-pragmas") \ @@ -304,7 +315,7 @@ #define ZEROERR_SUPPRESS_VARIADIC_MACRO \ ZEROERR_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wgnu-zero-variadic-macro-arguments") \ - ZEROERR_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wvariadic-macro-arguments-omitted") + ZEROERR_CLANG_SUPPRESS_VARIADIC_OMITTED #define ZEROERR_SUPPRESS_VARIADIC_MACRO_POP ZEROERR_CLANG_SUPPRESS_WARNING_POP diff --git a/include/zeroerr/log.h b/include/zeroerr/log.h index 6bc3b861..cf685384 100644 --- a/include/zeroerr/log.h +++ b/include/zeroerr/log.h @@ -186,11 +186,11 @@ extern int _ZEROERR_G_VERBOSE; #undef ZEROERR_G_CONTEXT_SCOPE #endif -#define ZEROERR_G_CONTEXT_SCOPE(x) \ - if (x) { \ - for (auto* i : zeroerr::_ZEROERR_G_CONTEXT_SCOPE_VECTOR) { \ - i->str(std::cerr); \ - } \ +#define ZEROERR_G_CONTEXT_SCOPE(x) \ + if (x) { \ + for (auto* zeroerr_context_scope : zeroerr::_ZEROERR_G_CONTEXT_SCOPE_VECTOR) { \ + zeroerr_context_scope->str(std::cerr); \ + } \ } #ifdef ZEROERR_PRINT_ASSERT_DEFAULT_PRINTER diff --git a/include/zeroerr/unittest.h b/include/zeroerr/unittest.h index d29976cf..805b56df 100644 --- a/include/zeroerr/unittest.h +++ b/include/zeroerr/unittest.h @@ -24,10 +24,9 @@ ZEROERR_SUPPRESS_COMMON_WARNINGS_PUSH zeroerr::SubCase(name, __FILE__, __LINE__, _ZEROERR_TEST_CONTEXT, {__VA_ARGS__}) \ << [=](ZEROERR_UNUSED(zeroerr::TestContext * _ZEROERR_TEST_CONTEXT)) mutable -#define SUB_CASE(...) \ - ZEROERR_SUPPRESS_COMMON_WARNINGS_PUSH \ - ZEROERR_EXPAND(ZEROERR_CREATE_SUB_CASE(__VA_ARGS__)) \ - ZEROERR_SUPPRESS_COMMON_WARNINGS_POP +// The caller supplies the lambda body, so no diagnostic pragma may follow +// the lambda declarator (GCC rejects it before the opening brace). +#define SUB_CASE(...) ZEROERR_EXPAND(ZEROERR_CREATE_SUB_CASE(__VA_ARGS__)) #define ZEROERR_CREATE_TEST_CLASS(fixture, classname, funcname, name, ...) \ class classname : public fixture { \ diff --git a/scripts/check-warning-hygiene.py b/scripts/check-warning-hygiene.py new file mode 100644 index 00000000..65b8b555 --- /dev/null +++ b/scripts/check-warning-hygiene.py @@ -0,0 +1,72 @@ +"""Compile warning regressions with --compiler clang++, g++, or cl (developer shell).""" +import argparse +from pathlib import Path +import subprocess +import tempfile + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--compiler", required=True) + args = parser.parse_args() + root = Path(__file__).resolve().parents[1] + msvc = Path(args.compiler).stem.lower() == "cl" + version = subprocess.run([args.compiler, "--version"], capture_output=True, text=True, + errors="replace") + clang = "clang" in version.stdout.lower() + with tempfile.TemporaryDirectory() as tmp: + source = Path(tmp) / "probe.cpp" + def compile_probe(code, flags, diagnostic=None): + source.write_text(code, encoding="utf-8") + if msvc: + command = [args.compiler, "/nologo", "/std:c++17", "/EHsc", "/utf-8", "/c", + f"/I{root / 'include'}", f"/I{root}", f"/Fo{tmp}/probe.obj"] + else: + command = [args.compiler, "-std=c++17", "-fsyntax-only", + f"-I{root / 'include'}", f"-I{root}"] + result = subprocess.run(command + flags + [str(source)], capture_output=True, + text=True, errors="replace") + output = result.stdout + result.stderr + if diagnostic is None: + assert result.returncode == 0, output + else: + assert result.returncode != 0 and diagnostic in output, output + + # A consumer's loop variable must not collide with assertion/logging internals. + for header in ("zeroerr.h", "zeroerr.hpp"): + compile_probe(f'#include "{header}"\n' + + ('' if msvc else '#pragma GCC diagnostic error "-Wshadow"\n') + + 'void probe() { for (int i = 0; i < 2; ++i) { REQUIRE(i >= 0); } }\n', + ["/W4", "/we4456", "/we4458"] if msvc else []) + # Pragmas cannot separate a function/lambda declarator from its body. + for header in ("zeroerr/fuzztest.h", "zeroerr.hpp"): + compile_probe(f'#include "{header}"\n' + 'FUZZ_TEST_CASE("fuzz declaration") {}\n' + 'TEST_CASE("subcase declaration") { SUB_CASE("child") {}; }\n', []) + implementation = '#define ZEROERR_IMPLEMENTATION\n#include "zeroerr.hpp"\n' + deprecated_flags = ["/we4996"] if msvc else ["-Werror=deprecated-declarations"] + compile_probe(implementation, deprecated_flags) + compile_probe(implementation + + '[[deprecated("user diagnostic")]] void old_api();\n' + 'void consumer() { old_api(); }\n', + deprecated_flags, "4996" if msvc else "deprecated") + if clang: + prefix = '#include "zeroerr/internal/config.h"\n' + scoped = ('ZEROERR_SUPPRESS_VARIADIC_MACRO\n' + '#define ZEROERR_PROBE(x, ...) x\n' + 'int value = ZEROERR_PROBE(1);\n' + 'ZEROERR_SUPPRESS_VARIADIC_MACRO_POP\n') + flags = ["-Werror=unknown-warning-option", "-Werror=gnu-zero-variadic-macro-arguments"] + compile_probe(prefix + scoped, flags) + # Leaving the scope must restore the caller's diagnostic state. + compile_probe(prefix + scoped + + '#define USER_PROBE(x, ...) x\nint user = USER_PROBE(2);\n', + flags, "variadic") + compile_probe(prefix + scoped + + '#pragma clang diagnostic ignored "-Wzeroerr-nonexistent-warning"\n', + flags, "unknown warning group") + print("Warning hygiene probes passed" + (" (including Clang diagnostic restoration)" if clang else "")) + + +if __name__ == "__main__": + main() diff --git a/src/log.cpp b/src/log.cpp index 6b8a0884..b07fc7ab 100644 --- a/src/log.cpp +++ b/src/log.cpp @@ -4,6 +4,10 @@ #include #include +// The implementation uses the portable CRT API. Keep this diagnostic local; +// callers must retain their own deprecation warnings. +ZEROERR_MSVC_SUPPRESS_WARNING_WITH_PUSH(4996) + #ifdef _WIN32 #include #else @@ -470,3 +474,5 @@ static std::string DefaultLogCallback(const LogMessage& msg, bool colorful) { #undef zeroerr_color } // namespace zeroerr + +ZEROERR_MSVC_SUPPRESS_WARNING_POP diff --git a/src/unittest.cpp b/src/unittest.cpp index d09370cd..18771755 100644 --- a/src/unittest.cpp +++ b/src/unittest.cpp @@ -937,7 +937,7 @@ class FailureDecorator : public Decorator { enum FailureType { may_fail, should_fail }; FailureDecorator(FailureType type) : type(type) {} - bool onFinish(const TestCase& tc, TestContext& ctx) override { + bool onFinish(const TestCase&, TestContext& ctx) override { if (type == FailureType::may_fail) { // Treat failures as warnings so the suite can continue cleanly. ctx.warning_as += ctx.failed_as; diff --git a/zeroerr.hpp b/zeroerr.hpp index 148121a2..41c63ce4 100644 --- a/zeroerr.hpp +++ b/zeroerr.hpp @@ -226,6 +226,17 @@ // == COMPILER WARNINGS ============================================================================ // ================================================================================================= +// Clang warning groups vary across upstream, Apple and Android releases. +// Test support instead of emitting an unknown diagnostic from the suppression itself. +#define ZEROERR_CLANG_SUPPRESS_VARIADIC_OMITTED +#if ZEROERR_CLANG && !ZEROERR_ICC +#if __has_warning("-Wvariadic-macro-arguments-omitted") +#undef ZEROERR_CLANG_SUPPRESS_VARIADIC_OMITTED +#define ZEROERR_CLANG_SUPPRESS_VARIADIC_OMITTED \ + ZEROERR_CLANG_SUPPRESS_WARNING("-Wvariadic-macro-arguments-omitted") +#endif +#endif + // both the header and the implementation suppress all of these, // so it only makes sense to aggregate them like so #define ZEROERR_SUPPRESS_COMMON_WARNINGS_PUSH \ @@ -236,7 +247,7 @@ ZEROERR_CLANG_SUPPRESS_WARNING("-Wmissing-prototypes") \ ZEROERR_CLANG_SUPPRESS_WARNING("-Wc++98-compat") \ ZEROERR_CLANG_SUPPRESS_WARNING("-Wc++98-compat-pedantic") \ - ZEROERR_CLANG_SUPPRESS_WARNING("-Wvariadic-macro-arguments-omitted") \ + ZEROERR_CLANG_SUPPRESS_VARIADIC_OMITTED \ \ ZEROERR_GCC_SUPPRESS_WARNING_PUSH \ ZEROERR_GCC_SUPPRESS_WARNING("-Wunknown-pragmas") \ @@ -307,7 +318,7 @@ #define ZEROERR_SUPPRESS_VARIADIC_MACRO \ ZEROERR_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wgnu-zero-variadic-macro-arguments") \ - ZEROERR_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wvariadic-macro-arguments-omitted") + ZEROERR_CLANG_SUPPRESS_VARIADIC_OMITTED #define ZEROERR_SUPPRESS_VARIADIC_MACRO_POP ZEROERR_CLANG_SUPPRESS_WARNING_POP @@ -1441,9 +1452,11 @@ struct Printer { #else ZEROERR_ENABLE_IF(ZEROERR_IS_ENUM) print(T value, unsigned level, const char* lb, rank<0>) { - // enum class has no operator<< and no implicit conversion, so fall back - // to its underlying numeric value instead of failing to compile. - os << tab(level) << static_cast::type>(value) << lb; + if constexpr (detail::is_streamable::value) { + os << tab(level) << value << lb; + } else { + os << tab(level) << "" << lb; + } } #endif @@ -3623,11 +3636,11 @@ extern int _ZEROERR_G_VERBOSE; #undef ZEROERR_G_CONTEXT_SCOPE #endif -#define ZEROERR_G_CONTEXT_SCOPE(x) \ - if (x) { \ - for (auto* i : zeroerr::_ZEROERR_G_CONTEXT_SCOPE_VECTOR) { \ - i->str(std::cerr); \ - } \ +#define ZEROERR_G_CONTEXT_SCOPE(x) \ + if (x) { \ + for (auto* zeroerr_context_scope : zeroerr::_ZEROERR_G_CONTEXT_SCOPE_VECTOR) { \ + zeroerr_context_scope->str(std::cerr); \ + } \ } #ifdef ZEROERR_PRINT_ASSERT_DEFAULT_PRINTER @@ -4306,10 +4319,9 @@ ZEROERR_SUPPRESS_COMMON_WARNINGS_PUSH zeroerr::SubCase(name, __FILE__, __LINE__, _ZEROERR_TEST_CONTEXT, {__VA_ARGS__}) \ << [=](ZEROERR_UNUSED(zeroerr::TestContext * _ZEROERR_TEST_CONTEXT)) mutable -#define SUB_CASE(...) \ - ZEROERR_SUPPRESS_COMMON_WARNINGS_PUSH \ - ZEROERR_EXPAND(ZEROERR_CREATE_SUB_CASE(__VA_ARGS__)) \ - ZEROERR_SUPPRESS_COMMON_WARNINGS_POP +// The caller supplies the lambda body, so no diagnostic pragma may follow +// the lambda declarator (GCC rejects it before the opening brace). +#define SUB_CASE(...) ZEROERR_EXPAND(ZEROERR_CREATE_SUB_CASE(__VA_ARGS__)) #define ZEROERR_CREATE_TEST_CLASS(fixture, classname, funcname, name, ...) \ class classname : public fixture { \ @@ -4689,15 +4701,15 @@ ZEROERR_SUPPRESS_COMMON_WARNINGS_POP ZEROERR_SUPPRESS_COMMON_WARNINGS_PUSH #define ZEROERR_CREATE_FUZZ_TEST_FUNC(function, name, ...) \ + ZEROERR_SUPPRESS_COMMON_WARNINGS_PUSH \ static void function(zeroerr::TestContext*); \ static zeroerr::detail::regTest ZEROERR_NAMEGEN(_zeroerr_reg)( \ {name, __FILE__, __LINE__, function, {__VA_ARGS__}}, zeroerr::TestType::fuzz_test); \ + ZEROERR_SUPPRESS_COMMON_WARNINGS_POP \ static void function(ZEROERR_UNUSED(zeroerr::TestContext* _ZEROERR_TEST_CONTEXT)) #define FUZZ_TEST_CASE(...) \ - ZEROERR_SUPPRESS_COMMON_WARNINGS_PUSH \ - ZEROERR_CREATE_FUZZ_TEST_FUNC(ZEROERR_NAMEGEN(_zeroerr_testcase), __VA_ARGS__) \ - ZEROERR_SUPPRESS_COMMON_WARNINGS_POP + ZEROERR_CREATE_FUZZ_TEST_FUNC(ZEROERR_NAMEGEN(_zeroerr_testcase), __VA_ARGS__) #define FUZZ_FUNC(func) zeroerr::FuzzFunction(func, _ZEROERR_TEST_CONTEXT) @@ -4903,6 +4915,7 @@ std::vector ReadCorpusFromDir(std::string dir); ZEROERR_SUPPRESS_COMMON_WARNINGS_POP + #ifdef ZEROERR_IMPLEMENTATION @@ -5179,6 +5192,10 @@ TerminalSize getTerminalWidth() { #include #include +// The implementation uses the portable CRT API. Keep this diagnostic local; +// callers must retain their own deprecation warnings. +ZEROERR_MSVC_SUPPRESS_WARNING_WITH_PUSH(4996) + #ifdef _WIN32 #include #else @@ -5646,6 +5663,8 @@ static std::string DefaultLogCallback(const LogMessage& msg, bool colorful) { } // namespace zeroerr +ZEROERR_MSVC_SUPPRESS_WARNING_POP + @@ -6935,7 +6954,7 @@ class FailureDecorator : public Decorator { enum FailureType { may_fail, should_fail }; FailureDecorator(FailureType type) : type(type) {} - bool onFinish(const TestCase& tc, TestContext& ctx) override { + bool onFinish(const TestCase&, TestContext& ctx) override { if (type == FailureType::may_fail) { // Treat failures as warnings so the suite can continue cleanly. ctx.warning_as += ctx.failed_as;