2022-03-16 22:20:05 +00:00
|
|
|
//===-- 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.
|
|
|
|
//
|
|
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
|
2022-03-10 08:57:32 +00:00
|
|
|
#include "clang/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.h"
|
|
|
|
#include "clang/AST/ASTContext.h"
|
2022-03-06 19:01:46 +00:00
|
|
|
#include "clang/AST/DeclCXX.h"
|
2022-03-10 08:57:32 +00:00
|
|
|
#include "clang/AST/Expr.h"
|
|
|
|
#include "clang/AST/ExprCXX.h"
|
|
|
|
#include "clang/AST/Stmt.h"
|
|
|
|
#include "clang/ASTMatchers/ASTMatchers.h"
|
2022-09-19 16:56:35 +00:00
|
|
|
#include "clang/Analysis/CFG.h"
|
|
|
|
#include "clang/Analysis/FlowSensitive/CFGMatchSwitch.h"
|
2022-03-10 08:57:32 +00:00
|
|
|
#include "clang/Analysis/FlowSensitive/DataflowEnvironment.h"
|
2022-06-29 20:10:31 +00:00
|
|
|
#include "clang/Analysis/FlowSensitive/NoopLattice.h"
|
2023-01-03 20:50:01 +00:00
|
|
|
#include "clang/Analysis/FlowSensitive/StorageLocation.h"
|
2022-03-10 08:57:32 +00:00
|
|
|
#include "clang/Analysis/FlowSensitive/Value.h"
|
[clang][dataflow] Add API to separate analysis from diagnosis
This patch adds an optional `PostVisitStmt` parameter to the `runTypeErasedDataflowAnalysis` function, which does one more pass over all statements in the CFG after a fixpoint is reached. It then defines a `diagnose` method for the optional model in a new `UncheckedOptionalAccessDiagnosis` class, but only integrates that into the tests and not the actual optional check for `clang-tidy`. That will be done in a followup patch.
The primary motivation is to separate the implementation of the unchecked optional access check into two parts, to allow for further refactoring of just the model part later, while leaving the checking part alone. Currently there is duplication between the `transferUnwrapCall` and `diagnoseUnwrapCall` functions, but that will be dealt with in the followup.
Because diagnostics are now all gathered into one collection rather than being populated at each program point like when computing a fixpoint, this patch removes the usage of `Pair` and `UnorderedElementsAre` from the optional model tests, and instead modifies all their expectations to simply check the stringified set of diagnostics against a single string, either `"safe"` or some concatenation of `"unsafe: input.cc:y:x"`. This is not ideal as it loses any connection to the `/*[[check]]*/` annotations in the source strings, but it does still retain the source locations from the diagnostic strings themselves.
Reviewed By: sgatev, gribozavr2, xazax.hun
Differential Revision: https://reviews.llvm.org/D127898
2022-06-29 19:18:10 +00:00
|
|
|
#include "clang/Basic/SourceLocation.h"
|
2022-03-10 08:57:32 +00:00
|
|
|
#include "llvm/ADT/StringRef.h"
|
|
|
|
#include "llvm/Support/Casting.h"
|
[clang][dataflow] In optional model, implement `widen` and make `compare` sound.
This patch includes two related changes:
1. Rewrite `compare` operation to be sound. Current version checks for equality
of `isNonEmptyOptional` on both values, judging the values `Same` when the
results are equal. While that works when both are true, it is problematic when
they are both false, because there are four cases in which that's can occur:
both empty, one empty and one unknown (which is two cases), and both unknown. In
the latter three cases, it is unsound to judge them `Same`. This patch changes
`compare` to explicitly check for case of `both empty` and then judge any other
case `Different`.
2. With the change to `compare`, a number of common cases will no longer
terminate. So, we also implement widening to properly handle those cases and
recover termination.
Drive-by: improve performance of `merge` operation.
Of the new tests, the code before the patch fails
* ReassignValueInLoopToSetUnsafe, and
* ReassignValueInLoopToUnknownUnsafe.
Differential Revision: https://reviews.llvm.org/D140344
2022-12-15 15:11:21 +00:00
|
|
|
#include "llvm/Support/ErrorHandling.h"
|
2022-03-10 08:57:32 +00:00
|
|
|
#include <cassert>
|
2022-03-10 15:25:42 +00:00
|
|
|
#include <memory>
|
2023-01-14 11:07:21 -08:00
|
|
|
#include <optional>
|
2022-03-10 15:25:42 +00:00
|
|
|
#include <utility>
|
[clang][dataflow] Add API to separate analysis from diagnosis
This patch adds an optional `PostVisitStmt` parameter to the `runTypeErasedDataflowAnalysis` function, which does one more pass over all statements in the CFG after a fixpoint is reached. It then defines a `diagnose` method for the optional model in a new `UncheckedOptionalAccessDiagnosis` class, but only integrates that into the tests and not the actual optional check for `clang-tidy`. That will be done in a followup patch.
The primary motivation is to separate the implementation of the unchecked optional access check into two parts, to allow for further refactoring of just the model part later, while leaving the checking part alone. Currently there is duplication between the `transferUnwrapCall` and `diagnoseUnwrapCall` functions, but that will be dealt with in the followup.
Because diagnostics are now all gathered into one collection rather than being populated at each program point like when computing a fixpoint, this patch removes the usage of `Pair` and `UnorderedElementsAre` from the optional model tests, and instead modifies all their expectations to simply check the stringified set of diagnostics against a single string, either `"safe"` or some concatenation of `"unsafe: input.cc:y:x"`. This is not ideal as it loses any connection to the `/*[[check]]*/` annotations in the source strings, but it does still retain the source locations from the diagnostic strings themselves.
Reviewed By: sgatev, gribozavr2, xazax.hun
Differential Revision: https://reviews.llvm.org/D127898
2022-06-29 19:18:10 +00:00
|
|
|
#include <vector>
|
2022-03-10 08:57:32 +00:00
|
|
|
|
|
|
|
namespace clang {
|
|
|
|
namespace dataflow {
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
using namespace ::clang::ast_matchers;
|
2022-06-29 20:10:31 +00:00
|
|
|
using LatticeTransferState = TransferState<NoopLattice>;
|
2022-03-10 08:57:32 +00:00
|
|
|
|
2022-03-06 19:01:46 +00:00
|
|
|
DeclarationMatcher optionalClass() {
|
2022-03-10 08:57:32 +00:00
|
|
|
return classTemplateSpecializationDecl(
|
|
|
|
anyOf(hasName("std::optional"), hasName("std::__optional_storage_base"),
|
|
|
|
hasName("__optional_destruct_base"), hasName("absl::optional"),
|
|
|
|
hasName("base::Optional")),
|
|
|
|
hasTemplateArgument(0, refersToType(type().bind("T"))));
|
|
|
|
}
|
|
|
|
|
2022-06-03 16:21:27 +00:00
|
|
|
auto optionalOrAliasType() {
|
2022-06-01 08:43:30 +00:00
|
|
|
return hasUnqualifiedDesugaredType(
|
|
|
|
recordType(hasDeclaration(optionalClass())));
|
|
|
|
}
|
|
|
|
|
2022-06-03 16:21:27 +00:00
|
|
|
/// Matches any of the spellings of the optional types and sugar, aliases, etc.
|
|
|
|
auto hasOptionalType() { return hasType(optionalOrAliasType()); }
|
|
|
|
|
2022-03-21 15:03:52 +00:00
|
|
|
auto isOptionalMemberCallWithName(
|
|
|
|
llvm::StringRef MemberName,
|
2023-01-14 12:31:01 -08:00
|
|
|
const std::optional<StatementMatcher> &Ignorable = std::nullopt) {
|
2022-03-21 15:03:52 +00:00
|
|
|
auto Exception = unless(Ignorable ? expr(anyOf(*Ignorable, cxxThisExpr()))
|
|
|
|
: cxxThisExpr());
|
2022-03-10 08:57:32 +00:00
|
|
|
return cxxMemberCallExpr(
|
2022-03-21 15:03:52 +00:00
|
|
|
on(expr(Exception)),
|
2022-03-10 08:57:32 +00:00
|
|
|
callee(cxxMethodDecl(hasName(MemberName), ofClass(optionalClass()))));
|
|
|
|
}
|
|
|
|
|
2022-03-21 15:03:52 +00:00
|
|
|
auto isOptionalOperatorCallWithName(
|
|
|
|
llvm::StringRef operator_name,
|
2023-01-14 12:31:01 -08:00
|
|
|
const std::optional<StatementMatcher> &Ignorable = std::nullopt) {
|
2022-03-21 15:03:52 +00:00
|
|
|
return cxxOperatorCallExpr(
|
|
|
|
hasOverloadedOperatorName(operator_name),
|
|
|
|
callee(cxxMethodDecl(ofClass(optionalClass()))),
|
|
|
|
Ignorable ? callExpr(unless(hasArgument(0, *Ignorable))) : callExpr());
|
2022-03-10 08:57:32 +00:00
|
|
|
}
|
|
|
|
|
2022-03-14 14:52:35 +00:00
|
|
|
auto isMakeOptionalCall() {
|
2022-03-10 15:25:42 +00:00
|
|
|
return callExpr(
|
|
|
|
callee(functionDecl(hasAnyName(
|
|
|
|
"std::make_optional", "base::make_optional", "absl::make_optional"))),
|
|
|
|
hasOptionalType());
|
|
|
|
}
|
|
|
|
|
2022-12-05 20:38:55 +00:00
|
|
|
auto nulloptTypeDecl() {
|
|
|
|
return namedDecl(
|
|
|
|
hasAnyName("std::nullopt_t", "absl::nullopt_t", "base::nullopt_t"));
|
2022-03-14 14:52:35 +00:00
|
|
|
}
|
|
|
|
|
2022-12-05 20:38:55 +00:00
|
|
|
auto hasNulloptType() { return hasType(nulloptTypeDecl()); }
|
|
|
|
|
|
|
|
// `optional` or `nullopt_t`
|
|
|
|
auto hasAnyOptionalType() {
|
|
|
|
return hasType(hasUnqualifiedDesugaredType(
|
|
|
|
recordType(hasDeclaration(anyOf(nulloptTypeDecl(), optionalClass())))));
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2022-03-14 14:52:35 +00:00
|
|
|
auto inPlaceClass() {
|
|
|
|
return recordDecl(
|
|
|
|
hasAnyName("std::in_place_t", "absl::in_place_t", "base::in_place_t"));
|
|
|
|
}
|
|
|
|
|
|
|
|
auto isOptionalNulloptConstructor() {
|
2023-01-03 20:50:01 +00:00
|
|
|
return cxxConstructExpr(
|
|
|
|
hasOptionalType(),
|
|
|
|
hasDeclaration(cxxConstructorDecl(parameterCountIs(1),
|
|
|
|
hasParameter(0, hasNulloptType()))));
|
2022-03-14 14:52:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
auto isOptionalInPlaceConstructor() {
|
|
|
|
return cxxConstructExpr(hasOptionalType(),
|
|
|
|
hasArgument(0, hasType(inPlaceClass())));
|
|
|
|
}
|
|
|
|
|
|
|
|
auto isOptionalValueOrConversionConstructor() {
|
|
|
|
return cxxConstructExpr(
|
|
|
|
hasOptionalType(),
|
|
|
|
unless(hasDeclaration(
|
|
|
|
cxxConstructorDecl(anyOf(isCopyConstructor(), isMoveConstructor())))),
|
|
|
|
argumentCountIs(1), hasArgument(0, unless(hasNulloptType())));
|
|
|
|
}
|
|
|
|
|
2022-03-16 22:20:05 +00:00
|
|
|
auto isOptionalValueOrConversionAssignment() {
|
|
|
|
return cxxOperatorCallExpr(
|
|
|
|
hasOverloadedOperatorName("="),
|
|
|
|
callee(cxxMethodDecl(ofClass(optionalClass()))),
|
|
|
|
unless(hasDeclaration(cxxMethodDecl(
|
|
|
|
anyOf(isCopyAssignmentOperator(), isMoveAssignmentOperator())))),
|
|
|
|
argumentCountIs(2), hasArgument(1, unless(hasNulloptType())));
|
|
|
|
}
|
|
|
|
|
2022-12-05 20:38:55 +00:00
|
|
|
auto isNulloptConstructor() {
|
|
|
|
return cxxConstructExpr(hasNulloptType(), argumentCountIs(1),
|
|
|
|
hasArgument(0, hasNulloptType()));
|
|
|
|
}
|
|
|
|
|
2022-03-16 22:20:05 +00:00
|
|
|
auto isOptionalNulloptAssignment() {
|
|
|
|
return cxxOperatorCallExpr(hasOverloadedOperatorName("="),
|
|
|
|
callee(cxxMethodDecl(ofClass(optionalClass()))),
|
|
|
|
argumentCountIs(2),
|
|
|
|
hasArgument(1, hasNulloptType()));
|
|
|
|
}
|
|
|
|
|
2022-03-21 12:06:16 +00:00
|
|
|
auto isStdSwapCall() {
|
|
|
|
return callExpr(callee(functionDecl(hasName("std::swap"))),
|
|
|
|
argumentCountIs(2), hasArgument(0, hasOptionalType()),
|
|
|
|
hasArgument(1, hasOptionalType()));
|
|
|
|
}
|
|
|
|
|
2022-03-21 20:37:04 +00:00
|
|
|
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)))));
|
|
|
|
}
|
|
|
|
|
2022-06-01 08:43:30 +00:00
|
|
|
auto isCallReturningOptional() {
|
2022-06-10 14:50:22 +00:00
|
|
|
return callExpr(hasType(qualType(anyOf(
|
|
|
|
optionalOrAliasType(), referenceType(pointee(optionalOrAliasType()))))));
|
2022-06-01 08:43:30 +00:00
|
|
|
}
|
|
|
|
|
2022-12-05 20:38:55 +00:00
|
|
|
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 it.
|
|
|
|
BoolValue &forceBoolValue(Environment &Env, const Expr &Expr) {
|
|
|
|
auto *Value = cast_or_null<BoolValue>(Env.getValue(Expr, SkipPast::None));
|
|
|
|
if (Value != nullptr)
|
|
|
|
return *Value;
|
|
|
|
|
|
|
|
auto &Loc = Env.createStorageLocation(Expr);
|
|
|
|
Value = &Env.makeAtomicBoolValue();
|
|
|
|
Env.setValue(Loc, *Value);
|
|
|
|
Env.setStorageLocation(Expr, Loc);
|
|
|
|
return *Value;
|
|
|
|
}
|
|
|
|
|
2022-05-18 21:57:40 +00:00
|
|
|
/// Sets `HasValueVal` as the symbolic value that represents the "has_value"
|
|
|
|
/// property of the optional value `OptionalVal`.
|
|
|
|
void setHasValue(Value &OptionalVal, BoolValue &HasValueVal) {
|
|
|
|
OptionalVal.setProperty("has_value", HasValueVal);
|
|
|
|
}
|
|
|
|
|
2022-03-10 15:25:42 +00:00
|
|
|
/// 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) {
|
|
|
|
auto OptionalVal = std::make_unique<StructValue>();
|
2022-05-18 21:57:40 +00:00
|
|
|
setHasValue(*OptionalVal, HasValueVal);
|
2022-03-10 15:25:42 +00:00
|
|
|
return Env.takeOwnership(std::move(OptionalVal));
|
|
|
|
}
|
|
|
|
|
2022-03-10 08:57:32 +00:00
|
|
|
/// Returns the symbolic value that represents the "has_value" property of the
|
2022-06-08 19:55:54 +02:00
|
|
|
/// optional value `OptionalVal`. Returns null if `OptionalVal` is null.
|
2022-05-03 15:53:35 +00:00
|
|
|
BoolValue *getHasValue(Environment &Env, Value *OptionalVal) {
|
|
|
|
if (OptionalVal != nullptr) {
|
|
|
|
auto *HasValueVal =
|
|
|
|
cast_or_null<BoolValue>(OptionalVal->getProperty("has_value"));
|
|
|
|
if (HasValueVal == nullptr) {
|
|
|
|
HasValueVal = &Env.makeAtomicBoolValue();
|
|
|
|
OptionalVal->setProperty("has_value", *HasValueVal);
|
|
|
|
}
|
|
|
|
return HasValueVal;
|
2022-03-10 08:57:32 +00:00
|
|
|
}
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
2022-03-14 14:52:35 +00:00
|
|
|
/// If `Type` is a reference type, returns the type of its pointee. Otherwise,
|
|
|
|
/// returns `Type` itself.
|
|
|
|
QualType stripReference(QualType Type) {
|
|
|
|
return Type->isReferenceType() ? Type->getPointeeType() : Type;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns true if and only if `Type` is an optional type.
|
2022-11-03 12:44:10 +00:00
|
|
|
bool isOptionalType(QualType Type) {
|
2022-03-14 14:52:35 +00:00
|
|
|
if (!Type->isRecordType())
|
|
|
|
return false;
|
|
|
|
// FIXME: Optimize this by avoiding the `getQualifiedNameAsString` call.
|
|
|
|
auto TypeName = Type->getAsCXXRecordDecl()->getQualifiedNameAsString();
|
|
|
|
return TypeName == "std::optional" || TypeName == "absl::optional" ||
|
|
|
|
TypeName == "base::Optional";
|
|
|
|
}
|
|
|
|
|
|
|
|
/// 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) {
|
2022-11-03 12:44:10 +00:00
|
|
|
if (!isOptionalType(Type))
|
2022-03-14 14:52:35 +00:00
|
|
|
return 0;
|
|
|
|
return 1 + countOptionalWrappers(
|
|
|
|
ASTCtx,
|
|
|
|
cast<ClassTemplateSpecializationDecl>(Type->getAsRecordDecl())
|
|
|
|
->getTemplateArgs()
|
|
|
|
.get(0)
|
|
|
|
.getAsType()
|
|
|
|
.getDesugaredType(ASTCtx));
|
|
|
|
}
|
|
|
|
|
2022-05-03 15:53:35 +00:00
|
|
|
/// Tries to initialize the `optional`'s value (that is, contents), and return
|
|
|
|
/// its location. Returns nullptr if the value can't be represented.
|
|
|
|
StorageLocation *maybeInitializeOptionalValueMember(QualType Q,
|
|
|
|
Value &OptionalVal,
|
|
|
|
Environment &Env) {
|
|
|
|
// The "value" property represents a synthetic field. As such, it needs
|
|
|
|
// `StorageLocation`, like normal fields (and other variables). So, we model
|
|
|
|
// it with a `ReferenceValue`, since that includes a storage location. Once
|
|
|
|
// the property is set, it will be shared by all environments that access the
|
|
|
|
// `Value` representing the optional (here, `OptionalVal`).
|
|
|
|
if (auto *ValueProp = OptionalVal.getProperty("value")) {
|
|
|
|
auto *ValueRef = clang::cast<ReferenceValue>(ValueProp);
|
2022-06-15 00:41:49 +02:00
|
|
|
auto &ValueLoc = ValueRef->getReferentLoc();
|
2022-05-03 15:53:35 +00:00
|
|
|
if (Env.getValue(ValueLoc) == nullptr) {
|
|
|
|
// The property was previously set, but the value has been lost. This can
|
|
|
|
// happen, for example, because of an environment merge (where the two
|
|
|
|
// environments mapped the property to different values, which resulted in
|
|
|
|
// them both being discarded), or when two blocks in the CFG, with neither
|
|
|
|
// a dominator of the other, visit the same optional value, or even when a
|
|
|
|
// block is revisited during testing to collect per-statement state.
|
|
|
|
// FIXME: This situation means that the optional contents are not shared
|
|
|
|
// between branches and the like. Practically, this lack of sharing
|
|
|
|
// reduces the precision of the model when the contents are relevant to
|
|
|
|
// the check, like another optional or a boolean that influences control
|
|
|
|
// flow.
|
|
|
|
auto *ValueVal = Env.createValue(ValueLoc.getType());
|
|
|
|
if (ValueVal == nullptr)
|
|
|
|
return nullptr;
|
|
|
|
Env.setValue(ValueLoc, *ValueVal);
|
|
|
|
}
|
|
|
|
return &ValueLoc;
|
|
|
|
}
|
|
|
|
|
|
|
|
auto Ty = stripReference(Q);
|
|
|
|
auto *ValueVal = Env.createValue(Ty);
|
|
|
|
if (ValueVal == nullptr)
|
|
|
|
return nullptr;
|
|
|
|
auto &ValueLoc = Env.createStorageLocation(Ty);
|
|
|
|
Env.setValue(ValueLoc, *ValueVal);
|
|
|
|
auto ValueRef = std::make_unique<ReferenceValue>(ValueLoc);
|
|
|
|
OptionalVal.setProperty("value", Env.takeOwnership(std::move(ValueRef)));
|
|
|
|
return &ValueLoc;
|
|
|
|
}
|
|
|
|
|
2022-03-14 14:52:35 +00:00
|
|
|
void initializeOptionalReference(const Expr *OptionalExpr,
|
|
|
|
const MatchFinder::MatchResult &,
|
|
|
|
LatticeTransferState &State) {
|
2022-06-08 19:55:54 +02:00
|
|
|
if (auto *OptionalVal =
|
|
|
|
State.Env.getValue(*OptionalExpr, SkipPast::Reference)) {
|
2022-03-10 08:57:32 +00:00
|
|
|
if (OptionalVal->getProperty("has_value") == nullptr) {
|
2022-05-18 21:57:40 +00:00
|
|
|
setHasValue(*OptionalVal, State.Env.makeAtomicBoolValue());
|
2022-03-10 08:57:32 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-18 21:57:40 +00:00
|
|
|
/// Returns true if and only if `OptionalVal` is initialized and known to be
|
|
|
|
/// empty in `Env.
|
|
|
|
bool isEmptyOptional(const Value &OptionalVal, const Environment &Env) {
|
|
|
|
auto *HasValueVal =
|
|
|
|
cast_or_null<BoolValue>(OptionalVal.getProperty("has_value"));
|
|
|
|
return HasValueVal != nullptr &&
|
|
|
|
Env.flowConditionImplies(Env.makeNot(*HasValueVal));
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns true if and only if `OptionalVal` is initialized and known to be
|
|
|
|
/// non-empty in `Env.
|
|
|
|
bool isNonEmptyOptional(const Value &OptionalVal, const Environment &Env) {
|
|
|
|
auto *HasValueVal =
|
|
|
|
cast_or_null<BoolValue>(OptionalVal.getProperty("has_value"));
|
|
|
|
return HasValueVal != nullptr && Env.flowConditionImplies(*HasValueVal);
|
|
|
|
}
|
|
|
|
|
2022-03-14 14:52:35 +00:00
|
|
|
void transferUnwrapCall(const Expr *UnwrapExpr, const Expr *ObjectExpr,
|
|
|
|
LatticeTransferState &State) {
|
2022-06-08 19:55:54 +02:00
|
|
|
if (auto *OptionalVal =
|
|
|
|
State.Env.getValue(*ObjectExpr, SkipPast::ReferenceThenPointer)) {
|
2022-05-03 15:53:35 +00:00
|
|
|
if (State.Env.getStorageLocation(*UnwrapExpr, SkipPast::None) == nullptr)
|
|
|
|
if (auto *Loc = maybeInitializeOptionalValueMember(
|
|
|
|
UnwrapExpr->getType(), *OptionalVal, State.Env))
|
|
|
|
State.Env.setStorageLocation(*UnwrapExpr, *Loc);
|
2022-03-10 08:57:32 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-14 14:52:35 +00:00
|
|
|
void transferMakeOptionalCall(const CallExpr *E,
|
|
|
|
const MatchFinder::MatchResult &,
|
|
|
|
LatticeTransferState &State) {
|
2022-03-10 15:25:42 +00:00
|
|
|
auto &Loc = State.Env.createStorageLocation(*E);
|
|
|
|
State.Env.setStorageLocation(*E, Loc);
|
|
|
|
State.Env.setValue(
|
|
|
|
Loc, createOptionalValue(State.Env, State.Env.getBoolLiteralValue(true)));
|
|
|
|
}
|
|
|
|
|
2022-03-14 14:52:35 +00:00
|
|
|
void transferOptionalHasValueCall(const CXXMemberCallExpr *CallExpr,
|
|
|
|
const MatchFinder::MatchResult &,
|
|
|
|
LatticeTransferState &State) {
|
2022-05-03 15:53:35 +00:00
|
|
|
if (auto *HasValueVal = getHasValue(
|
|
|
|
State.Env, State.Env.getValue(*CallExpr->getImplicitObjectArgument(),
|
|
|
|
SkipPast::ReferenceThenPointer))) {
|
2022-03-10 08:57:32 +00:00
|
|
|
auto &CallExprLoc = State.Env.createStorageLocation(*CallExpr);
|
|
|
|
State.Env.setValue(CallExprLoc, *HasValueVal);
|
|
|
|
State.Env.setStorageLocation(*CallExpr, CallExprLoc);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-21 20:37:04 +00:00
|
|
|
/// `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();
|
|
|
|
|
2022-05-03 15:53:35 +00:00
|
|
|
auto *HasValueVal = getHasValue(
|
|
|
|
State.Env,
|
|
|
|
State.Env.getValue(*ObjectArgumentExpr, SkipPast::ReferenceThenPointer));
|
|
|
|
if (HasValueVal == nullptr)
|
2022-03-21 20:37:04 +00:00
|
|
|
return;
|
|
|
|
|
2022-12-05 20:38:55 +00:00
|
|
|
Env.addToFlowCondition(
|
|
|
|
ModelPred(Env, forceBoolValue(Env, *ValueOrPredExpr), *HasValueVal));
|
2022-03-21 20:37:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-06-01 08:43:30 +00:00
|
|
|
void transferCallReturningOptional(const CallExpr *E,
|
|
|
|
const MatchFinder::MatchResult &Result,
|
|
|
|
LatticeTransferState &State) {
|
|
|
|
if (State.Env.getStorageLocation(*E, SkipPast::None) != nullptr)
|
|
|
|
return;
|
|
|
|
|
|
|
|
auto &Loc = State.Env.createStorageLocation(*E);
|
|
|
|
State.Env.setStorageLocation(*E, Loc);
|
|
|
|
State.Env.setValue(
|
|
|
|
Loc, createOptionalValue(State.Env, State.Env.makeAtomicBoolValue()));
|
|
|
|
}
|
|
|
|
|
2022-12-21 22:48:04 +00:00
|
|
|
void assignOptionalValue(const Expr &E, Environment &Env,
|
2022-03-14 14:52:35 +00:00
|
|
|
BoolValue &HasValueVal) {
|
|
|
|
if (auto *OptionalLoc =
|
2022-12-21 22:48:04 +00:00
|
|
|
Env.getStorageLocation(E, SkipPast::ReferenceThenPointer)) {
|
|
|
|
Env.setValue(*OptionalLoc, createOptionalValue(Env, HasValueVal));
|
2022-03-10 15:25:42 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-16 22:20:05 +00:00
|
|
|
/// 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`.
|
2022-12-05 20:38:55 +00:00
|
|
|
BoolValue &valueOrConversionHasValue(const FunctionDecl &F, const Expr &E,
|
|
|
|
const MatchFinder::MatchResult &MatchRes,
|
|
|
|
LatticeTransferState &State) {
|
2023-01-03 20:50:01 +00:00
|
|
|
assert(F.getTemplateSpecializationArgs() != nullptr);
|
2022-03-16 22:20:05 +00:00
|
|
|
assert(F.getTemplateSpecializationArgs()->size() > 0);
|
|
|
|
|
|
|
|
const int TemplateParamOptionalWrappersCount = countOptionalWrappers(
|
|
|
|
*MatchRes.Context,
|
|
|
|
stripReference(F.getTemplateSpecializationArgs()->get(0).getAsType()));
|
|
|
|
const int ArgTypeOptionalWrappersCount =
|
|
|
|
countOptionalWrappers(*MatchRes.Context, stripReference(E.getType()));
|
|
|
|
|
|
|
|
// Check if this is a constructor/assignment call for `optional<T>` with
|
|
|
|
// argument of type `U` such that `T` is constructible from `U`.
|
|
|
|
if (TemplateParamOptionalWrappersCount == ArgTypeOptionalWrappersCount)
|
|
|
|
return State.Env.getBoolLiteralValue(true);
|
|
|
|
|
|
|
|
// This is a constructor/assignment call for `optional<T>` with argument of
|
|
|
|
// type `optional<U>` such that `T` is constructible from `U`.
|
2022-05-03 15:53:35 +00:00
|
|
|
if (auto *HasValueVal =
|
|
|
|
getHasValue(State.Env, State.Env.getValue(E, SkipPast::Reference)))
|
|
|
|
return *HasValueVal;
|
2022-03-16 22:20:05 +00:00
|
|
|
return State.Env.makeAtomicBoolValue();
|
|
|
|
}
|
|
|
|
|
2022-03-14 14:52:35 +00:00
|
|
|
void transferValueOrConversionConstructor(
|
|
|
|
const CXXConstructExpr *E, const MatchFinder::MatchResult &MatchRes,
|
|
|
|
LatticeTransferState &State) {
|
|
|
|
assert(E->getNumArgs() > 0);
|
|
|
|
|
2022-12-21 22:48:04 +00:00
|
|
|
assignOptionalValue(*E, State.Env,
|
2022-12-05 20:38:55 +00:00
|
|
|
valueOrConversionHasValue(*E->getConstructor(),
|
|
|
|
*E->getArg(0), MatchRes,
|
|
|
|
State));
|
2022-03-16 22:20:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void transferAssignment(const CXXOperatorCallExpr *E, BoolValue &HasValueVal,
|
|
|
|
LatticeTransferState &State) {
|
|
|
|
assert(E->getNumArgs() > 0);
|
|
|
|
|
|
|
|
auto *OptionalLoc =
|
|
|
|
State.Env.getStorageLocation(*E->getArg(0), SkipPast::Reference);
|
2022-06-10 19:08:41 +00:00
|
|
|
if (OptionalLoc == nullptr)
|
|
|
|
return;
|
2022-03-16 22:20:05 +00:00
|
|
|
|
|
|
|
State.Env.setValue(*OptionalLoc, createOptionalValue(State.Env, HasValueVal));
|
|
|
|
|
|
|
|
// Assign a storage location for the whole expression.
|
|
|
|
State.Env.setStorageLocation(*E, *OptionalLoc);
|
|
|
|
}
|
|
|
|
|
|
|
|
void transferValueOrConversionAssignment(
|
|
|
|
const CXXOperatorCallExpr *E, const MatchFinder::MatchResult &MatchRes,
|
|
|
|
LatticeTransferState &State) {
|
|
|
|
assert(E->getNumArgs() > 1);
|
|
|
|
transferAssignment(E,
|
2022-12-05 20:38:55 +00:00
|
|
|
valueOrConversionHasValue(*E->getDirectCallee(),
|
|
|
|
*E->getArg(1), MatchRes, State),
|
2022-03-16 22:20:05 +00:00
|
|
|
State);
|
|
|
|
}
|
|
|
|
|
|
|
|
void transferNulloptAssignment(const CXXOperatorCallExpr *E,
|
|
|
|
const MatchFinder::MatchResult &,
|
|
|
|
LatticeTransferState &State) {
|
|
|
|
transferAssignment(E, State.Env.getBoolLiteralValue(false), State);
|
2022-03-10 15:25:42 +00:00
|
|
|
}
|
|
|
|
|
2023-01-26 14:31:03 +00:00
|
|
|
void transferSwap(const Expr &E1, SkipPast E1Skip, const Expr &E2,
|
|
|
|
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.
|
|
|
|
auto *Loc1 = Env.getStorageLocation(E1, E1Skip);
|
|
|
|
auto *Loc2 = Env.getStorageLocation(E2, SkipPast::Reference);
|
|
|
|
|
|
|
|
if (Loc1 == nullptr) {
|
|
|
|
if (Loc2 != nullptr)
|
|
|
|
Env.setValue(*Loc2, createOptionalValue(Env, Env.makeAtomicBoolValue()));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (Loc2 == nullptr) {
|
|
|
|
Env.setValue(*Loc1, createOptionalValue(Env, Env.makeAtomicBoolValue()));
|
|
|
|
return;
|
|
|
|
}
|
2022-03-21 12:06:16 +00:00
|
|
|
|
2023-01-26 14:31:03 +00:00
|
|
|
// 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.
|
|
|
|
auto *Val1 = Env.getValue(*Loc1);
|
|
|
|
if (Val1 == nullptr)
|
|
|
|
Val1 = &createOptionalValue(Env, Env.makeAtomicBoolValue());
|
2022-03-21 12:06:16 +00:00
|
|
|
|
2023-01-26 14:31:03 +00:00
|
|
|
auto *Val2 = Env.getValue(*Loc2);
|
|
|
|
if (Val2 == nullptr)
|
|
|
|
Val2 = &createOptionalValue(Env, Env.makeAtomicBoolValue());
|
|
|
|
|
|
|
|
Env.setValue(*Loc1, *Val2);
|
|
|
|
Env.setValue(*Loc2, *Val1);
|
2022-03-21 12:06:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void transferSwapCall(const CXXMemberCallExpr *E,
|
|
|
|
const MatchFinder::MatchResult &,
|
|
|
|
LatticeTransferState &State) {
|
|
|
|
assert(E->getNumArgs() == 1);
|
2023-01-26 14:31:03 +00:00
|
|
|
transferSwap(*E->getImplicitObjectArgument(), SkipPast::ReferenceThenPointer,
|
|
|
|
*E->getArg(0), State.Env);
|
2022-03-21 12:06:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void transferStdSwapCall(const CallExpr *E, const MatchFinder::MatchResult &,
|
|
|
|
LatticeTransferState &State) {
|
|
|
|
assert(E->getNumArgs() == 2);
|
2023-01-26 14:31:03 +00:00
|
|
|
transferSwap(*E->getArg(0), SkipPast::Reference, *E->getArg(1), State.Env);
|
2022-03-21 12:06:16 +00:00
|
|
|
}
|
|
|
|
|
2022-12-05 20:38:55 +00:00
|
|
|
BoolValue &evaluateEquality(Environment &Env, BoolValue &EqVal, BoolValue &LHS,
|
|
|
|
BoolValue &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 Env.makeAnd(
|
|
|
|
Env.makeImplication(
|
|
|
|
EqVal, Env.makeOr(Env.makeAnd(LHS, RHS),
|
|
|
|
Env.makeAnd(Env.makeNot(LHS), Env.makeNot(RHS)))),
|
|
|
|
Env.makeImplication(Env.makeNot(EqVal), Env.makeOr(LHS, RHS)));
|
|
|
|
}
|
|
|
|
|
|
|
|
void transferOptionalAndOptionalCmp(const clang::CXXOperatorCallExpr *CmpExpr,
|
|
|
|
const MatchFinder::MatchResult &,
|
|
|
|
LatticeTransferState &State) {
|
|
|
|
Environment &Env = State.Env;
|
|
|
|
auto *CmpValue = &forceBoolValue(Env, *CmpExpr);
|
|
|
|
if (auto *LHasVal = getHasValue(
|
|
|
|
Env, Env.getValue(*CmpExpr->getArg(0), SkipPast::Reference)))
|
|
|
|
if (auto *RHasVal = getHasValue(
|
|
|
|
Env, Env.getValue(*CmpExpr->getArg(1), SkipPast::Reference))) {
|
|
|
|
if (CmpExpr->getOperator() == clang::OO_ExclaimEqual)
|
|
|
|
CmpValue = &State.Env.makeNot(*CmpValue);
|
|
|
|
Env.addToFlowCondition(
|
|
|
|
evaluateEquality(Env, *CmpValue, *LHasVal, *RHasVal));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void transferOptionalAndValueCmp(const clang::CXXOperatorCallExpr *CmpExpr,
|
|
|
|
const clang::Expr *E, Environment &Env) {
|
|
|
|
auto *CmpValue = &forceBoolValue(Env, *CmpExpr);
|
|
|
|
if (auto *HasVal = getHasValue(Env, Env.getValue(*E, SkipPast::Reference))) {
|
|
|
|
if (CmpExpr->getOperator() == clang::OO_ExclaimEqual)
|
|
|
|
CmpValue = &Env.makeNot(*CmpValue);
|
|
|
|
Env.addToFlowCondition(evaluateEquality(Env, *CmpValue, *HasVal,
|
|
|
|
Env.getBoolLiteralValue(true)));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-14 12:31:01 -08:00
|
|
|
std::optional<StatementMatcher>
|
2022-03-21 15:03:52 +00:00
|
|
|
ignorableOptional(const UncheckedOptionalAccessModelOptions &Options) {
|
2022-12-14 13:54:38 +00:00
|
|
|
if (Options.IgnoreSmartPointerDereference) {
|
|
|
|
auto SmartPtrUse = expr(ignoringParenImpCasts(cxxOperatorCallExpr(
|
|
|
|
anyOf(hasOverloadedOperatorName("->"), hasOverloadedOperatorName("*")),
|
|
|
|
unless(hasArgument(0, expr(hasOptionalType()))))));
|
|
|
|
return expr(
|
|
|
|
anyOf(SmartPtrUse, memberExpr(hasObjectExpression(SmartPtrUse))));
|
|
|
|
}
|
2022-12-03 11:34:25 -08:00
|
|
|
return std::nullopt;
|
2022-03-21 15:03:52 +00:00
|
|
|
}
|
|
|
|
|
[clang][dataflow] Add API to separate analysis from diagnosis
This patch adds an optional `PostVisitStmt` parameter to the `runTypeErasedDataflowAnalysis` function, which does one more pass over all statements in the CFG after a fixpoint is reached. It then defines a `diagnose` method for the optional model in a new `UncheckedOptionalAccessDiagnosis` class, but only integrates that into the tests and not the actual optional check for `clang-tidy`. That will be done in a followup patch.
The primary motivation is to separate the implementation of the unchecked optional access check into two parts, to allow for further refactoring of just the model part later, while leaving the checking part alone. Currently there is duplication between the `transferUnwrapCall` and `diagnoseUnwrapCall` functions, but that will be dealt with in the followup.
Because diagnostics are now all gathered into one collection rather than being populated at each program point like when computing a fixpoint, this patch removes the usage of `Pair` and `UnorderedElementsAre` from the optional model tests, and instead modifies all their expectations to simply check the stringified set of diagnostics against a single string, either `"safe"` or some concatenation of `"unsafe: input.cc:y:x"`. This is not ideal as it loses any connection to the `/*[[check]]*/` annotations in the source strings, but it does still retain the source locations from the diagnostic strings themselves.
Reviewed By: sgatev, gribozavr2, xazax.hun
Differential Revision: https://reviews.llvm.org/D127898
2022-06-29 19:18:10 +00:00
|
|
|
StatementMatcher
|
2023-01-14 12:31:01 -08:00
|
|
|
valueCall(const std::optional<StatementMatcher> &IgnorableOptional) {
|
[clang][dataflow] Add API to separate analysis from diagnosis
This patch adds an optional `PostVisitStmt` parameter to the `runTypeErasedDataflowAnalysis` function, which does one more pass over all statements in the CFG after a fixpoint is reached. It then defines a `diagnose` method for the optional model in a new `UncheckedOptionalAccessDiagnosis` class, but only integrates that into the tests and not the actual optional check for `clang-tidy`. That will be done in a followup patch.
The primary motivation is to separate the implementation of the unchecked optional access check into two parts, to allow for further refactoring of just the model part later, while leaving the checking part alone. Currently there is duplication between the `transferUnwrapCall` and `diagnoseUnwrapCall` functions, but that will be dealt with in the followup.
Because diagnostics are now all gathered into one collection rather than being populated at each program point like when computing a fixpoint, this patch removes the usage of `Pair` and `UnorderedElementsAre` from the optional model tests, and instead modifies all their expectations to simply check the stringified set of diagnostics against a single string, either `"safe"` or some concatenation of `"unsafe: input.cc:y:x"`. This is not ideal as it loses any connection to the `/*[[check]]*/` annotations in the source strings, but it does still retain the source locations from the diagnostic strings themselves.
Reviewed By: sgatev, gribozavr2, xazax.hun
Differential Revision: https://reviews.llvm.org/D127898
2022-06-29 19:18:10 +00:00
|
|
|
return isOptionalMemberCallWithName("value", IgnorableOptional);
|
|
|
|
}
|
|
|
|
|
|
|
|
StatementMatcher
|
2023-01-14 12:31:01 -08:00
|
|
|
valueOperatorCall(const std::optional<StatementMatcher> &IgnorableOptional) {
|
[clang][dataflow] Add API to separate analysis from diagnosis
This patch adds an optional `PostVisitStmt` parameter to the `runTypeErasedDataflowAnalysis` function, which does one more pass over all statements in the CFG after a fixpoint is reached. It then defines a `diagnose` method for the optional model in a new `UncheckedOptionalAccessDiagnosis` class, but only integrates that into the tests and not the actual optional check for `clang-tidy`. That will be done in a followup patch.
The primary motivation is to separate the implementation of the unchecked optional access check into two parts, to allow for further refactoring of just the model part later, while leaving the checking part alone. Currently there is duplication between the `transferUnwrapCall` and `diagnoseUnwrapCall` functions, but that will be dealt with in the followup.
Because diagnostics are now all gathered into one collection rather than being populated at each program point like when computing a fixpoint, this patch removes the usage of `Pair` and `UnorderedElementsAre` from the optional model tests, and instead modifies all their expectations to simply check the stringified set of diagnostics against a single string, either `"safe"` or some concatenation of `"unsafe: input.cc:y:x"`. This is not ideal as it loses any connection to the `/*[[check]]*/` annotations in the source strings, but it does still retain the source locations from the diagnostic strings themselves.
Reviewed By: sgatev, gribozavr2, xazax.hun
Differential Revision: https://reviews.llvm.org/D127898
2022-06-29 19:18:10 +00:00
|
|
|
return expr(anyOf(isOptionalOperatorCallWithName("*", IgnorableOptional),
|
|
|
|
isOptionalOperatorCallWithName("->", IgnorableOptional)));
|
|
|
|
}
|
|
|
|
|
2022-12-14 13:54:38 +00:00
|
|
|
auto buildTransferMatchSwitch() {
|
2022-03-16 22:20:05 +00:00
|
|
|
// 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.
|
2022-09-19 16:56:35 +00:00
|
|
|
return CFGMatchSwitchBuilder<LatticeTransferState>()
|
2022-03-10 08:57:32 +00:00
|
|
|
// Attach a symbolic "has_value" state to optional values that we see for
|
|
|
|
// the first time.
|
2022-09-19 16:56:35 +00:00
|
|
|
.CaseOfCFGStmt<Expr>(
|
2022-06-03 16:21:27 +00:00
|
|
|
expr(anyOf(declRefExpr(), memberExpr()), hasOptionalType()),
|
|
|
|
initializeOptionalReference)
|
2022-03-10 08:57:32 +00:00
|
|
|
|
2022-03-10 15:25:42 +00:00
|
|
|
// make_optional
|
2022-09-19 16:56:35 +00:00
|
|
|
.CaseOfCFGStmt<CallExpr>(isMakeOptionalCall(), transferMakeOptionalCall)
|
2022-03-14 14:52:35 +00:00
|
|
|
|
2022-12-21 22:48:04 +00:00
|
|
|
// optional::optional (in place)
|
2022-09-19 16:56:35 +00:00
|
|
|
.CaseOfCFGStmt<CXXConstructExpr>(
|
2022-03-14 14:52:35 +00:00
|
|
|
isOptionalInPlaceConstructor(),
|
|
|
|
[](const CXXConstructExpr *E, const MatchFinder::MatchResult &,
|
|
|
|
LatticeTransferState &State) {
|
2022-12-21 22:48:04 +00:00
|
|
|
assignOptionalValue(*E, State.Env,
|
|
|
|
State.Env.getBoolLiteralValue(true));
|
2022-03-14 14:52:35 +00:00
|
|
|
})
|
2022-12-21 22:48:04 +00:00
|
|
|
// nullopt_t::nullopt_t
|
2022-09-19 16:56:35 +00:00
|
|
|
.CaseOfCFGStmt<CXXConstructExpr>(
|
2022-12-05 20:38:55 +00:00
|
|
|
isNulloptConstructor(),
|
2022-03-14 14:52:35 +00:00
|
|
|
[](const CXXConstructExpr *E, const MatchFinder::MatchResult &,
|
|
|
|
LatticeTransferState &State) {
|
2022-12-21 22:48:04 +00:00
|
|
|
assignOptionalValue(*E, State.Env,
|
2022-03-14 14:52:35 +00:00
|
|
|
State.Env.getBoolLiteralValue(false));
|
|
|
|
})
|
2022-12-21 22:48:04 +00:00
|
|
|
// optional::optional(nullopt_t)
|
2022-12-05 20:38:55 +00:00
|
|
|
.CaseOfCFGStmt<CXXConstructExpr>(
|
|
|
|
isOptionalNulloptConstructor(),
|
|
|
|
[](const CXXConstructExpr *E, const MatchFinder::MatchResult &,
|
|
|
|
LatticeTransferState &State) {
|
2022-12-21 22:48:04 +00:00
|
|
|
assignOptionalValue(*E, State.Env,
|
|
|
|
State.Env.getBoolLiteralValue(false));
|
2022-12-05 20:38:55 +00:00
|
|
|
})
|
2022-12-21 22:48:04 +00:00
|
|
|
// optional::optional (value/conversion)
|
2022-09-19 16:56:35 +00:00
|
|
|
.CaseOfCFGStmt<CXXConstructExpr>(isOptionalValueOrConversionConstructor(),
|
|
|
|
transferValueOrConversionConstructor)
|
2022-03-10 15:25:42 +00:00
|
|
|
|
2022-12-21 22:48:04 +00:00
|
|
|
|
2022-03-16 22:20:05 +00:00
|
|
|
// optional::operator=
|
2022-09-19 16:56:35 +00:00
|
|
|
.CaseOfCFGStmt<CXXOperatorCallExpr>(
|
|
|
|
isOptionalValueOrConversionAssignment(),
|
|
|
|
transferValueOrConversionAssignment)
|
|
|
|
.CaseOfCFGStmt<CXXOperatorCallExpr>(isOptionalNulloptAssignment(),
|
|
|
|
transferNulloptAssignment)
|
2022-03-16 22:20:05 +00:00
|
|
|
|
2022-03-10 08:57:32 +00:00
|
|
|
// optional::value
|
2022-09-19 16:56:35 +00:00
|
|
|
.CaseOfCFGStmt<CXXMemberCallExpr>(
|
2022-12-14 13:54:38 +00:00
|
|
|
valueCall(std::nullopt),
|
2022-03-14 14:52:35 +00:00
|
|
|
[](const CXXMemberCallExpr *E, const MatchFinder::MatchResult &,
|
|
|
|
LatticeTransferState &State) {
|
2022-03-10 08:57:32 +00:00
|
|
|
transferUnwrapCall(E, E->getImplicitObjectArgument(), State);
|
|
|
|
})
|
|
|
|
|
|
|
|
// optional::operator*, optional::operator->
|
2022-12-14 13:54:38 +00:00
|
|
|
.CaseOfCFGStmt<CallExpr>(valueOperatorCall(std::nullopt),
|
2022-09-19 16:56:35 +00:00
|
|
|
[](const CallExpr *E,
|
|
|
|
const MatchFinder::MatchResult &,
|
|
|
|
LatticeTransferState &State) {
|
|
|
|
transferUnwrapCall(E, E->getArg(0), State);
|
|
|
|
})
|
2022-03-10 08:57:32 +00:00
|
|
|
|
|
|
|
// optional::has_value
|
2022-09-19 16:56:35 +00:00
|
|
|
.CaseOfCFGStmt<CXXMemberCallExpr>(
|
|
|
|
isOptionalMemberCallWithName("has_value"),
|
|
|
|
transferOptionalHasValueCall)
|
2022-03-10 08:57:32 +00:00
|
|
|
|
2022-03-10 15:25:42 +00:00
|
|
|
// optional::operator bool
|
2022-09-19 16:56:35 +00:00
|
|
|
.CaseOfCFGStmt<CXXMemberCallExpr>(
|
|
|
|
isOptionalMemberCallWithName("operator bool"),
|
|
|
|
transferOptionalHasValueCall)
|
2022-03-10 15:25:42 +00:00
|
|
|
|
|
|
|
// optional::emplace
|
2022-09-19 16:56:35 +00:00
|
|
|
.CaseOfCFGStmt<CXXMemberCallExpr>(
|
2022-03-14 14:52:35 +00:00
|
|
|
isOptionalMemberCallWithName("emplace"),
|
|
|
|
[](const CXXMemberCallExpr *E, const MatchFinder::MatchResult &,
|
|
|
|
LatticeTransferState &State) {
|
2022-12-21 22:48:04 +00:00
|
|
|
assignOptionalValue(*E->getImplicitObjectArgument(), State.Env,
|
2022-03-14 14:52:35 +00:00
|
|
|
State.Env.getBoolLiteralValue(true));
|
|
|
|
})
|
2022-03-10 15:25:42 +00:00
|
|
|
|
|
|
|
// optional::reset
|
2022-09-19 16:56:35 +00:00
|
|
|
.CaseOfCFGStmt<CXXMemberCallExpr>(
|
2022-03-14 14:52:35 +00:00
|
|
|
isOptionalMemberCallWithName("reset"),
|
|
|
|
[](const CXXMemberCallExpr *E, const MatchFinder::MatchResult &,
|
|
|
|
LatticeTransferState &State) {
|
2022-12-21 22:48:04 +00:00
|
|
|
assignOptionalValue(*E->getImplicitObjectArgument(), State.Env,
|
2022-03-14 14:52:35 +00:00
|
|
|
State.Env.getBoolLiteralValue(false));
|
|
|
|
})
|
2022-03-10 15:25:42 +00:00
|
|
|
|
2022-03-21 12:06:16 +00:00
|
|
|
// optional::swap
|
2022-09-19 16:56:35 +00:00
|
|
|
.CaseOfCFGStmt<CXXMemberCallExpr>(isOptionalMemberCallWithName("swap"),
|
|
|
|
transferSwapCall)
|
2022-03-21 12:06:16 +00:00
|
|
|
|
|
|
|
// std::swap
|
2022-09-19 16:56:35 +00:00
|
|
|
.CaseOfCFGStmt<CallExpr>(isStdSwapCall(), transferStdSwapCall)
|
2022-03-21 12:06:16 +00:00
|
|
|
|
2022-03-21 20:37:04 +00:00
|
|
|
// opt.value_or("").empty()
|
2022-09-19 16:56:35 +00:00
|
|
|
.CaseOfCFGStmt<Expr>(isValueOrStringEmptyCall(),
|
|
|
|
transferValueOrStringEmptyCall)
|
2022-03-21 20:37:04 +00:00
|
|
|
|
|
|
|
// opt.value_or(X) != X
|
2022-09-19 16:56:35 +00:00
|
|
|
.CaseOfCFGStmt<Expr>(isValueOrNotEqX(), transferValueOrNotEqX)
|
2022-03-21 20:37:04 +00:00
|
|
|
|
2022-12-05 20:38:55 +00:00
|
|
|
// Comparisons (==, !=):
|
|
|
|
.CaseOfCFGStmt<CXXOperatorCallExpr>(
|
|
|
|
isComparisonOperatorCall(hasAnyOptionalType(), hasAnyOptionalType()),
|
|
|
|
transferOptionalAndOptionalCmp)
|
|
|
|
.CaseOfCFGStmt<CXXOperatorCallExpr>(
|
|
|
|
isComparisonOperatorCall(hasOptionalType(),
|
|
|
|
unless(hasAnyOptionalType())),
|
|
|
|
[](const clang::CXXOperatorCallExpr *Cmp,
|
|
|
|
const MatchFinder::MatchResult &, LatticeTransferState &State) {
|
|
|
|
transferOptionalAndValueCmp(Cmp, Cmp->getArg(0), State.Env);
|
|
|
|
})
|
|
|
|
.CaseOfCFGStmt<CXXOperatorCallExpr>(
|
|
|
|
isComparisonOperatorCall(unless(hasAnyOptionalType()),
|
|
|
|
hasOptionalType()),
|
|
|
|
[](const clang::CXXOperatorCallExpr *Cmp,
|
|
|
|
const MatchFinder::MatchResult &, LatticeTransferState &State) {
|
|
|
|
transferOptionalAndValueCmp(Cmp, Cmp->getArg(1), State.Env);
|
|
|
|
})
|
|
|
|
|
2022-06-01 08:43:30 +00:00
|
|
|
// returns optional
|
2022-09-19 16:56:35 +00:00
|
|
|
.CaseOfCFGStmt<CallExpr>(isCallReturningOptional(),
|
|
|
|
transferCallReturningOptional)
|
2022-06-01 08:43:30 +00:00
|
|
|
|
2022-03-10 08:57:32 +00:00
|
|
|
.Build();
|
|
|
|
}
|
|
|
|
|
[clang][dataflow] Add API to separate analysis from diagnosis
This patch adds an optional `PostVisitStmt` parameter to the `runTypeErasedDataflowAnalysis` function, which does one more pass over all statements in the CFG after a fixpoint is reached. It then defines a `diagnose` method for the optional model in a new `UncheckedOptionalAccessDiagnosis` class, but only integrates that into the tests and not the actual optional check for `clang-tidy`. That will be done in a followup patch.
The primary motivation is to separate the implementation of the unchecked optional access check into two parts, to allow for further refactoring of just the model part later, while leaving the checking part alone. Currently there is duplication between the `transferUnwrapCall` and `diagnoseUnwrapCall` functions, but that will be dealt with in the followup.
Because diagnostics are now all gathered into one collection rather than being populated at each program point like when computing a fixpoint, this patch removes the usage of `Pair` and `UnorderedElementsAre` from the optional model tests, and instead modifies all their expectations to simply check the stringified set of diagnostics against a single string, either `"safe"` or some concatenation of `"unsafe: input.cc:y:x"`. This is not ideal as it loses any connection to the `/*[[check]]*/` annotations in the source strings, but it does still retain the source locations from the diagnostic strings themselves.
Reviewed By: sgatev, gribozavr2, xazax.hun
Differential Revision: https://reviews.llvm.org/D127898
2022-06-29 19:18:10 +00:00
|
|
|
std::vector<SourceLocation> diagnoseUnwrapCall(const Expr *UnwrapExpr,
|
|
|
|
const Expr *ObjectExpr,
|
|
|
|
const Environment &Env) {
|
|
|
|
if (auto *OptionalVal =
|
|
|
|
Env.getValue(*ObjectExpr, SkipPast::ReferenceThenPointer)) {
|
|
|
|
auto *Prop = OptionalVal->getProperty("has_value");
|
|
|
|
if (auto *HasValueVal = cast_or_null<BoolValue>(Prop)) {
|
|
|
|
if (Env.flowConditionImplies(*HasValueVal))
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Record that this unwrap is *not* provably safe.
|
|
|
|
// FIXME: include either the name of the optional (if applicable) or a source
|
|
|
|
// range of the access for easier interpretation of the result.
|
|
|
|
return {ObjectExpr->getBeginLoc()};
|
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
2022-09-19 16:56:35 +00:00
|
|
|
return CFGMatchSwitchBuilder<const Environment, std::vector<SourceLocation>>()
|
[clang][dataflow] Add API to separate analysis from diagnosis
This patch adds an optional `PostVisitStmt` parameter to the `runTypeErasedDataflowAnalysis` function, which does one more pass over all statements in the CFG after a fixpoint is reached. It then defines a `diagnose` method for the optional model in a new `UncheckedOptionalAccessDiagnosis` class, but only integrates that into the tests and not the actual optional check for `clang-tidy`. That will be done in a followup patch.
The primary motivation is to separate the implementation of the unchecked optional access check into two parts, to allow for further refactoring of just the model part later, while leaving the checking part alone. Currently there is duplication between the `transferUnwrapCall` and `diagnoseUnwrapCall` functions, but that will be dealt with in the followup.
Because diagnostics are now all gathered into one collection rather than being populated at each program point like when computing a fixpoint, this patch removes the usage of `Pair` and `UnorderedElementsAre` from the optional model tests, and instead modifies all their expectations to simply check the stringified set of diagnostics against a single string, either `"safe"` or some concatenation of `"unsafe: input.cc:y:x"`. This is not ideal as it loses any connection to the `/*[[check]]*/` annotations in the source strings, but it does still retain the source locations from the diagnostic strings themselves.
Reviewed By: sgatev, gribozavr2, xazax.hun
Differential Revision: https://reviews.llvm.org/D127898
2022-06-29 19:18:10 +00:00
|
|
|
// optional::value
|
2022-09-19 16:56:35 +00:00
|
|
|
.CaseOfCFGStmt<CXXMemberCallExpr>(
|
[clang][dataflow] Add API to separate analysis from diagnosis
This patch adds an optional `PostVisitStmt` parameter to the `runTypeErasedDataflowAnalysis` function, which does one more pass over all statements in the CFG after a fixpoint is reached. It then defines a `diagnose` method for the optional model in a new `UncheckedOptionalAccessDiagnosis` class, but only integrates that into the tests and not the actual optional check for `clang-tidy`. That will be done in a followup patch.
The primary motivation is to separate the implementation of the unchecked optional access check into two parts, to allow for further refactoring of just the model part later, while leaving the checking part alone. Currently there is duplication between the `transferUnwrapCall` and `diagnoseUnwrapCall` functions, but that will be dealt with in the followup.
Because diagnostics are now all gathered into one collection rather than being populated at each program point like when computing a fixpoint, this patch removes the usage of `Pair` and `UnorderedElementsAre` from the optional model tests, and instead modifies all their expectations to simply check the stringified set of diagnostics against a single string, either `"safe"` or some concatenation of `"unsafe: input.cc:y:x"`. This is not ideal as it loses any connection to the `/*[[check]]*/` annotations in the source strings, but it does still retain the source locations from the diagnostic strings themselves.
Reviewed By: sgatev, gribozavr2, xazax.hun
Differential Revision: https://reviews.llvm.org/D127898
2022-06-29 19:18:10 +00:00
|
|
|
valueCall(IgnorableOptional),
|
|
|
|
[](const CXXMemberCallExpr *E, const MatchFinder::MatchResult &,
|
|
|
|
const Environment &Env) {
|
|
|
|
return diagnoseUnwrapCall(E, E->getImplicitObjectArgument(), Env);
|
|
|
|
})
|
|
|
|
|
|
|
|
// optional::operator*, optional::operator->
|
2022-09-19 16:56:35 +00:00
|
|
|
.CaseOfCFGStmt<CallExpr>(
|
[clang][dataflow] Add API to separate analysis from diagnosis
This patch adds an optional `PostVisitStmt` parameter to the `runTypeErasedDataflowAnalysis` function, which does one more pass over all statements in the CFG after a fixpoint is reached. It then defines a `diagnose` method for the optional model in a new `UncheckedOptionalAccessDiagnosis` class, but only integrates that into the tests and not the actual optional check for `clang-tidy`. That will be done in a followup patch.
The primary motivation is to separate the implementation of the unchecked optional access check into two parts, to allow for further refactoring of just the model part later, while leaving the checking part alone. Currently there is duplication between the `transferUnwrapCall` and `diagnoseUnwrapCall` functions, but that will be dealt with in the followup.
Because diagnostics are now all gathered into one collection rather than being populated at each program point like when computing a fixpoint, this patch removes the usage of `Pair` and `UnorderedElementsAre` from the optional model tests, and instead modifies all their expectations to simply check the stringified set of diagnostics against a single string, either `"safe"` or some concatenation of `"unsafe: input.cc:y:x"`. This is not ideal as it loses any connection to the `/*[[check]]*/` annotations in the source strings, but it does still retain the source locations from the diagnostic strings themselves.
Reviewed By: sgatev, gribozavr2, xazax.hun
Differential Revision: https://reviews.llvm.org/D127898
2022-06-29 19:18:10 +00:00
|
|
|
valueOperatorCall(IgnorableOptional),
|
|
|
|
[](const CallExpr *E, const MatchFinder::MatchResult &,
|
|
|
|
const Environment &Env) {
|
|
|
|
return diagnoseUnwrapCall(E, E->getArg(0), Env);
|
|
|
|
})
|
|
|
|
.Build();
|
|
|
|
}
|
|
|
|
|
2022-03-10 08:57:32 +00:00
|
|
|
} // namespace
|
|
|
|
|
2022-03-06 19:01:46 +00:00
|
|
|
ast_matchers::DeclarationMatcher
|
|
|
|
UncheckedOptionalAccessModel::optionalClassDecl() {
|
|
|
|
return optionalClass();
|
|
|
|
}
|
|
|
|
|
2022-12-14 13:54:38 +00:00
|
|
|
UncheckedOptionalAccessModel::UncheckedOptionalAccessModel(ASTContext &Ctx)
|
2022-06-29 20:10:31 +00:00
|
|
|
: DataflowAnalysis<UncheckedOptionalAccessModel, NoopLattice>(Ctx),
|
2022-12-14 13:54:38 +00:00
|
|
|
TransferMatchSwitch(buildTransferMatchSwitch()) {}
|
2022-03-10 08:57:32 +00:00
|
|
|
|
2022-09-19 16:56:35 +00:00
|
|
|
void UncheckedOptionalAccessModel::transfer(const CFGElement *Elt,
|
|
|
|
NoopLattice &L, Environment &Env) {
|
2022-03-10 08:57:32 +00:00
|
|
|
LatticeTransferState State(L, Env);
|
2022-09-19 16:56:35 +00:00
|
|
|
TransferMatchSwitch(*Elt, getASTContext(), State);
|
2022-03-10 08:57:32 +00:00
|
|
|
}
|
|
|
|
|
2022-11-03 12:44:10 +00:00
|
|
|
ComparisonResult UncheckedOptionalAccessModel::compare(
|
|
|
|
QualType Type, const Value &Val1, const Environment &Env1,
|
|
|
|
const Value &Val2, const Environment &Env2) {
|
|
|
|
if (!isOptionalType(Type))
|
|
|
|
return ComparisonResult::Unknown;
|
[clang][dataflow] In optional model, implement `widen` and make `compare` sound.
This patch includes two related changes:
1. Rewrite `compare` operation to be sound. Current version checks for equality
of `isNonEmptyOptional` on both values, judging the values `Same` when the
results are equal. While that works when both are true, it is problematic when
they are both false, because there are four cases in which that's can occur:
both empty, one empty and one unknown (which is two cases), and both unknown. In
the latter three cases, it is unsound to judge them `Same`. This patch changes
`compare` to explicitly check for case of `both empty` and then judge any other
case `Different`.
2. With the change to `compare`, a number of common cases will no longer
terminate. So, we also implement widening to properly handle those cases and
recover termination.
Drive-by: improve performance of `merge` operation.
Of the new tests, the code before the patch fails
* ReassignValueInLoopToSetUnsafe, and
* ReassignValueInLoopToUnknownUnsafe.
Differential Revision: https://reviews.llvm.org/D140344
2022-12-15 15:11:21 +00:00
|
|
|
bool MustNonEmpty1 = isNonEmptyOptional(Val1, Env1);
|
|
|
|
bool MustNonEmpty2 = isNonEmptyOptional(Val2, Env2);
|
|
|
|
if (MustNonEmpty1 && MustNonEmpty2) return ComparisonResult::Same;
|
|
|
|
// If exactly one is true, then they're different, no reason to check whether
|
|
|
|
// they're definitely empty.
|
|
|
|
if (MustNonEmpty1 || MustNonEmpty2) return ComparisonResult::Different;
|
|
|
|
// Check if they're both definitely empty.
|
|
|
|
return (isEmptyOptional(Val1, Env1) && isEmptyOptional(Val2, Env2))
|
2022-11-03 12:44:10 +00:00
|
|
|
? ComparisonResult::Same
|
|
|
|
: ComparisonResult::Different;
|
2022-05-18 21:57:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
bool UncheckedOptionalAccessModel::merge(QualType Type, const Value &Val1,
|
|
|
|
const Environment &Env1,
|
|
|
|
const Value &Val2,
|
|
|
|
const Environment &Env2,
|
|
|
|
Value &MergedVal,
|
|
|
|
Environment &MergedEnv) {
|
2022-11-03 12:44:10 +00:00
|
|
|
if (!isOptionalType(Type))
|
2022-05-18 21:57:40 +00:00
|
|
|
return true;
|
[clang][dataflow] In optional model, implement `widen` and make `compare` sound.
This patch includes two related changes:
1. Rewrite `compare` operation to be sound. Current version checks for equality
of `isNonEmptyOptional` on both values, judging the values `Same` when the
results are equal. While that works when both are true, it is problematic when
they are both false, because there are four cases in which that's can occur:
both empty, one empty and one unknown (which is two cases), and both unknown. In
the latter three cases, it is unsound to judge them `Same`. This patch changes
`compare` to explicitly check for case of `both empty` and then judge any other
case `Different`.
2. With the change to `compare`, a number of common cases will no longer
terminate. So, we also implement widening to properly handle those cases and
recover termination.
Drive-by: improve performance of `merge` operation.
Of the new tests, the code before the patch fails
* ReassignValueInLoopToSetUnsafe, and
* ReassignValueInLoopToUnknownUnsafe.
Differential Revision: https://reviews.llvm.org/D140344
2022-12-15 15:11:21 +00:00
|
|
|
// FIXME: uses same approach as join for `BoolValues`. Requires non-const
|
|
|
|
// values, though, so will require updating the interface.
|
2022-05-18 21:57:40 +00:00
|
|
|
auto &HasValueVal = MergedEnv.makeAtomicBoolValue();
|
[clang][dataflow] In optional model, implement `widen` and make `compare` sound.
This patch includes two related changes:
1. Rewrite `compare` operation to be sound. Current version checks for equality
of `isNonEmptyOptional` on both values, judging the values `Same` when the
results are equal. While that works when both are true, it is problematic when
they are both false, because there are four cases in which that's can occur:
both empty, one empty and one unknown (which is two cases), and both unknown. In
the latter three cases, it is unsound to judge them `Same`. This patch changes
`compare` to explicitly check for case of `both empty` and then judge any other
case `Different`.
2. With the change to `compare`, a number of common cases will no longer
terminate. So, we also implement widening to properly handle those cases and
recover termination.
Drive-by: improve performance of `merge` operation.
Of the new tests, the code before the patch fails
* ReassignValueInLoopToSetUnsafe, and
* ReassignValueInLoopToUnknownUnsafe.
Differential Revision: https://reviews.llvm.org/D140344
2022-12-15 15:11:21 +00:00
|
|
|
bool MustNonEmpty1 = isNonEmptyOptional(Val1, Env1);
|
|
|
|
bool MustNonEmpty2 = isNonEmptyOptional(Val2, Env2);
|
|
|
|
if (MustNonEmpty1 && MustNonEmpty2)
|
2022-05-18 21:57:40 +00:00
|
|
|
MergedEnv.addToFlowCondition(HasValueVal);
|
[clang][dataflow] In optional model, implement `widen` and make `compare` sound.
This patch includes two related changes:
1. Rewrite `compare` operation to be sound. Current version checks for equality
of `isNonEmptyOptional` on both values, judging the values `Same` when the
results are equal. While that works when both are true, it is problematic when
they are both false, because there are four cases in which that's can occur:
both empty, one empty and one unknown (which is two cases), and both unknown. In
the latter three cases, it is unsound to judge them `Same`. This patch changes
`compare` to explicitly check for case of `both empty` and then judge any other
case `Different`.
2. With the change to `compare`, a number of common cases will no longer
terminate. So, we also implement widening to properly handle those cases and
recover termination.
Drive-by: improve performance of `merge` operation.
Of the new tests, the code before the patch fails
* ReassignValueInLoopToSetUnsafe, and
* ReassignValueInLoopToUnknownUnsafe.
Differential Revision: https://reviews.llvm.org/D140344
2022-12-15 15:11:21 +00:00
|
|
|
else if (
|
|
|
|
// Only make the costly calls to `isEmptyOptional` if we got "unknown"
|
|
|
|
// (false) for both calls to `isNonEmptyOptional`.
|
|
|
|
!MustNonEmpty1 && !MustNonEmpty2 && isEmptyOptional(Val1, Env1) &&
|
|
|
|
isEmptyOptional(Val2, Env2))
|
2022-05-18 21:57:40 +00:00
|
|
|
MergedEnv.addToFlowCondition(MergedEnv.makeNot(HasValueVal));
|
|
|
|
setHasValue(MergedVal, HasValueVal);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
[clang][dataflow] In optional model, implement `widen` and make `compare` sound.
This patch includes two related changes:
1. Rewrite `compare` operation to be sound. Current version checks for equality
of `isNonEmptyOptional` on both values, judging the values `Same` when the
results are equal. While that works when both are true, it is problematic when
they are both false, because there are four cases in which that's can occur:
both empty, one empty and one unknown (which is two cases), and both unknown. In
the latter three cases, it is unsound to judge them `Same`. This patch changes
`compare` to explicitly check for case of `both empty` and then judge any other
case `Different`.
2. With the change to `compare`, a number of common cases will no longer
terminate. So, we also implement widening to properly handle those cases and
recover termination.
Drive-by: improve performance of `merge` operation.
Of the new tests, the code before the patch fails
* ReassignValueInLoopToSetUnsafe, and
* ReassignValueInLoopToUnknownUnsafe.
Differential Revision: https://reviews.llvm.org/D140344
2022-12-15 15:11:21 +00:00
|
|
|
Value *UncheckedOptionalAccessModel::widen(QualType Type, Value &Prev,
|
|
|
|
const Environment &PrevEnv,
|
|
|
|
Value &Current,
|
|
|
|
Environment &CurrentEnv) {
|
|
|
|
switch (compare(Type, Prev, PrevEnv, Current, CurrentEnv)) {
|
|
|
|
case ComparisonResult::Same:
|
|
|
|
return &Prev;
|
|
|
|
case ComparisonResult::Different:
|
|
|
|
if (auto *PrevHasVal =
|
|
|
|
cast_or_null<BoolValue>(Prev.getProperty("has_value"))) {
|
|
|
|
if (isa<TopBoolValue>(PrevHasVal))
|
|
|
|
return &Prev;
|
|
|
|
}
|
|
|
|
if (auto *CurrentHasVal =
|
|
|
|
cast_or_null<BoolValue>(Current.getProperty("has_value"))) {
|
|
|
|
if (isa<TopBoolValue>(CurrentHasVal))
|
|
|
|
return &Current;
|
|
|
|
}
|
|
|
|
return &createOptionalValue(CurrentEnv, CurrentEnv.makeTopBoolValue());
|
|
|
|
case ComparisonResult::Unknown:
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
llvm_unreachable("all cases covered in switch");
|
|
|
|
}
|
|
|
|
|
[clang][dataflow] Add API to separate analysis from diagnosis
This patch adds an optional `PostVisitStmt` parameter to the `runTypeErasedDataflowAnalysis` function, which does one more pass over all statements in the CFG after a fixpoint is reached. It then defines a `diagnose` method for the optional model in a new `UncheckedOptionalAccessDiagnosis` class, but only integrates that into the tests and not the actual optional check for `clang-tidy`. That will be done in a followup patch.
The primary motivation is to separate the implementation of the unchecked optional access check into two parts, to allow for further refactoring of just the model part later, while leaving the checking part alone. Currently there is duplication between the `transferUnwrapCall` and `diagnoseUnwrapCall` functions, but that will be dealt with in the followup.
Because diagnostics are now all gathered into one collection rather than being populated at each program point like when computing a fixpoint, this patch removes the usage of `Pair` and `UnorderedElementsAre` from the optional model tests, and instead modifies all their expectations to simply check the stringified set of diagnostics against a single string, either `"safe"` or some concatenation of `"unsafe: input.cc:y:x"`. This is not ideal as it loses any connection to the `/*[[check]]*/` annotations in the source strings, but it does still retain the source locations from the diagnostic strings themselves.
Reviewed By: sgatev, gribozavr2, xazax.hun
Differential Revision: https://reviews.llvm.org/D127898
2022-06-29 19:18:10 +00:00
|
|
|
UncheckedOptionalAccessDiagnoser::UncheckedOptionalAccessDiagnoser(
|
|
|
|
UncheckedOptionalAccessModelOptions Options)
|
|
|
|
: DiagnoseMatchSwitch(buildDiagnoseMatchSwitch(Options)) {}
|
|
|
|
|
|
|
|
std::vector<SourceLocation> UncheckedOptionalAccessDiagnoser::diagnose(
|
2022-09-19 16:56:35 +00:00
|
|
|
ASTContext &Ctx, const CFGElement *Elt, const Environment &Env) {
|
|
|
|
return DiagnoseMatchSwitch(*Elt, Ctx, Env);
|
[clang][dataflow] Add API to separate analysis from diagnosis
This patch adds an optional `PostVisitStmt` parameter to the `runTypeErasedDataflowAnalysis` function, which does one more pass over all statements in the CFG after a fixpoint is reached. It then defines a `diagnose` method for the optional model in a new `UncheckedOptionalAccessDiagnosis` class, but only integrates that into the tests and not the actual optional check for `clang-tidy`. That will be done in a followup patch.
The primary motivation is to separate the implementation of the unchecked optional access check into two parts, to allow for further refactoring of just the model part later, while leaving the checking part alone. Currently there is duplication between the `transferUnwrapCall` and `diagnoseUnwrapCall` functions, but that will be dealt with in the followup.
Because diagnostics are now all gathered into one collection rather than being populated at each program point like when computing a fixpoint, this patch removes the usage of `Pair` and `UnorderedElementsAre` from the optional model tests, and instead modifies all their expectations to simply check the stringified set of diagnostics against a single string, either `"safe"` or some concatenation of `"unsafe: input.cc:y:x"`. This is not ideal as it loses any connection to the `/*[[check]]*/` annotations in the source strings, but it does still retain the source locations from the diagnostic strings themselves.
Reviewed By: sgatev, gribozavr2, xazax.hun
Differential Revision: https://reviews.llvm.org/D127898
2022-06-29 19:18:10 +00:00
|
|
|
}
|
|
|
|
|
2022-03-10 08:57:32 +00:00
|
|
|
} // namespace dataflow
|
|
|
|
} // namespace clang
|