[nullability] Add support for `operator->` on `std::optional`.
We've repeatedly seen the pattern where people have a pointer stored in an
optional struct, perform a null-check on the pointer ("through" the optional),
then dereference it. Introducing limited support for optionals prevents false
positives on these code patterns.
In addition to `std::optional`, we also support
[`absl::optional`](https://github.com/abseil/abseil-cpp/blob/master/absl/types/optional.h)
and
[`folly::Optional`](https://github.com/facebook/folly/blob/main/folly/Optional.h).
PiperOrigin-RevId: 609261743
Change-Id: I79a0673d1fee71071dc23338b0d67fd7b0728c3d
diff --git a/bazel/llvm.bzl b/bazel/llvm.bzl
index 52c9422..505cf5e 100644
--- a/bazel/llvm.bzl
+++ b/bazel/llvm.bzl
@@ -53,7 +53,7 @@
executable = False,
)
-LLVM_COMMIT_SHA = "4c6043de0b837d23699424d875057d00956d80ac"
+LLVM_COMMIT_SHA = "2e29c91b96832504b9008be5e095f7dd640cdea0"
def llvm_loader_repository_dependencies():
# This *declares* the dependency, but it won't actually be *downloaded* unless it's used.
diff --git a/nullability/pointer_nullability_analysis.cc b/nullability/pointer_nullability_analysis.cc
index 1450863..0babeb6 100644
--- a/nullability/pointer_nullability_analysis.cc
+++ b/nullability/pointer_nullability_analysis.cc
@@ -1061,28 +1061,42 @@
}
}
+void handleConstMemberCall(absl::Nonnull<const CallExpr *> CE,
+ dataflow::RecordStorageLocation *RecordLoc,
+ const MatchFinder::MatchResult &Result,
+ TransferState<PointerNullabilityLattice> &State) {
+ if (!isSupportedRawPointerType(CE->getType()) || !CE->isPRValue() ||
+ RecordLoc == nullptr) {
+ // Perform default handling.
+ transferValue_CallExpr(CE, Result, State);
+ return;
+ }
+ PointerValue *PointerVal =
+ State.Lattice.getConstMethodReturnValue(*RecordLoc, CE, State.Env);
+ if (PointerVal) {
+ State.Env.setValue(*CE, *PointerVal);
+ initPointerFromTypeNullability(*PointerVal, CE, State);
+ }
+}
+
void transferValue_ConstMemberCall(
absl::Nonnull<const CXXMemberCallExpr *> MCE,
const MatchFinder::MatchResult &Result,
TransferState<PointerNullabilityLattice> &State) {
- if (!isSupportedRawPointerType(MCE->getType()) || !MCE->isPRValue()) {
- // We can't handle it as a special case, but still need to handle it.
- transferValue_CallExpr(MCE, Result, State);
- return;
- }
- dataflow::RecordStorageLocation *RecordLoc =
- dataflow::getImplicitObjectLocation(*MCE, State.Env);
- if (RecordLoc == nullptr) {
- // We can't handle it as a special case, but still need to handle it.
- transferValue_CallExpr(MCE, Result, State);
- return;
- }
- PointerValue *PointerVal =
- State.Lattice.getConstMethodReturnValue(*RecordLoc, MCE, State.Env);
- if (PointerVal) {
- State.Env.setValue(*MCE, *PointerVal);
- initPointerFromTypeNullability(*PointerVal, MCE, State);
- }
+ handleConstMemberCall(
+ MCE, dataflow::getImplicitObjectLocation(*MCE, State.Env), Result, State);
+}
+
+void transferValue_OptionalOperatorArrowCall(
+ absl::Nonnull<const CXXOperatorCallExpr *> OCE,
+ const MatchFinder::MatchResult &Result,
+ TransferState<PointerNullabilityLattice> &State) {
+ auto *RecordLoc = cast_or_null<dataflow::RecordStorageLocation>(
+ State.Env.getStorageLocation(*OCE->getArg(0)));
+ // `optional::operator->` isn't necessarily const, but it behaves the way we
+ // model "const member calls": It always returns the same pointer if the
+ // optional wasn't mutated in the meantime.
+ handleConstMemberCall(OCE, RecordLoc, Result, State);
}
void handleNonConstMemberCall(absl::Nonnull<const CallExpr *> CE,
@@ -1484,6 +1498,9 @@
transferValue_AccessorCall)
.CaseOfCFGStmt<CXXMemberCallExpr>(isZeroParamConstMemberCall(),
transferValue_ConstMemberCall)
+ .CaseOfCFGStmt<CXXOperatorCallExpr>(
+ isOptionalOperatorArrowCall(),
+ transferValue_OptionalOperatorArrowCall)
.CaseOfCFGStmt<CXXMemberCallExpr>(isNonConstMemberCall(),
transferValue_NonConstMemberCall)
.CaseOfCFGStmt<CXXOperatorCallExpr>(
diff --git a/nullability/pointer_nullability_lattice.h b/nullability/pointer_nullability_lattice.h
index 898e55f..33b0590 100644
--- a/nullability/pointer_nullability_lattice.h
+++ b/nullability/pointer_nullability_lattice.h
@@ -75,13 +75,14 @@
// the CXXMemberCallExpr to have a supported pointer type.
absl::Nonnull<dataflow::PointerValue *> getConstMethodReturnValue(
const dataflow::RecordStorageLocation &RecordLoc,
- absl::Nonnull<const CXXMemberCallExpr *> MCE,
- dataflow::Environment &Env) {
+ absl::Nonnull<const CallExpr *> CE, dataflow::Environment &Env) {
auto &ObjMap = ConstMethodReturnValues[&RecordLoc];
- auto it = ObjMap.find(MCE->getMethodDecl());
+ const FunctionDecl *DirectCallee = CE->getDirectCallee();
+ if (DirectCallee == nullptr) return nullptr;
+ auto it = ObjMap.find(DirectCallee);
if (it != ObjMap.end()) return it->second;
- auto *PV = cast<dataflow::PointerValue>(Env.createValue(MCE->getType()));
- ObjMap.insert({MCE->getMethodDecl(), PV});
+ auto *PV = cast<dataflow::PointerValue>(Env.createValue(CE->getType()));
+ ObjMap.insert({DirectCallee, PV});
return PV;
}
@@ -119,7 +120,7 @@
// from that const method.
llvm::SmallDenseMap<
const dataflow::RecordStorageLocation *,
- llvm::SmallDenseMap<const CXXMethodDecl *, dataflow::PointerValue *>>
+ llvm::SmallDenseMap<const FunctionDecl *, dataflow::PointerValue *>>
ConstMethodReturnValues;
};
diff --git a/nullability/pointer_nullability_matchers.cc b/nullability/pointer_nullability_matchers.cc
index 854b68e..7ac6378 100644
--- a/nullability/pointer_nullability_matchers.cc
+++ b/nullability/pointer_nullability_matchers.cc
@@ -57,6 +57,7 @@
using ast_matchers::isInStdNamespace;
using ast_matchers::isMemberInitializer;
using ast_matchers::memberExpr;
+using ast_matchers::ofClass;
using ast_matchers::parameterCountIs;
using ast_matchers::pointee;
using ast_matchers::pointerType;
@@ -109,6 +110,13 @@
callee(cxxMethodDecl(parameterCountIs(0), isConst())));
}
+Matcher<Stmt> isOptionalOperatorArrowCall() {
+ return cxxOperatorCallExpr(
+ hasOverloadedOperatorName("->"),
+ callee(cxxMethodDecl(ofClass(hasAnyName(
+ "::std::optional", "::absl::optional", "::folly::Optional")))));
+}
+
Matcher<Stmt> isNonConstMemberCall() {
return cxxMemberCallExpr(callee(cxxMethodDecl(unless(isConst()))));
}
diff --git a/nullability/pointer_nullability_matchers.h b/nullability/pointer_nullability_matchers.h
index acd8b9f..739ea88 100644
--- a/nullability/pointer_nullability_matchers.h
+++ b/nullability/pointer_nullability_matchers.h
@@ -46,6 +46,7 @@
ast_matchers::internal::Matcher<Stmt> isPointerReturn();
ast_matchers::internal::Matcher<CXXCtorInitializer> isCtorMemberInitializer();
ast_matchers::internal::Matcher<Stmt> isZeroParamConstMemberCall();
+ast_matchers::internal::Matcher<Stmt> isOptionalOperatorArrowCall();
ast_matchers::internal::Matcher<Stmt> isNonConstMemberCall();
ast_matchers::internal::Matcher<Stmt> isNonConstMemberOperatorCall();
ast_matchers::internal::Matcher<Stmt> isSmartPointerGlValue();
diff --git a/nullability/pointer_nullability_matchers_test.cc b/nullability/pointer_nullability_matchers_test.cc
index 13f924c..6687aeb 100644
--- a/nullability/pointer_nullability_matchers_test.cc
+++ b/nullability/pointer_nullability_matchers_test.cc
@@ -153,5 +153,62 @@
isSmartPointerBoolConversionCall()));
}
+TEST(PointerNullabilityTest, MatchOptionalOperatorArrowCall) {
+ llvm::StringRef Input(R"cc(
+ namespace std {
+ template <class T>
+ struct optional {
+ T* operator->();
+ T& operator*();
+ };
+ } // namespace std
+
+ namespace absl {
+ template <class T>
+ struct optional {
+ T* operator->();
+ };
+ } // namespace absl
+
+ namespace folly {
+ template <class T>
+ struct Optional {
+ T* operator->();
+ };
+ } // namespace folly
+
+ // Lots of libraries that used to define their own optional now make it an
+ // alias for `std::optional`, so we want to support this too.
+ template <class T>
+ using StdOptionalAlias = std::optional<T>;
+
+ template <class T>
+ struct optional {
+ T* operator->();
+ };
+
+ struct S {
+ int i;
+ };
+ )cc");
+
+ EXPECT_TRUE(matches(Input, "void target(std::optional<S> o){ o->i; }",
+ isOptionalOperatorArrowCall()));
+ EXPECT_TRUE(matches(Input, "void target(absl::optional<S> o){ o->i; }",
+ isOptionalOperatorArrowCall()));
+ EXPECT_TRUE(matches(Input, "void target(folly::Optional<S> o){ o->i; }",
+ isOptionalOperatorArrowCall()));
+ EXPECT_TRUE(matches(Input, "void target(StdOptionalAlias<S> o){ o->i; }",
+ isOptionalOperatorArrowCall()));
+
+ // Not one of the supported optional classes.
+ EXPECT_FALSE(matches(Input, "void target(optional<S> o){ o->i; }",
+ isOptionalOperatorArrowCall()));
+
+ // Not `operator->`.
+ EXPECT_FALSE(matches(Input, "void target(std::optional<S> o){ *o; }",
+ isOptionalOperatorArrowCall()));
+}
+
} // namespace
} // namespace clang::tidy::nullability
diff --git a/nullability/test/function_calls.cc b/nullability/test/function_calls.cc
index 4c1d926..b8ef90e 100644
--- a/nullability/test/function_calls.cc
+++ b/nullability/test/function_calls.cc
@@ -861,6 +861,35 @@
)cc"));
}
+TEST(PointerNullabilityTest, OptionalOperatorArrowCall) {
+ // Check that repeated accesses to a pointer behind an optional are considered
+ // to yield the same pointer -- but only if the optional is not modified in
+ // the meantime.
+ EXPECT_TRUE(checkDiagnostics(R"cc(
+ namespace std {
+ template <class T>
+ struct optional {
+ bool has_value() const;
+ T* operator->();
+ };
+ } // namespace std
+
+ struct S {
+ int* _Nullable p;
+ };
+
+ void target(std::optional<S> opt1, std::optional<S> opt2) {
+ if (!opt1.has_value() || !opt2.has_value()) return;
+ *opt1->p; // [[unsafe]]
+ if (opt1->p != nullptr) {
+ *opt1->p;
+ opt1 = opt2;
+ *opt1->p; // [[unsafe]]
+ }
+ }
+ )cc"));
+}
+
TEST(PointerNullabilityTest, FieldUndefinedValue) {
EXPECT_TRUE(checkDiagnostics(R"cc(
struct C {