From 9ae2d5a8bb124fc372a3b02d5968601a1c1f9389 Mon Sep 17 00:00:00 2001 From: BaLiKfromUA Date: Mon, 17 Aug 2026 00:36:43 +0100 Subject: [PATCH 1/5] Model `REQUIRE/CHECK` termination for static analyzer Under `CATCH_CONFIG_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT`, `REQUIRE( expr )` now evaluates `expr` directly and marks the failing path with `Catch::Detail::Unreachable()`, instead of routing the expression through `Catch::AssertionHandler`, which single-TU analyzers cannot see through. `CHECK` keeps falling through, so assertions that do not stop the test case keep being reported. `Unreachable()` is used rather than a throw because that is what `FAIL` and `SKIP` already use, and because it also works when exceptions are disabled. Related to #3170 --- src/catch2/internal/catch_result_type.hpp | 3 +++ src/catch2/internal/catch_test_macro_impl.hpp | 22 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/src/catch2/internal/catch_result_type.hpp b/src/catch2/internal/catch_result_type.hpp index 69a6ef1413..2ae7dcebc3 100644 --- a/src/catch2/internal/catch_result_type.hpp +++ b/src/catch2/internal/catch_result_type.hpp @@ -57,6 +57,9 @@ namespace Catch { constexpr bool isFalseTest( int flags ) { return ( flags & ResultDisposition::FalseTest ) != 0; } + constexpr bool shouldTerminateOnFailure( int flags ) { + return ( flags & ResultDisposition::Normal ) != 0; + } constexpr bool shouldSuppressFailure( int flags ) { return ( flags & ResultDisposition::SuppressFail ) != 0; } diff --git a/src/catch2/internal/catch_test_macro_impl.hpp b/src/catch2/internal/catch_test_macro_impl.hpp index f38ec6066c..8d41c0ac2a 100644 --- a/src/catch2/internal/catch_test_macro_impl.hpp +++ b/src/catch2/internal/catch_test_macro_impl.hpp @@ -10,9 +10,12 @@ #include #include +#include #include +#include #include #include +#include namespace Catch { namespace Detail { @@ -44,6 +47,8 @@ namespace Catch { #endif +#if !defined( CATCH_CONFIG_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT ) + /////////////////////////////////////////////////////////////////////////////// #define INTERNAL_CATCH_TEST( macroName, resultDisposition, ... ) \ do { /* NOLINT(bugprone-infinite-loop) */ \ @@ -60,6 +65,23 @@ namespace Catch { } while( (void)0, (false) && static_cast( !!(__VA_ARGS__) ) ) // the expression here is never evaluated at runtime but it forces the compiler to give it a look // The double negation silences MSVC's C4800 warning, the static_cast forces short-circuit evaluation if the type has overloaded &&. +#else // ^^ !CATCH_CONFIG_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT | vv CATCH_CONFIG_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT + +/////////////////////////////////////////////////////////////////////////////// +# define INTERNAL_CATCH_TEST( macroName, resultDisposition, ... ) \ + do { \ + const bool catchInternalAssertionResult = static_cast( __VA_ARGS__ ); \ + if ( Catch::shouldTerminateOnFailure( resultDisposition ) ) { \ + if ( Catch::isFalseTest( resultDisposition ) ) { \ + if ( catchInternalAssertionResult ) { Catch::Detail::Unreachable(); } \ + } else { \ + if ( !catchInternalAssertionResult ) { Catch::Detail::Unreachable(); } \ + } \ + } \ + } while ( false ) + +#endif // CATCH_CONFIG_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT + /////////////////////////////////////////////////////////////////////////////// #define INTERNAL_CATCH_IF( macroName, resultDisposition, ... ) \ INTERNAL_CATCH_TEST( macroName, resultDisposition, __VA_ARGS__ ); \ From c8efdc36adefcacb6367202b4d4fd3167f8f5e64 Mon Sep 17 00:00:00 2001 From: BaLiKfromUA Date: Mon, 17 Aug 2026 00:36:43 +0100 Subject: [PATCH 2/5] Model `CHECKED_IF/CHECKED_ELSE` for static analyzer In static analysis mode both macros expand to a plain `if` over the user's expression, so that the analyzer sees the branch condition directly, instead of `Catch::Detail::lastAssertionPassed()`, whose value it cannot know. Neither macro stops the test case when the expression is false, so there is no `Unreachable()` on either path. --- src/catch2/internal/catch_test_macro_impl.hpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/catch2/internal/catch_test_macro_impl.hpp b/src/catch2/internal/catch_test_macro_impl.hpp index 8d41c0ac2a..aa8b0a8ed9 100644 --- a/src/catch2/internal/catch_test_macro_impl.hpp +++ b/src/catch2/internal/catch_test_macro_impl.hpp @@ -82,6 +82,8 @@ namespace Catch { #endif // CATCH_CONFIG_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT +#if !defined( CATCH_CONFIG_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT ) + /////////////////////////////////////////////////////////////////////////////// #define INTERNAL_CATCH_IF( macroName, resultDisposition, ... ) \ INTERNAL_CATCH_TEST( macroName, resultDisposition, __VA_ARGS__ ); \ @@ -92,6 +94,18 @@ namespace Catch { INTERNAL_CATCH_TEST( macroName, resultDisposition, __VA_ARGS__ ); \ if( !Catch::Detail::lastAssertionPassed() ) +#else // ^^ !CATCH_CONFIG_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT | vv CATCH_CONFIG_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT + +/////////////////////////////////////////////////////////////////////////////// +# define INTERNAL_CATCH_IF( macroName, resultDisposition, ... ) \ + if ( __VA_ARGS__ ) + +/////////////////////////////////////////////////////////////////////////////// +# define INTERNAL_CATCH_ELSE( macroName, resultDisposition, ... ) \ + if ( !( __VA_ARGS__ ) ) + +#endif // CATCH_CONFIG_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT + /////////////////////////////////////////////////////////////////////////////// #define INTERNAL_CATCH_NO_THROW( macroName, resultDisposition, ... ) \ do { \ From 531e6c88bea60327a9059c5d8743f3f422812d84 Mon Sep 17 00:00:00 2001 From: BaLiKfromUA Date: Mon, 17 Aug 2026 00:36:43 +0100 Subject: [PATCH 3/5] Model `REQUIRE_NOTHROW/CHECK_NOTHROW` for static analyzer `REQUIRE_NOTHROW` marks its `catch( ... )` path unreachable, so the code after it is only reachable when the expression did not throw. --- src/catch2/internal/catch_test_macro_impl.hpp | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/catch2/internal/catch_test_macro_impl.hpp b/src/catch2/internal/catch_test_macro_impl.hpp index aa8b0a8ed9..94b78d1e8b 100644 --- a/src/catch2/internal/catch_test_macro_impl.hpp +++ b/src/catch2/internal/catch_test_macro_impl.hpp @@ -106,6 +106,8 @@ namespace Catch { #endif // CATCH_CONFIG_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT +#if !defined( CATCH_CONFIG_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT ) + /////////////////////////////////////////////////////////////////////////////// #define INTERNAL_CATCH_NO_THROW( macroName, resultDisposition, ... ) \ do { \ @@ -123,6 +125,25 @@ namespace Catch { catchAssertionHandler.complete(); \ } while( false ) +#else // ^^ !CATCH_CONFIG_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT | vv CATCH_CONFIG_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT + +/////////////////////////////////////////////////////////////////////////////// +# define INTERNAL_CATCH_NO_THROW( macroName, resultDisposition, ... ) \ + do { \ + try { \ + CATCH_INTERNAL_START_WARNINGS_SUPPRESSION \ + CATCH_INTERNAL_SUPPRESS_USELESS_CAST_WARNINGS \ + static_cast(__VA_ARGS__); \ + CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION \ + } catch ( ... ) { \ + if ( Catch::shouldTerminateOnFailure( resultDisposition ) ) { \ + Catch::Detail::Unreachable(); \ + } \ + } \ + } while ( false ) + +#endif // CATCH_CONFIG_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT + /////////////////////////////////////////////////////////////////////////////// #define INTERNAL_CATCH_THROWS( macroName, resultDisposition, ... ) \ do { \ From 28a3d72d11acaaada153f63ba5c4dc1ef90e1083 Mon Sep 17 00:00:00 2001 From: BaLiKfromUA Date: Mon, 17 Aug 2026 00:36:43 +0100 Subject: [PATCH 4/5] Model `REQUIRE_THROWS/CHECK_THROWS` for static analyzer `REQUIRE_THROWS` is the opposite: the path where the expression did not throw is the unreachable one. --- src/catch2/internal/catch_test_macro_impl.hpp | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/catch2/internal/catch_test_macro_impl.hpp b/src/catch2/internal/catch_test_macro_impl.hpp index 94b78d1e8b..c7e2c64eec 100644 --- a/src/catch2/internal/catch_test_macro_impl.hpp +++ b/src/catch2/internal/catch_test_macro_impl.hpp @@ -144,6 +144,8 @@ namespace Catch { #endif // CATCH_CONFIG_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT +#if !defined( CATCH_CONFIG_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT ) + /////////////////////////////////////////////////////////////////////////////// #define INTERNAL_CATCH_THROWS( macroName, resultDisposition, ... ) \ do { \ @@ -165,6 +167,29 @@ namespace Catch { catchAssertionHandler.complete(); \ } while( false ) +#else // ^^ !CATCH_CONFIG_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT | vv CATCH_CONFIG_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT + +/////////////////////////////////////////////////////////////////////////////// +# define INTERNAL_CATCH_THROWS( macroName, resultDisposition, ... ) \ + do { \ + bool catchInternalThrew = false; \ + try { \ + CATCH_INTERNAL_START_WARNINGS_SUPPRESSION \ + CATCH_INTERNAL_SUPPRESS_UNUSED_RESULT \ + CATCH_INTERNAL_SUPPRESS_USELESS_CAST_WARNINGS \ + static_cast(__VA_ARGS__); \ + CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION \ + } catch ( ... ) { \ + catchInternalThrew = true; \ + } \ + if ( !catchInternalThrew && \ + Catch::shouldTerminateOnFailure( resultDisposition ) ) { \ + Catch::Detail::Unreachable(); \ + } \ + } while ( false ) + +#endif // CATCH_CONFIG_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT + /////////////////////////////////////////////////////////////////////////////// #define INTERNAL_CATCH_THROWS_AS( macroName, exceptionType, resultDisposition, expr ) \ do { \ From 9088896f6e127f1eb24ef23a33a1638ac3c73f67 Mon Sep 17 00:00:00 2001 From: BaLiKfromUA Date: Mon, 17 Aug 2026 00:36:43 +0100 Subject: [PATCH 5/5] Model `REQUIRE_THROWS_AS/CHECK_THROWS_AS` for static analyzer Like `REQUIRE_THROWS`, but only `exceptionType` counts as the expected exception. --- src/catch2/internal/catch_test_macro_impl.hpp | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/catch2/internal/catch_test_macro_impl.hpp b/src/catch2/internal/catch_test_macro_impl.hpp index c7e2c64eec..fafd5be36f 100644 --- a/src/catch2/internal/catch_test_macro_impl.hpp +++ b/src/catch2/internal/catch_test_macro_impl.hpp @@ -190,6 +190,8 @@ namespace Catch { #endif // CATCH_CONFIG_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT +#if !defined( CATCH_CONFIG_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT ) + /////////////////////////////////////////////////////////////////////////////// #define INTERNAL_CATCH_THROWS_AS( macroName, exceptionType, resultDisposition, expr ) \ do { \ @@ -214,6 +216,30 @@ namespace Catch { catchAssertionHandler.complete(); \ } while( false ) +#else // ^^ !CATCH_CONFIG_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT | vv CATCH_CONFIG_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT + +/////////////////////////////////////////////////////////////////////////////// +# define INTERNAL_CATCH_THROWS_AS( macroName, exceptionType, resultDisposition, expr ) \ + do { \ + bool catchInternalCaughtExpected = false; \ + try { \ + CATCH_INTERNAL_START_WARNINGS_SUPPRESSION \ + CATCH_INTERNAL_SUPPRESS_UNUSED_RESULT \ + CATCH_INTERNAL_SUPPRESS_USELESS_CAST_WARNINGS \ + static_cast(expr); \ + CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION \ + } catch ( exceptionType const& ) { \ + catchInternalCaughtExpected = true; \ + } catch ( ... ) { \ + } \ + if ( !catchInternalCaughtExpected && \ + Catch::shouldTerminateOnFailure( resultDisposition ) ) { \ + Catch::Detail::Unreachable(); \ + } \ + } while ( false ) + +#endif // CATCH_CONFIG_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT + ///////////////////////////////////////////////////////////////////////////////