mirror of
https://github.com/llvm/llvm-project.git
synced 2025-05-01 04:56:12 +00:00

Report the range in diagnostics, in addition to the location in case the range helps disambiguate a little in chained `->` expressions. ``` b->a->f->x = 1; ^~~~~~~ ``` instead of just: ``` b->a->f->x = 1; ^ ``` As a followup we should probably also report the location/range of an `->` if that operator is used. Like: ``` b->a->f->x = 1; ^~ ```
1203 lines
48 KiB
C++
1203 lines
48 KiB
C++
//===-- UncheckedOptionalAccessModel.cpp ------------------------*- C++ -*-===//
|
|
//
|
|
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
|
// See https://llvm.org/LICENSE.txt for license information.
|
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This file defines a dataflow analysis that detects unsafe uses of optional
|
|
// values.
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
#include "clang/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.h"
|
|
#include "clang/AST/ASTContext.h"
|
|
#include "clang/AST/DeclCXX.h"
|
|
#include "clang/AST/Expr.h"
|
|
#include "clang/AST/ExprCXX.h"
|
|
#include "clang/AST/Stmt.h"
|
|
#include "clang/AST/Type.h"
|
|
#include "clang/ASTMatchers/ASTMatchers.h"
|
|
#include "clang/ASTMatchers/ASTMatchersMacros.h"
|
|
#include "clang/Analysis/CFG.h"
|
|
#include "clang/Analysis/FlowSensitive/CFGMatchSwitch.h"
|
|
#include "clang/Analysis/FlowSensitive/DataflowEnvironment.h"
|
|
#include "clang/Analysis/FlowSensitive/Formula.h"
|
|
#include "clang/Analysis/FlowSensitive/RecordOps.h"
|
|
#include "clang/Analysis/FlowSensitive/SmartPointerAccessorCaching.h"
|
|
#include "clang/Analysis/FlowSensitive/StorageLocation.h"
|
|
#include "clang/Analysis/FlowSensitive/Value.h"
|
|
#include "clang/Basic/OperatorKinds.h"
|
|
#include "clang/Basic/SourceLocation.h"
|
|
#include "llvm/ADT/StringRef.h"
|
|
#include "llvm/Support/Casting.h"
|
|
#include "llvm/Support/ErrorHandling.h"
|
|
#include <cassert>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <utility>
|
|
|
|
namespace clang {
|
|
namespace dataflow {
|
|
|
|
// Note: the Names appear in reverse order. E.g., to check
|
|
// if NS is foo::bar::, call isFullyQualifiedNamespaceEqualTo(NS, "bar", "foo")
|
|
template <class... NameTypes>
|
|
static bool isFullyQualifiedNamespaceEqualTo(const NamespaceDecl &NS,
|
|
llvm::StringRef Name,
|
|
NameTypes... Names) {
|
|
if (!(NS.getDeclName().isIdentifier() && NS.getName() == Name &&
|
|
NS.getParent() != nullptr))
|
|
return false;
|
|
|
|
if constexpr (sizeof...(NameTypes) > 0) {
|
|
if (NS.getParent()->isTranslationUnit())
|
|
return false;
|
|
if (const auto *NextNS = dyn_cast_or_null<NamespaceDecl>(NS.getParent()))
|
|
return isFullyQualifiedNamespaceEqualTo(*NextNS, Names...);
|
|
return false;
|
|
} else {
|
|
return NS.getParent()->isTranslationUnit();
|
|
}
|
|
}
|
|
|
|
static bool hasOptionalClassName(const CXXRecordDecl &RD) {
|
|
if (!RD.getDeclName().isIdentifier())
|
|
return false;
|
|
|
|
if (RD.getName() == "optional") {
|
|
if (const auto *N = dyn_cast_or_null<NamespaceDecl>(RD.getDeclContext()))
|
|
return N->isStdNamespace() ||
|
|
isFullyQualifiedNamespaceEqualTo(*N, "absl") ||
|
|
isFullyQualifiedNamespaceEqualTo(*N, "bsl");
|
|
return false;
|
|
}
|
|
|
|
if (RD.getName() == "Optional") {
|
|
// Check whether namespace is "::base" or "::folly".
|
|
const auto *N = dyn_cast_or_null<NamespaceDecl>(RD.getDeclContext());
|
|
return N != nullptr && (isFullyQualifiedNamespaceEqualTo(*N, "base") ||
|
|
isFullyQualifiedNamespaceEqualTo(*N, "folly"));
|
|
}
|
|
|
|
if (RD.getName() == "NullableValue") {
|
|
const auto *N = dyn_cast_or_null<NamespaceDecl>(RD.getDeclContext());
|
|
return N != nullptr &&
|
|
isFullyQualifiedNamespaceEqualTo(*N, "bdlb", "BloombergLP");
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static const CXXRecordDecl *getOptionalBaseClass(const CXXRecordDecl *RD) {
|
|
if (RD == nullptr)
|
|
return nullptr;
|
|
if (hasOptionalClassName(*RD))
|
|
return RD;
|
|
|
|
if (!RD->hasDefinition())
|
|
return nullptr;
|
|
|
|
for (const CXXBaseSpecifier &Base : RD->bases())
|
|
if (const CXXRecordDecl *BaseClass =
|
|
getOptionalBaseClass(Base.getType()->getAsCXXRecordDecl()))
|
|
return BaseClass;
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
static bool isSupportedOptionalType(QualType Ty) {
|
|
const CXXRecordDecl *Optional =
|
|
getOptionalBaseClass(Ty->getAsCXXRecordDecl());
|
|
return Optional != nullptr;
|
|
}
|
|
|
|
namespace {
|
|
|
|
using namespace ::clang::ast_matchers;
|
|
|
|
using LatticeTransferState = TransferState<UncheckedOptionalAccessLattice>;
|
|
|
|
AST_MATCHER(CXXRecordDecl, optionalClass) { return hasOptionalClassName(Node); }
|
|
|
|
AST_MATCHER(CXXRecordDecl, optionalOrDerivedClass) {
|
|
return getOptionalBaseClass(&Node) != nullptr;
|
|
}
|
|
|
|
auto desugarsToOptionalType() {
|
|
return hasUnqualifiedDesugaredType(
|
|
recordType(hasDeclaration(cxxRecordDecl(optionalClass()))));
|
|
}
|
|
|
|
auto desugarsToOptionalOrDerivedType() {
|
|
return hasUnqualifiedDesugaredType(
|
|
recordType(hasDeclaration(cxxRecordDecl(optionalOrDerivedClass()))));
|
|
}
|
|
|
|
auto hasOptionalType() { return hasType(desugarsToOptionalType()); }
|
|
|
|
/// Matches any of the spellings of the optional types and sugar, aliases,
|
|
/// derived classes, etc.
|
|
auto hasOptionalOrDerivedType() {
|
|
return hasType(desugarsToOptionalOrDerivedType());
|
|
}
|
|
|
|
QualType getPublicType(const Expr *E) {
|
|
auto *Cast = dyn_cast<ImplicitCastExpr>(E->IgnoreParens());
|
|
if (Cast == nullptr || Cast->getCastKind() != CK_UncheckedDerivedToBase) {
|
|
QualType Ty = E->getType();
|
|
if (Ty->isPointerType())
|
|
return Ty->getPointeeType();
|
|
return Ty;
|
|
}
|
|
|
|
// Is the derived type that we're casting from the type of `*this`? In this
|
|
// special case, we can upcast to the base class even if the base is
|
|
// non-public.
|
|
bool CastingFromThis = isa<CXXThisExpr>(Cast->getSubExpr());
|
|
|
|
// Find the least-derived type in the path (i.e. the last entry in the list)
|
|
// that we can access.
|
|
const CXXBaseSpecifier *PublicBase = nullptr;
|
|
for (const CXXBaseSpecifier *Base : Cast->path()) {
|
|
if (Base->getAccessSpecifier() != AS_public && !CastingFromThis)
|
|
break;
|
|
PublicBase = Base;
|
|
CastingFromThis = false;
|
|
}
|
|
|
|
if (PublicBase != nullptr)
|
|
return PublicBase->getType();
|
|
|
|
// We didn't find any public type that we could cast to. There may be more
|
|
// casts in `getSubExpr()`, so recurse. (If there aren't any more casts, this
|
|
// will return the type of `getSubExpr()`.)
|
|
return getPublicType(Cast->getSubExpr());
|
|
}
|
|
|
|
// Returns the least-derived type for the receiver of `MCE` that
|
|
// `MCE.getImplicitObjectArgument()->IgnoreParentImpCasts()` can be downcast to.
|
|
// Effectively, we upcast until we reach a non-public base class, unless that
|
|
// base is a base of `*this`.
|
|
//
|
|
// This is needed to correctly match methods called on types derived from
|
|
// `std::optional`.
|
|
//
|
|
// Say we have a `struct Derived : public std::optional<int> {} d;` For a call
|
|
// `d.has_value()`, the `getImplicitObjectArgument()` looks like this:
|
|
//
|
|
// ImplicitCastExpr 'const std::__optional_storage_base<int>' lvalue
|
|
// | <UncheckedDerivedToBase (optional -> __optional_storage_base)>
|
|
// `-DeclRefExpr 'Derived' lvalue Var 'd' 'Derived'
|
|
//
|
|
// The type of the implicit object argument is `__optional_storage_base`
|
|
// (since this is the internal type that `has_value()` is declared on). If we
|
|
// call `IgnoreParenImpCasts()` on the implicit object argument, we get the
|
|
// `DeclRefExpr`, which has type `Derived`. Neither of these types is
|
|
// `optional`, and hence neither is sufficient for querying whether we are
|
|
// calling a method on `optional`.
|
|
//
|
|
// Instead, starting with the most derived type, we need to follow the chain of
|
|
// casts
|
|
QualType getPublicReceiverType(const CXXMemberCallExpr &MCE) {
|
|
return getPublicType(MCE.getImplicitObjectArgument());
|
|
}
|
|
|
|
AST_MATCHER_P(CXXMemberCallExpr, publicReceiverType,
|
|
ast_matchers::internal::Matcher<QualType>, InnerMatcher) {
|
|
return InnerMatcher.matches(getPublicReceiverType(Node), Finder, Builder);
|
|
}
|
|
|
|
auto isOptionalMemberCallWithNameMatcher(
|
|
ast_matchers::internal::Matcher<NamedDecl> matcher,
|
|
const std::optional<StatementMatcher> &Ignorable = std::nullopt) {
|
|
return cxxMemberCallExpr(Ignorable ? on(expr(unless(*Ignorable)))
|
|
: anything(),
|
|
publicReceiverType(desugarsToOptionalType()),
|
|
callee(cxxMethodDecl(matcher)));
|
|
}
|
|
|
|
auto isOptionalOperatorCallWithName(
|
|
llvm::StringRef operator_name,
|
|
const std::optional<StatementMatcher> &Ignorable = std::nullopt) {
|
|
return cxxOperatorCallExpr(
|
|
hasOverloadedOperatorName(operator_name),
|
|
callee(cxxMethodDecl(ofClass(optionalClass()))),
|
|
Ignorable ? callExpr(unless(hasArgument(0, *Ignorable))) : callExpr());
|
|
}
|
|
|
|
auto isMakeOptionalCall() {
|
|
return callExpr(
|
|
callee(functionDecl(hasAnyName(
|
|
"std::make_optional", "base::make_optional", "absl::make_optional",
|
|
"folly::make_optional", "bsl::make_optional"))),
|
|
hasOptionalType());
|
|
}
|
|
|
|
auto nulloptTypeDecl() {
|
|
return namedDecl(hasAnyName("std::nullopt_t", "absl::nullopt_t",
|
|
"base::nullopt_t", "folly::None",
|
|
"bsl::nullopt_t"));
|
|
}
|
|
|
|
auto hasNulloptType() { return hasType(nulloptTypeDecl()); }
|
|
|
|
auto inPlaceClass() {
|
|
return recordDecl(hasAnyName("std::in_place_t", "absl::in_place_t",
|
|
"base::in_place_t", "folly::in_place_t",
|
|
"bsl::in_place_t"));
|
|
}
|
|
|
|
auto isOptionalNulloptConstructor() {
|
|
return cxxConstructExpr(
|
|
hasDeclaration(cxxConstructorDecl(parameterCountIs(1),
|
|
hasParameter(0, hasNulloptType()))),
|
|
hasOptionalOrDerivedType());
|
|
}
|
|
|
|
auto isOptionalInPlaceConstructor() {
|
|
return cxxConstructExpr(hasArgument(0, hasType(inPlaceClass())),
|
|
hasOptionalOrDerivedType());
|
|
}
|
|
|
|
auto isOptionalValueOrConversionConstructor() {
|
|
return cxxConstructExpr(
|
|
unless(hasDeclaration(
|
|
cxxConstructorDecl(anyOf(isCopyConstructor(), isMoveConstructor())))),
|
|
argumentCountIs(1), hasArgument(0, unless(hasNulloptType())),
|
|
hasOptionalOrDerivedType());
|
|
}
|
|
|
|
auto isOptionalValueOrConversionAssignment() {
|
|
return cxxOperatorCallExpr(
|
|
hasOverloadedOperatorName("="),
|
|
callee(cxxMethodDecl(ofClass(optionalOrDerivedClass()))),
|
|
unless(hasDeclaration(cxxMethodDecl(
|
|
anyOf(isCopyAssignmentOperator(), isMoveAssignmentOperator())))),
|
|
argumentCountIs(2), hasArgument(1, unless(hasNulloptType())));
|
|
}
|
|
|
|
auto isOptionalNulloptAssignment() {
|
|
return cxxOperatorCallExpr(
|
|
hasOverloadedOperatorName("="),
|
|
callee(cxxMethodDecl(ofClass(optionalOrDerivedClass()))),
|
|
argumentCountIs(2), hasArgument(1, hasNulloptType()));
|
|
}
|
|
|
|
auto isStdSwapCall() {
|
|
return callExpr(callee(functionDecl(hasName("std::swap"))),
|
|
argumentCountIs(2),
|
|
hasArgument(0, hasOptionalOrDerivedType()),
|
|
hasArgument(1, hasOptionalOrDerivedType()));
|
|
}
|
|
|
|
auto isStdForwardCall() {
|
|
return callExpr(callee(functionDecl(hasName("std::forward"))),
|
|
argumentCountIs(1),
|
|
hasArgument(0, hasOptionalOrDerivedType()));
|
|
}
|
|
|
|
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)))));
|
|
}
|
|
|
|
auto isZeroParamConstMemberCall() {
|
|
return cxxMemberCallExpr(
|
|
callee(cxxMethodDecl(parameterCountIs(0), isConst())));
|
|
}
|
|
|
|
auto isZeroParamConstMemberOperatorCall() {
|
|
return cxxOperatorCallExpr(
|
|
callee(cxxMethodDecl(parameterCountIs(0), isConst())));
|
|
}
|
|
|
|
auto isNonConstMemberCall() {
|
|
return cxxMemberCallExpr(callee(cxxMethodDecl(unless(isConst()))));
|
|
}
|
|
|
|
auto isNonConstMemberOperatorCall() {
|
|
return cxxOperatorCallExpr(callee(cxxMethodDecl(unless(isConst()))));
|
|
}
|
|
|
|
auto isCallReturningOptional() {
|
|
return callExpr(hasType(qualType(
|
|
anyOf(desugarsToOptionalOrDerivedType(),
|
|
referenceType(pointee(desugarsToOptionalOrDerivedType()))))));
|
|
}
|
|
|
|
template <typename L, typename R>
|
|
auto isComparisonOperatorCall(L lhs_arg_matcher, R rhs_arg_matcher) {
|
|
return cxxOperatorCallExpr(
|
|
anyOf(hasOverloadedOperatorName("=="), hasOverloadedOperatorName("!=")),
|
|
argumentCountIs(2), hasArgument(0, lhs_arg_matcher),
|
|
hasArgument(1, rhs_arg_matcher));
|
|
}
|
|
|
|
/// Ensures that `Expr` is mapped to a `BoolValue` and returns its formula.
|
|
const Formula &forceBoolValue(Environment &Env, const Expr &Expr) {
|
|
auto *Value = Env.get<BoolValue>(Expr);
|
|
if (Value != nullptr)
|
|
return Value->formula();
|
|
|
|
Value = &Env.makeAtomicBoolValue();
|
|
Env.setValue(Expr, *Value);
|
|
return Value->formula();
|
|
}
|
|
|
|
StorageLocation &locForHasValue(const RecordStorageLocation &OptionalLoc) {
|
|
return OptionalLoc.getSyntheticField("has_value");
|
|
}
|
|
|
|
StorageLocation &locForValue(const RecordStorageLocation &OptionalLoc) {
|
|
return OptionalLoc.getSyntheticField("value");
|
|
}
|
|
|
|
/// Sets `HasValueVal` as the symbolic value that represents the "has_value"
|
|
/// property of the optional at `OptionalLoc`.
|
|
void setHasValue(RecordStorageLocation &OptionalLoc, BoolValue &HasValueVal,
|
|
Environment &Env) {
|
|
Env.setValue(locForHasValue(OptionalLoc), HasValueVal);
|
|
}
|
|
|
|
/// Returns the symbolic value that represents the "has_value" property of the
|
|
/// optional at `OptionalLoc`. Returns null if `OptionalLoc` is null.
|
|
BoolValue *getHasValue(Environment &Env, RecordStorageLocation *OptionalLoc) {
|
|
if (OptionalLoc == nullptr)
|
|
return nullptr;
|
|
StorageLocation &HasValueLoc = locForHasValue(*OptionalLoc);
|
|
auto *HasValueVal = Env.get<BoolValue>(HasValueLoc);
|
|
if (HasValueVal == nullptr) {
|
|
HasValueVal = &Env.makeAtomicBoolValue();
|
|
Env.setValue(HasValueLoc, *HasValueVal);
|
|
}
|
|
return HasValueVal;
|
|
}
|
|
|
|
QualType valueTypeFromOptionalDecl(const CXXRecordDecl &RD) {
|
|
auto &CTSD = cast<ClassTemplateSpecializationDecl>(RD);
|
|
return CTSD.getTemplateArgs()[0].getAsType();
|
|
}
|
|
|
|
/// Returns the number of optional wrappers in `Type`.
|
|
///
|
|
/// For example, if `Type` is `optional<optional<int>>`, the result of this
|
|
/// function will be 2.
|
|
int countOptionalWrappers(const ASTContext &ASTCtx, QualType Type) {
|
|
const CXXRecordDecl *Optional =
|
|
getOptionalBaseClass(Type->getAsCXXRecordDecl());
|
|
if (Optional == nullptr)
|
|
return 0;
|
|
return 1 + countOptionalWrappers(
|
|
ASTCtx,
|
|
valueTypeFromOptionalDecl(*Optional).getDesugaredType(ASTCtx));
|
|
}
|
|
|
|
StorageLocation *getLocBehindPossiblePointer(const Expr &E,
|
|
const Environment &Env) {
|
|
if (E.isPRValue()) {
|
|
if (auto *PointerVal = dyn_cast_or_null<PointerValue>(Env.getValue(E)))
|
|
return &PointerVal->getPointeeLoc();
|
|
return nullptr;
|
|
}
|
|
return Env.getStorageLocation(E);
|
|
}
|
|
|
|
void transferUnwrapCall(const Expr *UnwrapExpr, const Expr *ObjectExpr,
|
|
LatticeTransferState &State) {
|
|
if (auto *OptionalLoc = cast_or_null<RecordStorageLocation>(
|
|
getLocBehindPossiblePointer(*ObjectExpr, State.Env))) {
|
|
if (State.Env.getStorageLocation(*UnwrapExpr) == nullptr)
|
|
State.Env.setStorageLocation(*UnwrapExpr, locForValue(*OptionalLoc));
|
|
}
|
|
}
|
|
|
|
void transferArrowOpCall(const Expr *UnwrapExpr, const Expr *ObjectExpr,
|
|
LatticeTransferState &State) {
|
|
if (auto *OptionalLoc = cast_or_null<RecordStorageLocation>(
|
|
getLocBehindPossiblePointer(*ObjectExpr, State.Env)))
|
|
State.Env.setValue(
|
|
*UnwrapExpr, State.Env.create<PointerValue>(locForValue(*OptionalLoc)));
|
|
}
|
|
|
|
void transferMakeOptionalCall(const CallExpr *E,
|
|
const MatchFinder::MatchResult &,
|
|
LatticeTransferState &State) {
|
|
setHasValue(State.Env.getResultObjectLocation(*E),
|
|
State.Env.getBoolLiteralValue(true), State.Env);
|
|
}
|
|
|
|
void transferOptionalHasValueCall(const CXXMemberCallExpr *CallExpr,
|
|
const MatchFinder::MatchResult &,
|
|
LatticeTransferState &State) {
|
|
if (auto *HasValueVal = getHasValue(
|
|
State.Env, getImplicitObjectLocation(*CallExpr, State.Env))) {
|
|
State.Env.setValue(*CallExpr, *HasValueVal);
|
|
}
|
|
}
|
|
|
|
void transferOptionalIsNullCall(const CXXMemberCallExpr *CallExpr,
|
|
const MatchFinder::MatchResult &,
|
|
LatticeTransferState &State) {
|
|
if (auto *HasValueVal = getHasValue(
|
|
State.Env, getImplicitObjectLocation(*CallExpr, State.Env))) {
|
|
State.Env.setValue(*CallExpr, State.Env.makeNot(*HasValueVal));
|
|
}
|
|
}
|
|
|
|
/// `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,
|
|
const Formula &(*ModelPred)(Environment &Env, const Formula &ExprVal,
|
|
const Formula &HasValueVal)) {
|
|
auto &Env = State.Env;
|
|
|
|
const auto *MCE =
|
|
Result.Nodes.getNodeAs<clang::CXXMemberCallExpr>(ValueOrCallID);
|
|
|
|
auto *HasValueVal =
|
|
getHasValue(State.Env, getImplicitObjectLocation(*MCE, State.Env));
|
|
if (HasValueVal == nullptr)
|
|
return;
|
|
|
|
Env.assume(ModelPred(Env, forceBoolValue(Env, *ValueOrPredExpr),
|
|
HasValueVal->formula()));
|
|
}
|
|
|
|
void transferValueOrStringEmptyCall(const clang::Expr *ComparisonExpr,
|
|
const MatchFinder::MatchResult &Result,
|
|
LatticeTransferState &State) {
|
|
return transferValueOrImpl(ComparisonExpr, Result, State,
|
|
[](Environment &Env, const Formula &ExprVal,
|
|
const Formula &HasValueVal) -> const Formula & {
|
|
auto &A = Env.arena();
|
|
// 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 A.makeImplies(A.makeNot(ExprVal),
|
|
HasValueVal);
|
|
});
|
|
}
|
|
|
|
void transferValueOrNotEqX(const Expr *ComparisonExpr,
|
|
const MatchFinder::MatchResult &Result,
|
|
LatticeTransferState &State) {
|
|
transferValueOrImpl(ComparisonExpr, Result, State,
|
|
[](Environment &Env, const Formula &ExprVal,
|
|
const Formula &HasValueVal) -> const Formula & {
|
|
auto &A = Env.arena();
|
|
// We know that if `(opt.value_or(X) != X)` then
|
|
// `opt.hasValue()`, even without knowing further
|
|
// details about the contents of `opt`.
|
|
return A.makeImplies(ExprVal, HasValueVal);
|
|
});
|
|
}
|
|
|
|
void transferCallReturningOptional(const CallExpr *E,
|
|
const MatchFinder::MatchResult &Result,
|
|
LatticeTransferState &State) {
|
|
RecordStorageLocation *Loc = nullptr;
|
|
if (E->isPRValue()) {
|
|
Loc = &State.Env.getResultObjectLocation(*E);
|
|
} else {
|
|
Loc = State.Env.get<RecordStorageLocation>(*E);
|
|
if (Loc == nullptr) {
|
|
Loc = &cast<RecordStorageLocation>(State.Env.createStorageLocation(*E));
|
|
State.Env.setStorageLocation(*E, *Loc);
|
|
}
|
|
}
|
|
|
|
if (State.Env.getValue(locForHasValue(*Loc)) != nullptr)
|
|
return;
|
|
|
|
setHasValue(*Loc, State.Env.makeAtomicBoolValue(), State.Env);
|
|
}
|
|
|
|
// Returns true if the const accessor is handled by caching.
|
|
// Returns false if we could not cache. We should perform default handling
|
|
// in that case.
|
|
bool handleConstMemberCall(const CallExpr *CE,
|
|
dataflow::RecordStorageLocation *RecordLoc,
|
|
const MatchFinder::MatchResult &Result,
|
|
LatticeTransferState &State) {
|
|
if (RecordLoc == nullptr)
|
|
return false;
|
|
|
|
// Cache if the const method returns a reference.
|
|
if (CE->isGLValue()) {
|
|
const FunctionDecl *DirectCallee = CE->getDirectCallee();
|
|
if (DirectCallee == nullptr)
|
|
return false;
|
|
|
|
// Initialize the optional's "has_value" property to true if the type is
|
|
// optional, otherwise no-op. If we want to support const ref to pointers or
|
|
// bools we should initialize their values here too.
|
|
auto Init = [&](StorageLocation &Loc) {
|
|
if (isSupportedOptionalType(CE->getType()))
|
|
setHasValue(cast<RecordStorageLocation>(Loc),
|
|
State.Env.makeAtomicBoolValue(), State.Env);
|
|
};
|
|
StorageLocation &Loc =
|
|
State.Lattice.getOrCreateConstMethodReturnStorageLocation(
|
|
*RecordLoc, DirectCallee, State.Env, Init);
|
|
|
|
State.Env.setStorageLocation(*CE, Loc);
|
|
return true;
|
|
}
|
|
// PRValue cases:
|
|
if (CE->getType()->isBooleanType() || CE->getType()->isPointerType()) {
|
|
// If the const method returns a boolean or pointer type.
|
|
Value *Val = State.Lattice.getOrCreateConstMethodReturnValue(*RecordLoc, CE,
|
|
State.Env);
|
|
if (Val == nullptr)
|
|
return false;
|
|
State.Env.setValue(*CE, *Val);
|
|
return true;
|
|
}
|
|
if (isSupportedOptionalType(CE->getType())) {
|
|
// If the const method returns an optional by value.
|
|
const FunctionDecl *DirectCallee = CE->getDirectCallee();
|
|
if (DirectCallee == nullptr)
|
|
return false;
|
|
StorageLocation &Loc =
|
|
State.Lattice.getOrCreateConstMethodReturnStorageLocation(
|
|
*RecordLoc, DirectCallee, State.Env, [&](StorageLocation &Loc) {
|
|
setHasValue(cast<RecordStorageLocation>(Loc),
|
|
State.Env.makeAtomicBoolValue(), State.Env);
|
|
});
|
|
// Use copyRecord to link the optional to the result object of the call
|
|
// expression.
|
|
auto &ResultLoc = State.Env.getResultObjectLocation(*CE);
|
|
copyRecord(cast<RecordStorageLocation>(Loc), ResultLoc, State.Env);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void handleConstMemberCallWithFallbacks(
|
|
const CallExpr *CE, dataflow::RecordStorageLocation *RecordLoc,
|
|
const MatchFinder::MatchResult &Result, LatticeTransferState &State) {
|
|
if (handleConstMemberCall(CE, RecordLoc, Result, State))
|
|
return;
|
|
// Perform default handling if the call returns an optional, but wasn't
|
|
// handled by caching.
|
|
if (isSupportedOptionalType(CE->getType()))
|
|
transferCallReturningOptional(CE, Result, State);
|
|
}
|
|
|
|
void transferConstMemberCall(const CXXMemberCallExpr *MCE,
|
|
const MatchFinder::MatchResult &Result,
|
|
LatticeTransferState &State) {
|
|
handleConstMemberCallWithFallbacks(
|
|
MCE, dataflow::getImplicitObjectLocation(*MCE, State.Env), Result, State);
|
|
}
|
|
|
|
void transferConstMemberOperatorCall(const CXXOperatorCallExpr *OCE,
|
|
const MatchFinder::MatchResult &Result,
|
|
LatticeTransferState &State) {
|
|
auto *RecordLoc = cast_or_null<dataflow::RecordStorageLocation>(
|
|
State.Env.getStorageLocation(*OCE->getArg(0)));
|
|
handleConstMemberCallWithFallbacks(OCE, RecordLoc, Result, State);
|
|
}
|
|
|
|
void handleNonConstMemberCall(const CallExpr *CE,
|
|
dataflow::RecordStorageLocation *RecordLoc,
|
|
const MatchFinder::MatchResult &Result,
|
|
LatticeTransferState &State) {
|
|
if (RecordLoc != nullptr) {
|
|
// When a non-const member function is called, clear all (non-const)
|
|
// optional fields of the receiver. Const-qualified fields can't be
|
|
// changed (at least, not without UB).
|
|
for (const auto &[Field, FieldLoc] : RecordLoc->children()) {
|
|
QualType FieldType = Field->getType();
|
|
if (!FieldType.isConstQualified() &&
|
|
isSupportedOptionalType(Field->getType())) {
|
|
auto *FieldRecordLoc = cast_or_null<RecordStorageLocation>(FieldLoc);
|
|
if (FieldRecordLoc) {
|
|
setHasValue(*FieldRecordLoc, State.Env.makeAtomicBoolValue(),
|
|
State.Env);
|
|
}
|
|
}
|
|
}
|
|
State.Lattice.clearConstMethodReturnValues(*RecordLoc);
|
|
State.Lattice.clearConstMethodReturnStorageLocations(*RecordLoc);
|
|
}
|
|
|
|
// Perform default handling if the call returns an optional.
|
|
if (isSupportedOptionalType(CE->getType())) {
|
|
transferCallReturningOptional(CE, Result, State);
|
|
}
|
|
}
|
|
|
|
void transferValue_NonConstMemberCall(const CXXMemberCallExpr *MCE,
|
|
const MatchFinder::MatchResult &Result,
|
|
LatticeTransferState &State) {
|
|
handleNonConstMemberCall(
|
|
MCE, dataflow::getImplicitObjectLocation(*MCE, State.Env), Result, State);
|
|
}
|
|
|
|
void transferValue_NonConstMemberOperatorCall(
|
|
const CXXOperatorCallExpr *OCE, const MatchFinder::MatchResult &Result,
|
|
LatticeTransferState &State) {
|
|
auto *RecordLoc = cast_or_null<dataflow::RecordStorageLocation>(
|
|
State.Env.getStorageLocation(*OCE->getArg(0)));
|
|
handleNonConstMemberCall(OCE, RecordLoc, Result, State);
|
|
}
|
|
|
|
void constructOptionalValue(const Expr &E, Environment &Env,
|
|
BoolValue &HasValueVal) {
|
|
RecordStorageLocation &Loc = Env.getResultObjectLocation(E);
|
|
setHasValue(Loc, HasValueVal, Env);
|
|
}
|
|
|
|
/// Returns a symbolic value for the "has_value" property of an `optional<T>`
|
|
/// value that is constructed/assigned from a value of type `U` or `optional<U>`
|
|
/// where `T` is constructible from `U`.
|
|
BoolValue &valueOrConversionHasValue(QualType DestType, const Expr &E,
|
|
const MatchFinder::MatchResult &MatchRes,
|
|
LatticeTransferState &State) {
|
|
const int DestTypeOptionalWrappersCount =
|
|
countOptionalWrappers(*MatchRes.Context, DestType);
|
|
const int ArgTypeOptionalWrappersCount = countOptionalWrappers(
|
|
*MatchRes.Context, E.getType().getNonReferenceType());
|
|
|
|
// Is this an constructor of the form `template<class U> optional(U &&)` /
|
|
// assignment of the form `template<class U> optional& operator=(U &&)`
|
|
// (where `T` is assignable / constructible from `U`)?
|
|
// We recognize this because the number of optionals in the optional being
|
|
// assigned to is different from the function argument type.
|
|
if (DestTypeOptionalWrappersCount != ArgTypeOptionalWrappersCount)
|
|
return State.Env.getBoolLiteralValue(true);
|
|
|
|
// Otherwise, this must be a constructor of the form
|
|
// `template <class U> optional<optional<U> &&)` / assignment of the form
|
|
// `template <class U> optional& operator=(optional<U> &&)
|
|
// (where, again, `T` is assignable / constructible from `U`).
|
|
auto *Loc = State.Env.get<RecordStorageLocation>(E);
|
|
if (auto *HasValueVal = getHasValue(State.Env, Loc))
|
|
return *HasValueVal;
|
|
return State.Env.makeAtomicBoolValue();
|
|
}
|
|
|
|
void transferValueOrConversionConstructor(
|
|
const CXXConstructExpr *E, const MatchFinder::MatchResult &MatchRes,
|
|
LatticeTransferState &State) {
|
|
assert(E->getNumArgs() > 0);
|
|
|
|
constructOptionalValue(
|
|
*E, State.Env,
|
|
valueOrConversionHasValue(
|
|
E->getConstructor()->getThisType()->getPointeeType(), *E->getArg(0),
|
|
MatchRes, State));
|
|
}
|
|
|
|
void transferAssignment(const CXXOperatorCallExpr *E, BoolValue &HasValueVal,
|
|
LatticeTransferState &State) {
|
|
assert(E->getNumArgs() > 0);
|
|
|
|
if (auto *Loc = State.Env.get<RecordStorageLocation>(*E->getArg(0))) {
|
|
setHasValue(*Loc, HasValueVal, State.Env);
|
|
|
|
// Assign a storage location for the whole expression.
|
|
State.Env.setStorageLocation(*E, *Loc);
|
|
}
|
|
}
|
|
|
|
void transferValueOrConversionAssignment(
|
|
const CXXOperatorCallExpr *E, const MatchFinder::MatchResult &MatchRes,
|
|
LatticeTransferState &State) {
|
|
assert(E->getNumArgs() > 1);
|
|
transferAssignment(
|
|
E,
|
|
valueOrConversionHasValue(E->getArg(0)->getType().getNonReferenceType(),
|
|
*E->getArg(1), MatchRes, State),
|
|
State);
|
|
}
|
|
|
|
void transferNulloptAssignment(const CXXOperatorCallExpr *E,
|
|
const MatchFinder::MatchResult &,
|
|
LatticeTransferState &State) {
|
|
transferAssignment(E, State.Env.getBoolLiteralValue(false), State);
|
|
}
|
|
|
|
void transferSwap(RecordStorageLocation *Loc1, RecordStorageLocation *Loc2,
|
|
Environment &Env) {
|
|
// We account for cases where one or both of the optionals are not modeled,
|
|
// either lacking associated storage locations, or lacking values associated
|
|
// to such storage locations.
|
|
|
|
if (Loc1 == nullptr) {
|
|
if (Loc2 != nullptr)
|
|
setHasValue(*Loc2, Env.makeAtomicBoolValue(), Env);
|
|
return;
|
|
}
|
|
if (Loc2 == nullptr) {
|
|
setHasValue(*Loc1, Env.makeAtomicBoolValue(), Env);
|
|
return;
|
|
}
|
|
|
|
// Both expressions have locations, though they may not have corresponding
|
|
// values. In that case, we create a fresh value at this point. Note that if
|
|
// two branches both do this, they will not share the value, but it at least
|
|
// allows for local reasoning about the value. To avoid the above, we would
|
|
// need *lazy* value allocation.
|
|
// FIXME: allocate values lazily, instead of just creating a fresh value.
|
|
BoolValue *BoolVal1 = getHasValue(Env, Loc1);
|
|
if (BoolVal1 == nullptr)
|
|
BoolVal1 = &Env.makeAtomicBoolValue();
|
|
|
|
BoolValue *BoolVal2 = getHasValue(Env, Loc2);
|
|
if (BoolVal2 == nullptr)
|
|
BoolVal2 = &Env.makeAtomicBoolValue();
|
|
|
|
setHasValue(*Loc1, *BoolVal2, Env);
|
|
setHasValue(*Loc2, *BoolVal1, Env);
|
|
}
|
|
|
|
void transferSwapCall(const CXXMemberCallExpr *E,
|
|
const MatchFinder::MatchResult &,
|
|
LatticeTransferState &State) {
|
|
assert(E->getNumArgs() == 1);
|
|
auto *OtherLoc = State.Env.get<RecordStorageLocation>(*E->getArg(0));
|
|
transferSwap(getImplicitObjectLocation(*E, State.Env), OtherLoc, State.Env);
|
|
}
|
|
|
|
void transferStdSwapCall(const CallExpr *E, const MatchFinder::MatchResult &,
|
|
LatticeTransferState &State) {
|
|
assert(E->getNumArgs() == 2);
|
|
auto *Arg0Loc = State.Env.get<RecordStorageLocation>(*E->getArg(0));
|
|
auto *Arg1Loc = State.Env.get<RecordStorageLocation>(*E->getArg(1));
|
|
transferSwap(Arg0Loc, Arg1Loc, State.Env);
|
|
}
|
|
|
|
void transferStdForwardCall(const CallExpr *E, const MatchFinder::MatchResult &,
|
|
LatticeTransferState &State) {
|
|
assert(E->getNumArgs() == 1);
|
|
|
|
if (auto *Loc = State.Env.getStorageLocation(*E->getArg(0)))
|
|
State.Env.setStorageLocation(*E, *Loc);
|
|
}
|
|
|
|
const Formula &evaluateEquality(Arena &A, const Formula &EqVal,
|
|
const Formula &LHS, const Formula &RHS) {
|
|
// Logically, an optional<T> object is composed of two values - a `has_value`
|
|
// bit and a value of type T. Equality of optional objects compares both
|
|
// values. Therefore, merely comparing the `has_value` bits isn't sufficient:
|
|
// when two optional objects are engaged, the equality of their respective
|
|
// values of type T matters. Since we only track the `has_value` bits, we
|
|
// can't make any conclusions about equality when we know that two optional
|
|
// objects are engaged.
|
|
//
|
|
// We express this as two facts about the equality:
|
|
// a) EqVal => (LHS & RHS) v (!RHS & !LHS)
|
|
// If they are equal, then either both are set or both are unset.
|
|
// b) (!LHS & !RHS) => EqVal
|
|
// If neither is set, then they are equal.
|
|
// We rewrite b) as !EqVal => (LHS v RHS), for a more compact formula.
|
|
return A.makeAnd(
|
|
A.makeImplies(EqVal, A.makeOr(A.makeAnd(LHS, RHS),
|
|
A.makeAnd(A.makeNot(LHS), A.makeNot(RHS)))),
|
|
A.makeImplies(A.makeNot(EqVal), A.makeOr(LHS, RHS)));
|
|
}
|
|
|
|
void transferOptionalAndOptionalCmp(const clang::CXXOperatorCallExpr *CmpExpr,
|
|
const MatchFinder::MatchResult &,
|
|
LatticeTransferState &State) {
|
|
Environment &Env = State.Env;
|
|
auto &A = Env.arena();
|
|
auto *CmpValue = &forceBoolValue(Env, *CmpExpr);
|
|
auto *Arg0Loc = Env.get<RecordStorageLocation>(*CmpExpr->getArg(0));
|
|
if (auto *LHasVal = getHasValue(Env, Arg0Loc)) {
|
|
auto *Arg1Loc = Env.get<RecordStorageLocation>(*CmpExpr->getArg(1));
|
|
if (auto *RHasVal = getHasValue(Env, Arg1Loc)) {
|
|
if (CmpExpr->getOperator() == clang::OO_ExclaimEqual)
|
|
CmpValue = &A.makeNot(*CmpValue);
|
|
Env.assume(evaluateEquality(A, *CmpValue, LHasVal->formula(),
|
|
RHasVal->formula()));
|
|
}
|
|
}
|
|
}
|
|
|
|
void transferOptionalAndValueCmp(const clang::CXXOperatorCallExpr *CmpExpr,
|
|
const clang::Expr *E, Environment &Env) {
|
|
auto &A = Env.arena();
|
|
auto *CmpValue = &forceBoolValue(Env, *CmpExpr);
|
|
auto *Loc = Env.get<RecordStorageLocation>(*E);
|
|
if (auto *HasVal = getHasValue(Env, Loc)) {
|
|
if (CmpExpr->getOperator() == clang::OO_ExclaimEqual)
|
|
CmpValue = &A.makeNot(*CmpValue);
|
|
Env.assume(
|
|
evaluateEquality(A, *CmpValue, HasVal->formula(), A.makeLiteral(true)));
|
|
}
|
|
}
|
|
|
|
void transferOptionalAndNulloptCmp(const clang::CXXOperatorCallExpr *CmpExpr,
|
|
const clang::Expr *E, Environment &Env) {
|
|
auto &A = Env.arena();
|
|
auto *CmpValue = &forceBoolValue(Env, *CmpExpr);
|
|
auto *Loc = Env.get<RecordStorageLocation>(*E);
|
|
if (auto *HasVal = getHasValue(Env, Loc)) {
|
|
if (CmpExpr->getOperator() == clang::OO_ExclaimEqual)
|
|
CmpValue = &A.makeNot(*CmpValue);
|
|
Env.assume(evaluateEquality(A, *CmpValue, HasVal->formula(),
|
|
A.makeLiteral(false)));
|
|
}
|
|
}
|
|
|
|
std::optional<StatementMatcher>
|
|
ignorableOptional(const UncheckedOptionalAccessModelOptions &Options) {
|
|
if (Options.IgnoreSmartPointerDereference) {
|
|
auto SmartPtrUse = expr(ignoringParenImpCasts(cxxOperatorCallExpr(
|
|
anyOf(hasOverloadedOperatorName("->"), hasOverloadedOperatorName("*")),
|
|
unless(hasArgument(0, expr(hasOptionalType()))))));
|
|
return expr(
|
|
anyOf(SmartPtrUse, memberExpr(hasObjectExpression(SmartPtrUse))));
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
StatementMatcher
|
|
valueCall(const std::optional<StatementMatcher> &IgnorableOptional) {
|
|
return isOptionalMemberCallWithNameMatcher(hasName("value"),
|
|
IgnorableOptional);
|
|
}
|
|
|
|
StatementMatcher
|
|
valueOperatorCall(const std::optional<StatementMatcher> &IgnorableOptional) {
|
|
return expr(anyOf(isOptionalOperatorCallWithName("*", IgnorableOptional),
|
|
isOptionalOperatorCallWithName("->", IgnorableOptional)));
|
|
}
|
|
|
|
auto buildTransferMatchSwitch() {
|
|
// FIXME: Evaluate the efficiency of matchers. If using matchers results in a
|
|
// lot of duplicated work (e.g. string comparisons), consider providing APIs
|
|
// that avoid it through memoization.
|
|
return CFGMatchSwitchBuilder<LatticeTransferState>()
|
|
// make_optional
|
|
.CaseOfCFGStmt<CallExpr>(isMakeOptionalCall(), transferMakeOptionalCall)
|
|
|
|
// optional::optional (in place)
|
|
.CaseOfCFGStmt<CXXConstructExpr>(
|
|
isOptionalInPlaceConstructor(),
|
|
[](const CXXConstructExpr *E, const MatchFinder::MatchResult &,
|
|
LatticeTransferState &State) {
|
|
constructOptionalValue(*E, State.Env,
|
|
State.Env.getBoolLiteralValue(true));
|
|
})
|
|
// optional::optional(nullopt_t)
|
|
.CaseOfCFGStmt<CXXConstructExpr>(
|
|
isOptionalNulloptConstructor(),
|
|
[](const CXXConstructExpr *E, const MatchFinder::MatchResult &,
|
|
LatticeTransferState &State) {
|
|
constructOptionalValue(*E, State.Env,
|
|
State.Env.getBoolLiteralValue(false));
|
|
})
|
|
// optional::optional (value/conversion)
|
|
.CaseOfCFGStmt<CXXConstructExpr>(isOptionalValueOrConversionConstructor(),
|
|
transferValueOrConversionConstructor)
|
|
|
|
// optional::operator=
|
|
.CaseOfCFGStmt<CXXOperatorCallExpr>(
|
|
isOptionalValueOrConversionAssignment(),
|
|
transferValueOrConversionAssignment)
|
|
.CaseOfCFGStmt<CXXOperatorCallExpr>(isOptionalNulloptAssignment(),
|
|
transferNulloptAssignment)
|
|
|
|
// optional::value
|
|
.CaseOfCFGStmt<CXXMemberCallExpr>(
|
|
valueCall(std::nullopt),
|
|
[](const CXXMemberCallExpr *E, const MatchFinder::MatchResult &,
|
|
LatticeTransferState &State) {
|
|
transferUnwrapCall(E, E->getImplicitObjectArgument(), State);
|
|
})
|
|
|
|
// optional::operator*
|
|
.CaseOfCFGStmt<CallExpr>(isOptionalOperatorCallWithName("*"),
|
|
[](const CallExpr *E,
|
|
const MatchFinder::MatchResult &,
|
|
LatticeTransferState &State) {
|
|
transferUnwrapCall(E, E->getArg(0), State);
|
|
})
|
|
|
|
// optional::operator->
|
|
.CaseOfCFGStmt<CallExpr>(isOptionalOperatorCallWithName("->"),
|
|
[](const CallExpr *E,
|
|
const MatchFinder::MatchResult &,
|
|
LatticeTransferState &State) {
|
|
transferArrowOpCall(E, E->getArg(0), State);
|
|
})
|
|
|
|
// optional::has_value, optional::hasValue
|
|
// Of the supported optionals only folly::Optional uses hasValue, but this
|
|
// will also pass for other types
|
|
.CaseOfCFGStmt<CXXMemberCallExpr>(
|
|
isOptionalMemberCallWithNameMatcher(
|
|
hasAnyName("has_value", "hasValue")),
|
|
transferOptionalHasValueCall)
|
|
|
|
// optional::operator bool
|
|
.CaseOfCFGStmt<CXXMemberCallExpr>(
|
|
isOptionalMemberCallWithNameMatcher(hasName("operator bool")),
|
|
transferOptionalHasValueCall)
|
|
|
|
// NullableValue::isNull
|
|
// Only NullableValue has isNull
|
|
.CaseOfCFGStmt<CXXMemberCallExpr>(
|
|
isOptionalMemberCallWithNameMatcher(hasName("isNull")),
|
|
transferOptionalIsNullCall)
|
|
|
|
// optional::emplace
|
|
.CaseOfCFGStmt<CXXMemberCallExpr>(
|
|
isOptionalMemberCallWithNameMatcher(hasName("emplace")),
|
|
[](const CXXMemberCallExpr *E, const MatchFinder::MatchResult &,
|
|
LatticeTransferState &State) {
|
|
if (RecordStorageLocation *Loc =
|
|
getImplicitObjectLocation(*E, State.Env)) {
|
|
setHasValue(*Loc, State.Env.getBoolLiteralValue(true), State.Env);
|
|
}
|
|
})
|
|
|
|
// optional::reset
|
|
.CaseOfCFGStmt<CXXMemberCallExpr>(
|
|
isOptionalMemberCallWithNameMatcher(hasName("reset")),
|
|
[](const CXXMemberCallExpr *E, const MatchFinder::MatchResult &,
|
|
LatticeTransferState &State) {
|
|
if (RecordStorageLocation *Loc =
|
|
getImplicitObjectLocation(*E, State.Env)) {
|
|
setHasValue(*Loc, State.Env.getBoolLiteralValue(false),
|
|
State.Env);
|
|
}
|
|
})
|
|
|
|
// optional::swap
|
|
.CaseOfCFGStmt<CXXMemberCallExpr>(
|
|
isOptionalMemberCallWithNameMatcher(hasName("swap")),
|
|
transferSwapCall)
|
|
|
|
// std::swap
|
|
.CaseOfCFGStmt<CallExpr>(isStdSwapCall(), transferStdSwapCall)
|
|
|
|
// std::forward
|
|
.CaseOfCFGStmt<CallExpr>(isStdForwardCall(), transferStdForwardCall)
|
|
|
|
// opt.value_or("").empty()
|
|
.CaseOfCFGStmt<Expr>(isValueOrStringEmptyCall(),
|
|
transferValueOrStringEmptyCall)
|
|
|
|
// opt.value_or(X) != X
|
|
.CaseOfCFGStmt<Expr>(isValueOrNotEqX(), transferValueOrNotEqX)
|
|
|
|
// Comparisons (==, !=):
|
|
.CaseOfCFGStmt<CXXOperatorCallExpr>(
|
|
isComparisonOperatorCall(hasOptionalType(), hasOptionalType()),
|
|
transferOptionalAndOptionalCmp)
|
|
.CaseOfCFGStmt<CXXOperatorCallExpr>(
|
|
isComparisonOperatorCall(hasOptionalType(), hasNulloptType()),
|
|
[](const clang::CXXOperatorCallExpr *Cmp,
|
|
const MatchFinder::MatchResult &, LatticeTransferState &State) {
|
|
transferOptionalAndNulloptCmp(Cmp, Cmp->getArg(0), State.Env);
|
|
})
|
|
.CaseOfCFGStmt<CXXOperatorCallExpr>(
|
|
isComparisonOperatorCall(hasNulloptType(), hasOptionalType()),
|
|
[](const clang::CXXOperatorCallExpr *Cmp,
|
|
const MatchFinder::MatchResult &, LatticeTransferState &State) {
|
|
transferOptionalAndNulloptCmp(Cmp, Cmp->getArg(1), State.Env);
|
|
})
|
|
.CaseOfCFGStmt<CXXOperatorCallExpr>(
|
|
isComparisonOperatorCall(
|
|
hasOptionalType(),
|
|
unless(anyOf(hasOptionalType(), hasNulloptType()))),
|
|
[](const clang::CXXOperatorCallExpr *Cmp,
|
|
const MatchFinder::MatchResult &, LatticeTransferState &State) {
|
|
transferOptionalAndValueCmp(Cmp, Cmp->getArg(0), State.Env);
|
|
})
|
|
.CaseOfCFGStmt<CXXOperatorCallExpr>(
|
|
isComparisonOperatorCall(
|
|
unless(anyOf(hasOptionalType(), hasNulloptType())),
|
|
hasOptionalType()),
|
|
[](const clang::CXXOperatorCallExpr *Cmp,
|
|
const MatchFinder::MatchResult &, LatticeTransferState &State) {
|
|
transferOptionalAndValueCmp(Cmp, Cmp->getArg(1), State.Env);
|
|
})
|
|
|
|
// Smart-pointer-like operator* and operator-> calls that may look like
|
|
// const accessors (below) but need special handling to allow mixing
|
|
// the accessor calls.
|
|
.CaseOfCFGStmt<CXXOperatorCallExpr>(
|
|
isSmartPointerLikeOperatorStar(),
|
|
[](const CXXOperatorCallExpr *E,
|
|
const MatchFinder::MatchResult &Result,
|
|
LatticeTransferState &State) {
|
|
transferSmartPointerLikeCachedDeref(
|
|
E,
|
|
dyn_cast_or_null<RecordStorageLocation>(
|
|
getLocBehindPossiblePointer(*E->getArg(0), State.Env)),
|
|
State, [](StorageLocation &Loc) {});
|
|
})
|
|
.CaseOfCFGStmt<CXXOperatorCallExpr>(
|
|
isSmartPointerLikeOperatorArrow(),
|
|
[](const CXXOperatorCallExpr *E,
|
|
const MatchFinder::MatchResult &Result,
|
|
LatticeTransferState &State) {
|
|
transferSmartPointerLikeCachedGet(
|
|
E,
|
|
dyn_cast_or_null<RecordStorageLocation>(
|
|
getLocBehindPossiblePointer(*E->getArg(0), State.Env)),
|
|
State, [](StorageLocation &Loc) {});
|
|
})
|
|
.CaseOfCFGStmt<CXXMemberCallExpr>(
|
|
isSmartPointerLikeValueMethodCall(),
|
|
[](const CXXMemberCallExpr *E, const MatchFinder::MatchResult &Result,
|
|
LatticeTransferState &State) {
|
|
transferSmartPointerLikeCachedDeref(
|
|
E, getImplicitObjectLocation(*E, State.Env), State,
|
|
[](StorageLocation &Loc) {});
|
|
})
|
|
.CaseOfCFGStmt<CXXMemberCallExpr>(
|
|
isSmartPointerLikeGetMethodCall(),
|
|
[](const CXXMemberCallExpr *E, const MatchFinder::MatchResult &Result,
|
|
LatticeTransferState &State) {
|
|
transferSmartPointerLikeCachedGet(
|
|
E, getImplicitObjectLocation(*E, State.Env), State,
|
|
[](StorageLocation &Loc) {});
|
|
})
|
|
|
|
// const accessor calls
|
|
.CaseOfCFGStmt<CXXMemberCallExpr>(isZeroParamConstMemberCall(),
|
|
transferConstMemberCall)
|
|
.CaseOfCFGStmt<CXXOperatorCallExpr>(isZeroParamConstMemberOperatorCall(),
|
|
transferConstMemberOperatorCall)
|
|
// non-const member calls that may modify the state of an object.
|
|
.CaseOfCFGStmt<CXXMemberCallExpr>(isNonConstMemberCall(),
|
|
transferValue_NonConstMemberCall)
|
|
.CaseOfCFGStmt<CXXOperatorCallExpr>(
|
|
isNonConstMemberOperatorCall(),
|
|
transferValue_NonConstMemberOperatorCall)
|
|
|
|
// other cases of returning optional
|
|
.CaseOfCFGStmt<CallExpr>(isCallReturningOptional(),
|
|
transferCallReturningOptional)
|
|
|
|
.Build();
|
|
}
|
|
|
|
llvm::SmallVector<UncheckedOptionalAccessDiagnostic>
|
|
diagnoseUnwrapCall(const Expr *ObjectExpr, const Environment &Env) {
|
|
if (auto *OptionalLoc = cast_or_null<RecordStorageLocation>(
|
|
getLocBehindPossiblePointer(*ObjectExpr, Env))) {
|
|
auto *Prop = Env.getValue(locForHasValue(*OptionalLoc));
|
|
if (auto *HasValueVal = cast_or_null<BoolValue>(Prop)) {
|
|
if (Env.proves(HasValueVal->formula()))
|
|
return {};
|
|
}
|
|
}
|
|
|
|
// Record that this unwrap is *not* provably safe.
|
|
// FIXME: include the name of the optional (if applicable).
|
|
auto Range = CharSourceRange::getTokenRange(ObjectExpr->getSourceRange());
|
|
return {UncheckedOptionalAccessDiagnostic{Range}};
|
|
}
|
|
|
|
auto buildDiagnoseMatchSwitch(
|
|
const UncheckedOptionalAccessModelOptions &Options) {
|
|
// FIXME: Evaluate the efficiency of matchers. If using matchers results in a
|
|
// lot of duplicated work (e.g. string comparisons), consider providing APIs
|
|
// that avoid it through memoization.
|
|
auto IgnorableOptional = ignorableOptional(Options);
|
|
return CFGMatchSwitchBuilder<
|
|
const Environment,
|
|
llvm::SmallVector<UncheckedOptionalAccessDiagnostic>>()
|
|
// optional::value
|
|
.CaseOfCFGStmt<CXXMemberCallExpr>(
|
|
valueCall(IgnorableOptional),
|
|
[](const CXXMemberCallExpr *E, const MatchFinder::MatchResult &,
|
|
const Environment &Env) {
|
|
return diagnoseUnwrapCall(E->getImplicitObjectArgument(), Env);
|
|
})
|
|
|
|
// optional::operator*, optional::operator->
|
|
.CaseOfCFGStmt<CallExpr>(valueOperatorCall(IgnorableOptional),
|
|
[](const CallExpr *E,
|
|
const MatchFinder::MatchResult &,
|
|
const Environment &Env) {
|
|
return diagnoseUnwrapCall(E->getArg(0), Env);
|
|
})
|
|
.Build();
|
|
}
|
|
|
|
} // namespace
|
|
|
|
ast_matchers::DeclarationMatcher
|
|
UncheckedOptionalAccessModel::optionalClassDecl() {
|
|
return cxxRecordDecl(optionalClass());
|
|
}
|
|
|
|
UncheckedOptionalAccessModel::UncheckedOptionalAccessModel(ASTContext &Ctx,
|
|
Environment &Env)
|
|
: DataflowAnalysis<UncheckedOptionalAccessModel,
|
|
UncheckedOptionalAccessLattice>(Ctx),
|
|
TransferMatchSwitch(buildTransferMatchSwitch()) {
|
|
Env.getDataflowAnalysisContext().setSyntheticFieldCallback(
|
|
[&Ctx](QualType Ty) -> llvm::StringMap<QualType> {
|
|
const CXXRecordDecl *Optional =
|
|
getOptionalBaseClass(Ty->getAsCXXRecordDecl());
|
|
if (Optional == nullptr)
|
|
return {};
|
|
return {{"value", valueTypeFromOptionalDecl(*Optional)},
|
|
{"has_value", Ctx.BoolTy}};
|
|
});
|
|
}
|
|
|
|
void UncheckedOptionalAccessModel::transfer(const CFGElement &Elt,
|
|
UncheckedOptionalAccessLattice &L,
|
|
Environment &Env) {
|
|
LatticeTransferState State(L, Env);
|
|
TransferMatchSwitch(Elt, getASTContext(), State);
|
|
}
|
|
|
|
UncheckedOptionalAccessDiagnoser::UncheckedOptionalAccessDiagnoser(
|
|
UncheckedOptionalAccessModelOptions Options)
|
|
: DiagnoseMatchSwitch(buildDiagnoseMatchSwitch(Options)) {}
|
|
|
|
} // namespace dataflow
|
|
} // namespace clang
|