Describe the bug
Consider the code below:
TEST_CASE("demo_with_optional_safe") {
std::optional<int> opt;
REQUIRE(opt);
CHECK(*opt == 42); // safe: false-positive
}
TEST_CASE("demo_with_optional_unsafe") {
std::optional<int> opt;
CHECK(opt);
CHECK(*opt == 42); // unsafe
}If we run the clang-tidy bugprone-unchecked-optional-access check, which warns when an optional is accessed without a preceding check that it holds a value, we get 2 warnings:
<source>:28:10: warning: unchecked access to optional value [bugprone-unchecked-optional-access]
28 | CHECK(*opt == 42); // safe: false-positive
| ^
<source>:35:10: warning: unchecked access to optional value [bugprone-unchecked-optional-access]
35 | CHECK(*opt == 42); // unsafe
| ^
2 warnings generated.
Suppressed 2 warnings (2 NOLINT).Expected behavior
The optional access in "demo_with_optional_safe" should not be flagged, since REQUIRE terminates the test on failure, guaranteeing opt.has_value() at the point of access.
The optional access in "demo_with_optional_unsafe" should be flagged, since CHECK doesn't terminate the test on failure, so the access is reachable with an empty optional.
Reproduction steps
Same example in Compiler Explorer
Additional context
I'm addressing high-priority bugprone-unchecked-optional-access false positives at Bloomberg. I recently fixed a related false positive for code using GTest (llvm/llvm-project#181737), and ran into an identical issue with Catch2.
Because of how REQUIRE expands internally, clang-tidy's dataflow analysis can't tell that a failed assertion terminates the test, so it can't rule out the "unchecked" path.
I initially considered adding Catch2-specific AST matchers to clang-tidy to model Catch::AssertionHandler's control flow directly, similar to the GTest fix. After discussing it internally, though, it looks like the fix belongs on the Catch2 side instead, since there is already a precedent for this kind of thing (docs).
I wanted to raise this here before taking it to the clang-tidy maintainers. I believe the patch on Catch2 side will be much simpler and less error-prone.
During my experiments with CE, I put together a rough proof-of-concept that resolves the false positive. I am not proposing it as the final implementation, but sharing it as a starting point for discussion:
#if defined(__clang_analyzer__)
#undef REQUIRE
#define REQUIRE(...) \
do { if (!(__VA_ARGS__)) Catch::throw_test_failure_exception(); } while(false)
#undef CHECK
#define CHECK(...) \
do { (void)(__VA_ARGS__); } while(false)
#endifI'm happy to put together a proper patch if we agree this should be fixed inside Catch2. Keen to discuss the right approach here first.
Activity
2I implemented initial patch for most of the REQUIRE macros, but left out the matcher-based ones (REQUIRE_THAT, REQUIRE_THROWS_WITH, REQUIRE_THROWS_MATCHES).
Two reasons:
- I am not sure what shape would be appropriate. The obvious one is
if ( !matcher.match( arg ) ) { Unreachable(); }but maybe it's not enough. - I am not sure how to test it. It looks more like path-sensitive analysis which does not work for my clang-tidy scenario. Maybe I am wrong!
I am happy to add them on top of existing PRs!
But, for now, I need a first round of feedback since it's my first contribution to this project :)
Thoughts:
bugprone-unchecked-optional-accessI am surprised it doesn't have configuration option for custom assert macros. This might actually be worth adding to upstream separately, because supporting custom assert macros is fairly common in similar checks.REQUIREmacros (REQUIRE,REQUIRE_THAT,REQUIRE_THROWS, ...).Doing the moral equivalent of
if (!expr) { throw ... }is probably good enough alternative implementation. There might be some complications further down the route, but those are best discovered by trying.