Skip to content

Model REQUIRE/REQUIRE_FALSE and REQUIRE_THROWS/NOTHROW/THROWS_AS early termination for Static Analysis mode - #3194

Open
BaLiKfromUA wants to merge 6 commits into
catchorg:develfrom
BaLiKfromUA:issues/3170
Open

Model REQUIRE/REQUIRE_FALSE and REQUIRE_THROWS/NOTHROW/THROWS_AS early termination for Static Analysis mode#3194
BaLiKfromUA wants to merge 6 commits into
catchorg:develfrom
BaLiKfromUA:issues/3170

Conversation

@BaLiKfromUA

@BaLiKfromUA BaLiKfromUA commented Aug 19, 2026

Copy link
Copy Markdown

Description

This patch adds separate implementations of several assertion macros under CATCH_CONFIG_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT, so that single-TU static analysis, such as clang-tidy's bugprone-unchecked-optional-access, can reason about Catch2's assertions.

The goal is to remove a class of false positives (in particular for flow-sensitive analysers) that users currently get in every test that guards with REQUIRE.

The main idea is to model early termination in case of REQUIRE macro by using Catch::Detail::Unreachable().

Testing

The mode's binaries are not runnable, so there is nothing for ctest to assert on; compilation is the only thing that can be checked automatically. I added X08-StaticAnalysisSupport.cpp, a compile-only test that instantiates every affected macro in this mode and fails the build if one of them stops compiling.

That test does not check that the analysis actually improved, so I verified that part by hand (see below).

GitHub Issues

Partially address #3170

Some discussed follow-ups have not been implemented yet:

  • Support of REQUIRE_THAT
  • Support of REQUIRE_THROWS_MATCHES
  • Support of REQUIRE_THROWS_WITH

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 catchorg#3170
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.
`REQUIRE_NOTHROW` marks its `catch( ... )` path unreachable, so the code
after it is only reachable when the expression did not throw.
`REQUIRE_THROWS` is the opposite: the path where the expression did not throw
is the unreachable one.
Like `REQUIRE_THROWS`, but only `exceptionType` counts as the expected
exception.
The expansions in this mode use symbols that are left undefined on purpose,
so the file is compiled but never linked, and compiling it is the test.
@BaLiKfromUA

Copy link
Copy Markdown
Author

Manual tests

REQUIRE/REQUIRE_FALSE

Code example:

#include <catch2/catch_test_macros.hpp>
#include <optional>

TEST_CASE("demo_with_optional_safe") {
  std::optional<int> opt;
  REQUIRE(opt);
  CHECK(*opt == 42);
}

TEST_CASE("demo_with_optional_unsafe1") {
  std::optional<int> opt;
  REQUIRE_FALSE(opt);
  CHECK(*opt == 42); // true positive expected here
}

TEST_CASE("demo_with_optional_unsafe2") {
  std::optional<int> opt;
  CHECK(opt);
  CHECK(*opt == 42); // true positive expected here

  CHECK_FALSE(!opt.has_value());
  CHECK(*opt == 42); // true positive expected here
}

clang-tidy output:

3 warnings generated.
/home/balik/Desktop/Catch2/reproduction.cpp:13:10: warning: unchecked access to optional value [bugprone-unchecked-optional-access]
   13 |   CHECK(*opt == 42); // true positive expected here
      |          ^
/home/balik/Desktop/Catch2/reproduction.cpp:19:10: warning: unchecked access to optional value [bugprone-unchecked-optional-access]
   19 |   CHECK(*opt == 42); // true positive expected here
      |          ^
/home/balik/Desktop/Catch2/reproduction.cpp:22:10: warning: unchecked access to optional value [bugprone-unchecked-optional-access]
   22 |   CHECK(*opt == 42); // true positive expected here

CHECKED_IF / CHECKED_ELSE

Code example:

#include <catch2/catch_test_macros.hpp>
#include <optional>

