Model REQUIRE/REQUIRE_FALSE and REQUIRE_THROWS/NOTHROW/THROWS_AS early termination for Static Analysis mode - #3194
Model REQUIRE/REQUIRE_FALSE and REQUIRE_THROWS/NOTHROW/THROWS_AS early termination for Static Analysis mode#3194BaLiKfromUA wants to merge 6 commits into
REQUIRE/REQUIRE_FALSE and REQUIRE_THROWS/NOTHROW/THROWS_AS early termination for Static Analysis mode#3194Conversation
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.
Manual tests
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## devel #3194 +/- ##
==========================================
- Coverage 91.25% 91.17% -0.08%
==========================================
Files 204 206 +2
Lines 8965 9031 +66
==========================================
+ Hits 8181 8234 +53
- Misses 784 797 +13 🚀 New features to boost your workflow:
|
|
At a glance the changes look correct. The tests however, need to be done differently. As a rough draft:
We already have some scripts that do their own builds of Catch2 in |
9833b6c to
9088896
Compare
5befc9b to
bd28db5
Compare
|
@horenmar I pushed my trial to implement your draft but I am not sure that did everything correctly with cmake setup and python script scope so bear with me please :) Few comments:
Thank you for your review and recommendation! |
Adds `tests/TestProjects/StaticAnalysis`, one TU per modelled macro, where every line clang-tidy has to report carries an `expect-warning: <check>` marker. `testStaticAnalysisSupport.py` builds it under clang-tidy and fails on an unmarked warning or an unwarned marker. `bugprone-unchecked-optional-access` only honours `[[noreturn]]` since clang-tidy 17, so the test skips itself on older versions. Enabled with `CATCH_ENABLE_STATIC_ANALYSIS_TESTS`, and run in its own CI job. Related to catchorg#3170
bd28db5 to
473c627
Compare
| /////////////////////////////////////////////////////////////////////////////// | ||
| # define INTERNAL_CATCH_TEST( macroName, resultDisposition, ... ) \ | ||
| do { \ | ||
| const bool catchInternalAssertionResult = static_cast<bool>( __VA_ARGS__ ); \ |
There was a problem hiding this comment.
One potential issue that I spotted during self-review is exception throwing from this expression.
We might want to rewrite this line with something like:
bool catchInternalAssertionResult = false;
CATCH_TRY {
catchInternalAssertionResult = static_cast<bool>( __VA_ARGS__ );
} CATCH_CATCH_ALL {
// do nothing?
}
Otherwise, we introduce a false-negative for this strange case:
TEST_CASE( "CHECK with exception inside does not hide warning" ) {
std::optional<int> opt;
CHECK((opt ? true : throw std::runtime_error("empty")));
CHECK(*opt == 42); // expect-warning: bugprone-unchecked-optional-access
}I think this case is strange because:
- Usually, an exception comes via other function/method call e.g
CHECK(vector.at(123) == 42)so Dataflow analysis would not catch it IIRC. - I don't think that people would see this frequently if they don't write
throwdirectly inside assertion :)
I decided to flag it anyway: I don't mind to modify my implementation but I don't know if it adds enough benefit for additional complexity.
Because if we go this route, we might need to adjust handling of REQUIRE_FALSE as well, but maybe I am overthinking...
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'sbugprone-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
REQUIREmacro by usingCatch::Detail::Unreachable().Testing
I tried to do automated testing based on guidance from #3194 (comment)
Manual testing results are posted in #3194 (comment)
GitHub Issues
Partially address #3170
Some discussed follow-ups have not been implemented yet:
REQUIRE_THATREQUIRE_THROWS_MATCHESREQUIRE_THROWS_WITH