[clang] 7f07600 - [clang][dataflow] Add support for `value_or` in a comparison.

Yitzhak Mandelbaum via cfe-commits cfe-commits at lists.llvm.org
Thu Mar 31 06:22:04 PDT 2022


Author: Yitzhak Mandelbaum
Date: 2022-03-31T13:21:39Z
New Revision: 7f076004e941fe60ab613a218da31a25b09b0925

URL: https://github.com/llvm/llvm-project/commit/7f076004e941fe60ab613a218da31a25b09b0925
DIFF: https://github.com/llvm/llvm-project/commit/7f076004e941fe60ab613a218da31a25b09b0925.diff

LOG: [clang][dataflow] Add support for `value_or` in a comparison.

This patch adds limited modeling of the `value_or` method. Specifically, when
used in a particular idiom in a comparison to implicitly check whether the
optional holds a value.

Differential Revision: https://reviews.llvm.org/D122231

Added: 
    

Modified: 
    clang/lib/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.cpp
    clang/unittests/Analysis/FlowSensitive/UncheckedOptionalAccessModelTest.cpp

Removed: 
    


################################################################################
diff  --git a/clang/lib/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.cpp b/clang/lib/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.cpp
index b775698dafb5d..aaf8289c48674 100644
--- a/clang/lib/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.cpp
+++ b/clang/lib/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.cpp
@@ -121,6 +121,43 @@ auto isStdSwapCall() {
                   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 isValueOrNotEqX() {
+  auto ComparesToSame = [](ast_matchers::internal::Matcher<Stmt> Arg) {
+    return hasOperands(
+        ignoringImplicit(
+            cxxMemberCallExpr(on(expr(unless(cxxThisExpr()))),
+                              callee(cxxMethodDecl(hasName("value_or"),
+                                                   ofClass(optionalClass()))),
+                              hasArgument(0, Arg))
+                .bind(ValueOrCallID)),
+        ignoringImplicit(Arg));
+  };
+
+  // `opt.value_or(X) != X`, for X is `nullptr`, `""`, or `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.  FIXME: define a matcher that compares values across
+  // nodes, which would let us generalize this to any `X`.
+  return binaryOperation(hasOperatorName("!="),
+                         anyOf(ComparesToSame(cxxNullPtrLiteralExpr()),
+                               ComparesToSame(stringLiteral(hasSize(0))),
+                               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) {
@@ -220,6 +257,69 @@ void transferOptionalHasValueCall(const CXXMemberCallExpr *CallExpr,
   }
 }
 
+/// `ModelPred` builds a logical formula relating the predicate in
+/// `ValueOrPredExpr` to the optional's `has_value` property.
+void transferValueOrImpl(const clang::Expr *ValueOrPredExpr,
+                         const MatchFinder::MatchResult &Result,
+                         LatticeTransferState &State,
+                         BoolValue &(*ModelPred)(Environment &Env,
+                                                 BoolValue &ExprVal,
+                                                 BoolValue &HasValueVal)) {
+  auto &Env = State.Env;
+
+  const auto *ObjectArgumentExpr =
+      Result.Nodes.getNodeAs<clang::CXXMemberCallExpr>(ValueOrCallID)
+          ->getImplicitObjectArgument();
+
+  auto *OptionalVal = cast_or_null<StructValue>(
+      Env.getValue(*ObjectArgumentExpr, SkipPast::ReferenceThenPointer));
+  if (OptionalVal == nullptr)
+    return;
+  auto *HasValueVal = getHasValue(OptionalVal);
+  assert(HasValueVal != nullptr);
+
+  auto *ExprValue = cast_or_null<BoolValue>(
+      State.Env.getValue(*ValueOrPredExpr, SkipPast::None));
+  if (ExprValue == nullptr) {
+    auto &ExprLoc = State.Env.createStorageLocation(*ValueOrPredExpr);
+    ExprValue = &State.Env.makeAtomicBoolValue();
+    State.Env.setValue(ExprLoc, *ExprValue);
+    State.Env.setStorageLocation(*ValueOrPredExpr, ExprLoc);
+  }
+
+  Env.addToFlowCondition(ModelPred(Env, *ExprValue, *HasValueVal));
+}
+
+void transferValueOrStringEmptyCall(const clang::Expr *ComparisonExpr,
+                                    const MatchFinder::MatchResult &Result,
+                                    LatticeTransferState &State) {
+  return transferValueOrImpl(ComparisonExpr, Result, State,
+                             [](Environment &Env, BoolValue &ExprVal,
+                                BoolValue &HasValueVal) -> BoolValue & {
+                               // If the result is *not* empty, then we know the
+                               // optional must have been holding a value. If
+                               // `ExprVal` is true, though, we don't learn
+                               // anything definite about `has_value`, so we
+                               // don't add any corresponding implications to
+                               // the flow condition.
+                               return Env.makeImplication(Env.makeNot(ExprVal),
+                                                          HasValueVal);
+                             });
+}
+
+void transferValueOrNotEqX(const Expr *ComparisonExpr,
+                           const MatchFinder::MatchResult &Result,
+                           LatticeTransferState &State) {
+  transferValueOrImpl(ComparisonExpr, Result, State,
+                      [](Environment &Env, BoolValue &ExprVal,
+                         BoolValue &HasValueVal) -> BoolValue & {
+                        // We know that if `(opt.value_or(X) != X)` then
+                        // `opt.hasValue()`, even without knowing further
+                        // details about the contents of `opt`.
+                        return Env.makeImplication(ExprVal, HasValueVal);
+                      });
+}
+
 void assignOptionalValue(const Expr &E, LatticeTransferState &State,
                          BoolValue &HasValueVal) {
   if (auto *OptionalLoc =
@@ -439,6 +539,12 @@ auto buildTransferMatchSwitch(
       // std::swap
       .CaseOf<CallExpr>(isStdSwapCall(), transferStdSwapCall)
 
+      // opt.value_or("").empty()
+      .CaseOf<Expr>(isValueOrStringEmptyCall(), transferValueOrStringEmptyCall)
+
+      // opt.value_or(X) != X
+      .CaseOf<Expr>(isValueOrNotEqX(), transferValueOrNotEqX)
+
       .Build();
 }
 

diff  --git a/clang/unittests/Analysis/FlowSensitive/UncheckedOptionalAccessModelTest.cpp b/clang/unittests/Analysis/FlowSensitive/UncheckedOptionalAccessModelTest.cpp
index 6ef4b2a9c3336..a501f549851f9 100644
--- a/clang/unittests/Analysis/FlowSensitive/UncheckedOptionalAccessModelTest.cpp
+++ b/clang/unittests/Analysis/FlowSensitive/UncheckedOptionalAccessModelTest.cpp
@@ -496,6 +496,24 @@ using enable_if_t = typename std::enable_if<B, T>::type;
 #endif // ABSL_TYPE_TRAITS_H
 )";
 
+static constexpr char StdStringHeader[] = R"(
+#ifndef STRING_H
+#define STRING_H
+
+namespace std {
+
+struct string {
+  string(const char*);
+  ~string();
+  bool empty();
+};
+bool operator!=(const string &LHS, const char *RHS);
+
+} // namespace std
+
+#endif // STRING_H
+)";
+
 static constexpr char StdUtilityHeader[] = R"(
 #ifndef UTILITY_H
 #define UTILITY_H
@@ -1198,6 +1216,7 @@ class UncheckedOptionalAccessTest
     std::vector<std::pair<std::string, std::string>> Headers;
     Headers.emplace_back("cstddef.h", CSDtdDefHeader);
     Headers.emplace_back("std_initializer_list.h", StdInitializerListHeader);
+    Headers.emplace_back("std_string.h", StdStringHeader);
     Headers.emplace_back("std_type_traits.h", StdTypeTraitsHeader);
     Headers.emplace_back("std_utility.h", StdUtilityHeader);
     Headers.emplace_back("std_optional.h", StdOptionalHeader);
@@ -1209,6 +1228,7 @@ class UncheckedOptionalAccessTest
       #include "base_optional.h"
       #include "std_initializer_list.h"
       #include "std_optional.h"
+      #include "std_string.h"
       #include "std_utility.h"
 
       template <typename T>
@@ -1712,6 +1732,102 @@ TEST_P(UncheckedOptionalAccessTest, ValueOr) {
                          UnorderedElementsAre(Pair("check", "safe")));
 }
 
+TEST_P(UncheckedOptionalAccessTest, ValueOrComparison) {
+  // Pointers.
+  ExpectLatticeChecksFor(
+      R"code(
+    #include "unchecked_optional_access_test.h"
+
+    void target($ns::$optional<int*> 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:9:9")));
+
+  // Integers.
+  ExpectLatticeChecksFor(
+      R"code(
+    #include "unchecked_optional_access_test.h"
+
+    void target($ns::$optional<int> 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:9:9")));
+
+  // Strings.
+  ExpectLatticeChecksFor(
+      R"code(
+    #include "unchecked_optional_access_test.h"
+
+    void target($ns::$optional<std::string> 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:9:9")));
+
+  ExpectLatticeChecksFor(
+      R"code(
+    #include "unchecked_optional_access_test.h"
+
+    void target($ns::$optional<std::string> opt) {
+      if (opt.value_or("") != "") {
+        opt.value();
+        /*[[check-strings-neq-1]]*/
+      } else {
+        opt.value();
+        /*[[check-strings-neq-2]]*/
+      }
+    }
+  )code",
+      UnorderedElementsAre(
+          Pair("check-strings-neq-1", "safe"),
+          Pair("check-strings-neq-2", "unsafe: input.cc:9:9")));
+
+  // Pointer-to-optional.
+  //
+  // FIXME: make `opt` a parameter directly, once we ensure that all `optional`
+  // values have a `has_value` property.
+  ExpectLatticeChecksFor(
+      R"code(
+    #include "unchecked_optional_access_test.h"
+
+    void target($ns::$optional<int> p) {
+      $ns::$optional<int> *opt = &p;
+      if (opt->value_or(0) != 0) {
+        opt->value();
+        /*[[check-pto-1]]*/
+      } else {
+        opt->value();
+        /*[[check-pto-2]]*/
+      }
+    }
+  )code",
+      UnorderedElementsAre(Pair("check-pto-1", "safe"),
+                           Pair("check-pto-2", "unsafe: input.cc:10:9")));
+}
+
 TEST_P(UncheckedOptionalAccessTest, Emplace) {
   ExpectLatticeChecksFor(R"(
     #include "unchecked_optional_access_test.h"
@@ -2038,5 +2154,4 @@ TEST_P(UncheckedOptionalAccessTest, UniquePtrToStructWithOptionalField) {
 // - 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


        


More information about the cfe-commits mailing list