diff --git a/clang/lib/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.cpp b/clang/lib/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.cpp --- a/clang/lib/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.cpp +++ b/clang/lib/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.cpp @@ -113,6 +113,41 @@ hasArgument(1, hasOptionalType())); } +auto isValueOrCondition(llvm::StringRef CallID) { + // `!opt.value_or("").empty()`. + auto NonEmptyStringOptional = unaryOperator( + hasOperatorName("!"), + hasUnaryOperand(cxxMemberCallExpr( + callee(cxxMethodDecl(hasName("empty"))), + onImplicitObjectArgument(ignoringImplicit( + cxxMemberCallExpr(on(expr(unless(cxxThisExpr()))), + callee(cxxMethodDecl(hasName("value_or"), + ofClass(optionalClass()))), + hasArgument(0, stringLiteral(hasSize(0)))) + .bind(CallID)))))); + + auto ComparesToSame = + [CallID](clang::ast_matchers::internal::Matcher Arg) { + return hasOperands( + cxxMemberCallExpr(on(expr(unless(cxxThisExpr()))), + callee(cxxMethodDecl(hasName("value_or"), + ofClass(optionalClass()))), + hasArgument(0, Arg)) + .bind(CallID), + ignoringImplicit(Arg)); + }; + + return expr( + anyOf(NonEmptyStringOptional, + // `opt.value_or(nullptr) != nullptr` and `opt.value_or(0) != + // 0`. Ideally, we'd support this pattern for any expression, but + // the AST does not have a generic expression comparison facility, + // so we specialize to common cases seen in practice. + binaryOperation(hasOperatorName("!="), + anyOf(ComparesToSame(cxxNullPtrLiteralExpr()), + ComparesToSame(integerLiteral(equals(0))))))); +} + /// Creates a symbolic value for an `optional` value using `HasValueVal` as the /// symbolic value of its "has_value" property. StructValue &createOptionalValue(Environment &Env, BoolValue &HasValueVal) { @@ -212,6 +247,30 @@ } } +void transferOptionalValueOrCall(const clang::Expr *ComparisonExpr, + const clang::Expr *ObjectArgumentExpr, + LatticeTransferState &State) { + auto *ComparisonExprValue = clang::cast_or_null( + State.Env.getValue(*ComparisonExpr, SkipPast::None)); + if (ComparisonExprValue == nullptr) { + auto &ComparisonExprLoc = State.Env.createStorageLocation(*ComparisonExpr); + ComparisonExprValue = &State.Env.makeAtomicBoolValue(); + State.Env.setValue(ComparisonExprLoc, *ComparisonExprValue); + State.Env.setStorageLocation(*ComparisonExpr, ComparisonExprLoc); + } + + if (auto *OptionalVal = cast_or_null(State.Env.getValue( + *ObjectArgumentExpr, SkipPast::ReferenceThenPointer))) { + auto *HasValueVal = getHasValue(OptionalVal); + assert(HasValueVal != nullptr); + + // Connect the comparison expression to the optional's `hasValue` through + // the implication `(opt.value_or(X) != X) => opt.hasValue()`. + State.Env.addToFlowCondition( + State.Env.makeImplication(*ComparisonExprValue, *HasValueVal)); + } +} + void assignOptionalValue(const Expr &E, LatticeTransferState &State, BoolValue &HasValueVal) { if (auto *OptionalLoc = @@ -418,6 +477,18 @@ // std::swap .CaseOf(isStdSwapCall(), transferStdSwapCall) + // opt.value_or(X) != X, !opt.value_or("").empty(): + .CaseOf( + isValueOrCondition("ValueOrCall"), + [](const clang::Expr *E, const MatchFinder::MatchResult &Result, + LatticeTransferState &State) { + transferOptionalValueOrCall( + E, + Result.Nodes.getNodeAs("ValueOrCall") + ->getImplicitObjectArgument(), + State); + }) + .Build(); } diff --git a/clang/unittests/Analysis/FlowSensitive/UncheckedOptionalAccessModelTest.cpp b/clang/unittests/Analysis/FlowSensitive/UncheckedOptionalAccessModelTest.cpp --- a/clang/unittests/Analysis/FlowSensitive/UncheckedOptionalAccessModelTest.cpp +++ b/clang/unittests/Analysis/FlowSensitive/UncheckedOptionalAccessModelTest.cpp @@ -1710,6 +1710,95 @@ UnorderedElementsAre(Pair("check", "safe"))); } +TEST_P(UncheckedOptionalAccessTest, ValueOrComparison) { + // Pointers. + ExpectLatticeChecksFor( + R"code( + #include "unchecked_optional_access_test.h" + + int* target() { + $ns::$optional opt; + if (opt.value_or(nullptr) != nullptr) { + return *opt; + /*[[check-ptrs-1]]*/ + } else { + *opt; + /*[[check-ptrs-2]]*/ + } + return nullptr; + } + )code", + UnorderedElementsAre(Pair("check-ptrs-1", "safe"), + Pair("check-ptrs-2", "unsafe: input.cc:10:10"))); + + // Integers. + ExpectLatticeChecksFor( + R"code( + #include "unchecked_optional_access_test.h" + + int target() { + $ns::$optional opt; + if (opt.value_or(0) != 0) { + return *opt; + /*[[check-ints-1]]*/ + } else { + *opt; + /*[[check-ints-2]]*/ + } + return 0; + } + )code", + UnorderedElementsAre(Pair("check-ints-1", "safe"), + Pair("check-ints-2", "unsafe: input.cc:10:10"))); + + // Strings. + ExpectLatticeChecksFor( + R"code( + #include "unchecked_optional_access_test.h" + + namespace std { + struct string { + bool empty(); + }; + } + + std::string target() { + $ns::$optional opt; + if (!opt.value_or("").empty()) { + return *opt; + /*[[check-strings-1]]*/ + } else { + *opt; + /*[[check-strings-2]]*/ + } + return {}; + } + )code", + UnorderedElementsAre(Pair("check-strings-1", "safe"), + Pair("check-strings-2", "unsafe: input.cc:16:10"))); + + // Pointer-to-optional. + ExpectLatticeChecksFor( + R"code( + #include "unchecked_optional_access_test.h" + + int target() { + $ns::$optional opt; + auto *opt_p = &opt; + if (opt_p->value_or(0) != 0) { + return opt_p->value(); + /*[[check-pto-1]]*/ + } else { + opt_p->value(); + /*[[check-pto-2]]*/ + } + return 0; + } + )code", + UnorderedElementsAre(Pair("check-pto-1", "safe"), + Pair("check-pto-2", "unsafe: input.cc:11:9"))); +} + TEST_P(UncheckedOptionalAccessTest, Emplace) { ExpectLatticeChecksFor(R"( #include "unchecked_optional_access_test.h" @@ -2008,5 +2097,4 @@ // - constructors (copy, move) // - assignment operators (default, copy, move) // - invalidation (passing optional by non-const reference/pointer) -// - `value_or(nullptr) != nullptr`, `value_or(0) != 0`, `value_or("").empty()` // - nested `optional` values