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,40 @@ hasArgument(1, hasOptionalType())); } +constexpr llvm::StringLiteral ValueOrCallID = "ValueOrCall"; + +auto isValueOrStringEmptyCall() { + // `opt.value_or("").empty()` + return 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(ValueOrCallID)))); +} + +auto isValueOrCondition() { + auto ComparesToSame = [](ast_matchers::internal::Matcher Arg) { + return hasOperands( + cxxMemberCallExpr(on(expr(unless(cxxThisExpr()))), + callee(cxxMethodDecl(hasName("value_or"), + ofClass(optionalClass()))), + hasArgument(0, Arg)) + .bind(ValueOrCallID), + ignoringImplicit(Arg)); + }; + + // `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. + return 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 +246,74 @@ } } +void transferValueOrImpl(const clang::Expr *ComparisonExpr, + const MatchFinder::MatchResult &Result, + LatticeTransferState &State, + BoolValue &(*MakeValue)(Environment &Env, + BoolValue &HasValueVal)) { + auto &Env = State.Env; + + const auto *ObjectArgumentExpr = + Result.Nodes.getNodeAs(ValueOrCallID) + ->getImplicitObjectArgument(); + + auto *OptionalVal = cast_or_null( + Env.getValue(*ObjectArgumentExpr, SkipPast::ReferenceThenPointer)); + if (OptionalVal == nullptr) + return; + + auto *HasValueVal = getHasValue(OptionalVal); + assert(HasValueVal != nullptr); + + // Constrain the `BoolValue` representing the comparison, initializing as + // needed. + BoolValue &ComparisonValue = MakeValue(Env, *HasValueVal); + auto *ComparisonExprLoc = + Env.getStorageLocation(*ComparisonExpr, SkipPast::None); + if (ComparisonExprLoc == nullptr) { + ComparisonExprLoc = &Env.createStorageLocation(*ComparisonExpr); + Env.setStorageLocation(*ComparisonExpr, *ComparisonExprLoc); + Env.setValue(*ComparisonExprLoc, ComparisonValue); + } else if (auto *CurrentValue = + cast_or_null(Env.getValue(*ComparisonExprLoc))) { + Env.setValue(*ComparisonExprLoc, + Env.makeAnd(*CurrentValue, ComparisonValue)); + } else { + Env.setValue(*ComparisonExprLoc, ComparisonValue); + } +} + +void transferValueOrStringEmptyCall(const clang::Expr *ComparisonExpr, + const MatchFinder::MatchResult &Result, + LatticeTransferState &State) { + return transferValueOrImpl( + ComparisonExpr, Result, State, + [](Environment &Env, BoolValue &HasValueVal) -> BoolValue & { + // We can't reason about strings, so we encode the constraint on the + // optional's contents (namely, that it holds the empty string) as an + // atom. + BoolValue &OptionalHoldsEmptyString = Env.makeAtomicBoolValue(); + return Env.makeOr(Env.makeAnd(HasValueVal, OptionalHoldsEmptyString), + Env.makeNot(HasValueVal)); + }); +} + +void transferValueOrNotEqX(const Expr *ComparisonExpr, + const MatchFinder::MatchResult &Result, + LatticeTransferState &State) { + transferValueOrImpl( + ComparisonExpr, Result, State, + [](Environment &Env, BoolValue &HasValueVal) -> BoolValue & { + // We can't reason about general vlaues, so we encode the constraint on + // the optional's contents (namely, that it is not equal to X) as an + // atom. The conjunction is important in case this is used in a + // conditinoal -- the negation is "!HasValue or ContentsEqx". Without + // the conjunction, we'd get the simpler, and incorrect, "!HasValue". + BoolValue &ContentsNotEqX = Env.makeAtomicBoolValue(); + return Env.makeAnd(HasValueVal, ContentsNotEqX); + }); +} + void assignOptionalValue(const Expr &E, LatticeTransferState &State, BoolValue &HasValueVal) { if (auto *OptionalLoc = @@ -418,6 +520,12 @@ // std::swap .CaseOf(isStdSwapCall(), transferStdSwapCall) + // opt.value_or("").empty() + .CaseOf(isValueOrStringEmptyCall(), transferValueOrStringEmptyCall) + + // opt.value_or(X) != X + .CaseOf(isValueOrCondition(), transferValueOrNotEqX) + .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,91 @@ UnorderedElementsAre(Pair("check", "safe"))); } +TEST_P(UncheckedOptionalAccessTest, ValueOrComparison) { + // Pointers. + ExpectLatticeChecksFor( + R"code( + #include "unchecked_optional_access_test.h" + + void target() { + $ns::$optional opt; + if (opt.value_or(nullptr) != nullptr) { + opt.value(); + /*[[check-ptrs-1]]*/ + } else { + opt.value(); + /*[[check-ptrs-2]]*/ + } + } + )code", + UnorderedElementsAre(Pair("check-ptrs-1", "safe"), + Pair("check-ptrs-2", "unsafe: input.cc:10:9"))); + + // Integers. + ExpectLatticeChecksFor( + R"code( + #include "unchecked_optional_access_test.h" + + void target() { + $ns::$optional opt; + if (opt.value_or(0) != 0) { + opt.value(); + /*[[check-ints-1]]*/ + } else { + opt.value(); + /*[[check-ints-2]]*/ + } + } + )code", + UnorderedElementsAre(Pair("check-ints-1", "safe"), + Pair("check-ints-2", "unsafe: input.cc:10:9"))); + + // Strings. + ExpectLatticeChecksFor( + R"code( + #include "unchecked_optional_access_test.h" + + namespace std { + struct string { + bool empty(); + }; + } + + void target() { + $ns::$optional opt; + if (!opt.value_or("").empty()) { + opt.value(); + /*[[check-strings-1]]*/ + } else { + opt.value(); + /*[[check-strings-2]]*/ + } + } + )code", + UnorderedElementsAre(Pair("check-strings-1", "safe"), + Pair("check-strings-2", "unsafe: input.cc:16:9"))); + + // Pointer-to-optional. + ExpectLatticeChecksFor( + R"code( + #include "unchecked_optional_access_test.h" + + void target() { + $ns::$optional opt; + auto *opt_p = &opt; + if (opt_p->value_or(0) != 0) { + opt_p->value(); + /*[[check-pto-1]]*/ + } else { + opt_p->value(); + /*[[check-pto-2]]*/ + } + } + )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 +2093,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