TEST_CASE("checked_if_safe") {
  std::optional<int> opt;
  CHECKED_IF( opt ) {
    CHECK(*opt == 42); 
  }
}

TEST_CASE("checked_else_safe") {
  std::optional<int> opt;
  CHECKED_ELSE( opt ) {

  } else {
    CHECK(*opt == 42); 
  }
}

TEST_CASE("checked_if_unsafe") {
  std::optional<int> opt;
  CHECKED_IF( opt.has_value() ) {
    
  }
  CHECK(*opt == 42); // true positive expected here
}

clang-tidy output:

1 warning generated.
/home/balik/Desktop/Catch2/reproduction1.cpp:25:10: warning: unchecked access to optional value [bugprone-unchecked-optional-access]
   25 |   CHECK(*opt == 42); // true positive expected here

REQUIRE_NOTHROW

Code example:

#include <catch2/catch_test_macros.hpp>
#include <optional>
#include <stdexcept>

std::optional<int> getOpt();

TEST_CASE("nothrow_optional_safe") {
    std::optional<int> opt = getOpt();
    REQUIRE_NOTHROW( opt ? 0 : throw std::runtime_error("empty") );
    CHECK(*opt == 42);
}

TEST_CASE("nothrow_optional_unsafe") {
    std::optional<int> opt = getOpt();
    REQUIRE_NOTHROW( opt ? 0 : -1 );
    CHECK(*opt == 42); // true positive expected here
}

clang-tidy output:

1 warning generated.
/home/balik/Desktop/Catch2/reproduction2.cpp:16:12: warning: unchecked access to optional value [bugprone-unchecked-optional-access]
   16 |     CHECK(*opt == 42); // true positive expected here

REQUIRE_THROWS

Code example:

#include <catch2/catch_test_macros.hpp>
#include <optional>
#include <stdexcept>

std::optional<int> getOpt();

TEST_CASE("throws_optional_safe") {
    std::optional<int> opt = getOpt();
    REQUIRE_THROWS( opt ? throw std::runtime_error("has value") : 0 );
    CHECK(*opt == 42);
}


TEST_CASE("throws_optional_unsafe") {
    std::optional<int> opt = getOpt();
    CHECK_THROWS( opt ? throw std::runtime_error("has value") : 0 );
    CHECK(*opt == 42); // true positive expected here
}

clang-tidy output:

1 warning generated.
/home/balik/Desktop/Catch2/reproduction3.cpp:17:12: warning: unchecked access to optional value [bugprone-unchecked-optional-access]
   17 |     CHECK(*opt == 42); // true positive expected here

REQUIRE_THROWS_AS

Code example:

#include <catch2/catch_test_macros.hpp>
#include <optional>
#include <stdexcept>

struct MyExc : std::exception {};
std::optional<int> getOpt();

TEST_CASE("throws_as_optional_safe") {
    std::optional<int> opt = getOpt();
    REQUIRE_THROWS_AS( opt ? throw MyExc{} : 0, MyExc );
    CHECK(*opt == 42); 
}


TEST_CASE("throws_as_optional_unsafe") {
    std::optional<int> opt = getOpt();
    CHECK_THROWS_AS( opt ? throw MyExc{} : 0, MyExc );
    CHECK(*opt == 42); // true positive
}

clang-tidy output:

1 warning generated.
/home/balik/Desktop/Catch2/reproduction4.cpp:18:12: warning: unchecked access to optional value [bugprone-unchecked-optional-access]
   18 |     CHECK(*opt == 42); // true positive

@codecov

codecov Bot commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.24%. Comparing base (4cde128) to head (9833b6c).
⚠️ Report is 4 commits behind head on devel.

Additional details and impacted files
@@            Coverage Diff             @@
##            devel    #3194      +/-   ##
==========================================
- Coverage   91.25%   91.24%   -0.01%     
==========================================
  Files         204      204              
  Lines        8965     8973       +8     
==========================================
+ Hits         8181     8187       +6     
- Misses        784      786       +2     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant