Generalize support for const member functions
Treat consecutive const member function calls as idempotent, even when they aren't simple accessors/the implementation is not visible.
PiperOrigin-RevId: 580596960
Change-Id: Id75da4b24ccf48e63b6d4e8060dbaf261c690169
diff --git a/nullability/pointer_nullability_analysis.cc b/nullability/pointer_nullability_analysis.cc
index b02582d..be1e0f8 100644
--- a/nullability/pointer_nullability_analysis.cc
+++ b/nullability/pointer_nullability_analysis.cc
@@ -537,6 +537,21 @@
}
}
+void transferFlowSensitiveConstMemberCall(
+ const CXXMemberCallExpr *MCE, const MatchFinder::MatchResult &Result,
+ TransferState<PointerNullabilityLattice> &State) {
+ if (!isSupportedPointerType(MCE->getType())) return;
+ dataflow::RecordStorageLocation *RecordLoc =
+ dataflow::getImplicitObjectLocation(*MCE, State.Env);
+ if (RecordLoc == nullptr) return;
+ PointerValue *PointerVal =
+ State.Lattice.getConstMethodReturnValue(*RecordLoc, MCE, State.Env);
+ if (PointerVal) {
+ State.Env.setValue(*MCE, *PointerVal);
+ initPointerFromTypeNullability(*PointerVal, MCE, State);
+ }
+}
+
void transferFlowSensitiveNonConstMemberCall(
const CXXMemberCallExpr *MCE, const MatchFinder::MatchResult &Result,
TransferState<PointerNullabilityLattice> &State) {
@@ -549,6 +564,7 @@
Value *V = State.Env.createValue(Field->getType());
State.Env.setValue(*FieldLoc, *V);
}
+ State.Lattice.clearConstMethodReturnValues(*RecordLoc);
}
// The nullability of the Expr itself still needs to be handled.
transferFlowSensitiveCallExpr(MCE, Result, State);
@@ -868,6 +884,8 @@
transferFlowSensitiveNullPointer)
.CaseOfCFGStmt<CXXMemberCallExpr>(isSupportedPointerAccessorCall(),
transferFlowSensitiveAccessorCall)
+ .CaseOfCFGStmt<CXXMemberCallExpr>(isZeroParamConstMemberCall(),
+ transferFlowSensitiveConstMemberCall)
.CaseOfCFGStmt<CXXMemberCallExpr>(isNonConstMemberCall(),
transferFlowSensitiveNonConstMemberCall)
.CaseOfCFGStmt<CallExpr>(isCallExpr(), transferFlowSensitiveCallExpr)
diff --git a/nullability/pointer_nullability_lattice.h b/nullability/pointer_nullability_lattice.h
index 932fbb3..6457ca5 100644
--- a/nullability/pointer_nullability_lattice.h
+++ b/nullability/pointer_nullability_lattice.h
@@ -12,9 +12,13 @@
#include "absl/container/flat_hash_map.h"
#include "absl/log/check.h"
#include "nullability/type_nullability.h"
+#include "clang/AST/DeclCXX.h"
#include "clang/AST/Expr.h"
#include "clang/Analysis/FlowSensitive/DataflowAnalysisContext.h"
+#include "clang/Analysis/FlowSensitive/DataflowEnvironment.h"
#include "clang/Analysis/FlowSensitive/DataflowLattice.h"
+#include "clang/Analysis/FlowSensitive/StorageLocation.h"
+#include "clang/Analysis/FlowSensitive/Value.h"
#include "clang/Basic/LLVM.h"
#include "llvm/ADT/FunctionExtras.h"
@@ -62,6 +66,25 @@
return Iterator->second;
}
+ // Gets the PointerValue associated with the RecordStorageLocation and
+ // MethodDecl of the CallExpr, creating one if it doesn't yet exist. Requires
+ // the CXXMemberCallExpr to have a supported pointer type.
+ dataflow::PointerValue *getConstMethodReturnValue(
+ const dataflow::RecordStorageLocation &RecordLoc,
+ const CXXMemberCallExpr *MCE, dataflow::Environment &Env) {
+ auto &ObjMap = ConstMethodReturnValues[&RecordLoc];
+ auto it = ObjMap.find(MCE->getMethodDecl());
+ if (it != ObjMap.end()) return it->second;
+ auto *PV = cast<dataflow::PointerValue>(Env.createValue(MCE->getType()));
+ ObjMap.insert({MCE->getMethodDecl(), PV});
+ return PV;
+ }
+
+ void clearConstMethodReturnValues(
+ const dataflow::RecordStorageLocation &RecordLoc) {
+ ConstMethodReturnValues.erase(&RecordLoc);
+ }
+
// If nullability for the decl D has been overridden, patch N to reflect it.
// (N is the nullability of an access to D).
void overrideNullabilityFromDecl(const Decl *D, TypeNullability &N) const;
@@ -69,13 +92,29 @@
bool operator==(const PointerNullabilityLattice &Other) const { return true; }
dataflow::LatticeJoinEffect join(const PointerNullabilityLattice &Other) {
- return dataflow::LatticeJoinEffect::Unchanged;
+ if (ConstMethodReturnValues.empty())
+ return dataflow::LatticeJoinEffect::Unchanged;
+ // Conservatively, just clear the `ConstMethodReturnValues` map entirely.
+ // This means that we can't check the return value from a const method
+ // before a join, then call the method again to use the pointer after the
+ // join -- we'll get a false positive in this case.
+ // TODO(b/309667920): Add code to actually join the maps if it turns out
+ // these types of false positives are common.
+ ConstMethodReturnValues.clear();
+ return dataflow::LatticeJoinEffect::Changed;
}
private:
// Owned by the PointerNullabilityAnalysis object, shared by all lattice
// elements within one analysis run.
NonFlowSensitiveState &NFS;
+
+ // Maps a record storage location and const method to the value to return
+ // from that const method.
+ llvm::SmallDenseMap<
+ const dataflow::RecordStorageLocation *,
+ llvm::SmallDenseMap<const CXXMethodDecl *, dataflow::PointerValue *>>
+ ConstMethodReturnValues;
};
inline std::ostream &operator<<(std::ostream &OS,
diff --git a/nullability/pointer_nullability_matchers.cc b/nullability/pointer_nullability_matchers.cc
index 00b1720..fc3d74c 100644
--- a/nullability/pointer_nullability_matchers.cc
+++ b/nullability/pointer_nullability_matchers.cc
@@ -39,6 +39,7 @@
using ast_matchers::isConst;
using ast_matchers::isMemberInitializer;
using ast_matchers::memberExpr;
+using ast_matchers::parameterCountIs;
using ast_matchers::returnStmt;
using ast_matchers::statementCountIs;
using ast_matchers::unaryOperator;
@@ -75,6 +76,11 @@
return cxxCtorInitializer(isMemberInitializer());
}
+Matcher<Stmt> isZeroParamConstMemberCall() {
+ return cxxMemberCallExpr(
+ callee(cxxMethodDecl(parameterCountIs(0), isConst())));
+}
+
Matcher<Stmt> isNonConstMemberCall() {
return cxxMemberCallExpr(callee(cxxMethodDecl(unless(isConst()))));
}
diff --git a/nullability/pointer_nullability_matchers.h b/nullability/pointer_nullability_matchers.h
index 4e67076..e6641e1 100644
--- a/nullability/pointer_nullability_matchers.h
+++ b/nullability/pointer_nullability_matchers.h
@@ -30,6 +30,7 @@
ast_matchers::internal::Matcher<Stmt> isPointerReturn();
ast_matchers::internal::Matcher<Stmt> isConstructExpr();
ast_matchers::internal::Matcher<CXXCtorInitializer> isCtorMemberInitializer();
+ast_matchers::internal::Matcher<Stmt> isZeroParamConstMemberCall();
ast_matchers::internal::Matcher<Stmt> isNonConstMemberCall();
ast_matchers::internal::Matcher<Stmt> isSupportedPointerAccessorCall();
diff --git a/nullability/pointer_nullability_matchers_test.cc b/nullability/pointer_nullability_matchers_test.cc
index 5ae437b..77c0717 100644
--- a/nullability/pointer_nullability_matchers_test.cc
+++ b/nullability/pointer_nullability_matchers_test.cc
@@ -62,5 +62,28 @@
isSupportedPointerAccessorCall()));
}
+TEST(PointerNullabilityTest, MatchConstMemberFunctions) {
+ llvm::StringRef Input(R"cc(
+ class C {
+ public:
+ int *_Nullable get() const;
+ int *_Nullable get(int i) const;
+ int *_Nullable get_with_default_arg(int i = 0) const;
+ int *_Nullable get_nonconst();
+ };
+ C foo() { return C(); }
+ )cc");
+ EXPECT_TRUE(matches(Input, "void target(){ C().get(); }",
+ isZeroParamConstMemberCall()));
+ EXPECT_TRUE(matches(Input, "void target(){ foo().get(); }",
+ isZeroParamConstMemberCall()));
+
+ EXPECT_FALSE(matches(Input, "void target(){ C().get(0); }",
+ isZeroParamConstMemberCall()));
+ EXPECT_FALSE(matches(Input, "void target(){ C().get_with_default_arg(); }",
+ isZeroParamConstMemberCall()));
+ EXPECT_FALSE(matches(Input, "void target(){ C().get_nonconst(); }",
+ isZeroParamConstMemberCall()));
+}
} // namespace
} // namespace clang::tidy::nullability
diff --git a/nullability/test/function_calls.cc b/nullability/test/function_calls.cc
index 51a5c3b..aadaba8 100644
--- a/nullability/test/function_calls.cc
+++ b/nullability/test/function_calls.cc
@@ -678,6 +678,60 @@
)cc"));
}
+TEST(PointerNullabilityTest, ConstMethodNoImpl) {
+ EXPECT_TRUE(checkDiagnostics(R"cc(
+ struct C {
+ int *_Nullable property() const;
+ void may_mutate();
+ };
+ void target() {
+ C obj;
+ if (obj.property() != nullptr) {
+ obj.may_mutate();
+ *obj.property(); // [[unsafe]]
+ };
+ if (obj.property() != nullptr) *obj.property();
+ }
+ )cc"));
+}
+
+TEST(PointerNullabilityTest, ConstMethodEarlyReturn) {
+ EXPECT_TRUE(checkDiagnostics(R"cc(
+ struct C {
+ int *_Nullable property() const;
+ };
+ void target() {
+ C c;
+ if (!c.property()) return;
+ // No false positive in this case, as there is no join.
+ *c.property();
+ }
+ )cc"));
+}
+
+TEST(PointerNullabilityTest, ConstMethodEarlyReturnWithConditional) {
+ EXPECT_TRUE(checkDiagnostics(R"cc(
+ struct C {
+ int *_Nullable property() const;
+ };
+ bool cond();
+ void some_operation(int);
+ void target() {
+ C c;
+ if (!c.property()) return;
+ if (cond()) {
+ some_operation(1);
+ } else {
+ some_operation(2);
+ }
+ // TODO(b/309667920): False positive below due to clearing the const
+ // method return values on join. If this is a pattern that occurs
+ // frequently in real code, we need to fix this.
+ *c.property(); // [[unsafe]]
+ }
+ )cc"));
+}
+
TEST(PointerNullabilityTest, FieldUndefinedValue) {
EXPECT_TRUE(checkDiagnostics(R"cc(
struct C {