llvm-project/clang/lib/Analysis/FlowSensitive/Models/UncheckedStatusOrAccessModel.cpp
Florian Mayer 419fbf906b
[FlowSensitive] [StatusOr] [3/N] Support absl::Status ops (#163868)
`absl::StatusOr::status` allows extraction of the status associated with
a StatusOr value. That can also be used to check whether the StatusOr
has a value or not.

This makes sure code like this is checked properly:

```cpp
int target(absl::StatusOr<int> SOR) {
  if (SOR.status().ok())
    return *SOR;
  return 0;
}
```
2025-10-21 13:50:08 -07:00

365 lines
13 KiB
C++

//===- UncheckedStatusOrAccessModel.cpp -----------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//
#include "clang/Analysis/FlowSensitive/Models/UncheckedStatusOrAccessModel.h"
#include <cassert>
#include <utility>
#include "clang/AST/DeclCXX.h"
#include "clang/AST/DeclTemplate.h"
#include "clang/AST/Expr.h"
#include "clang/AST/ExprCXX.h"
#include "clang/AST/TypeBase.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/ASTMatchers/ASTMatchersInternal.h"
#include "clang/Analysis/CFG.h"
#include "clang/Analysis/FlowSensitive/CFGMatchSwitch.h"
#include "clang/Analysis/FlowSensitive/DataflowAnalysis.h"
#include "clang/Analysis/FlowSensitive/DataflowEnvironment.h"
#include "clang/Analysis/FlowSensitive/MatchSwitch.h"
#include "clang/Analysis/FlowSensitive/RecordOps.h"
#include "clang/Analysis/FlowSensitive/StorageLocation.h"
#include "clang/Analysis/FlowSensitive/Value.h"
#include "clang/Basic/LLVM.h"
#include "clang/Basic/SourceLocation.h"
#include "llvm/ADT/StringMap.h"
namespace clang::dataflow::statusor_model {
namespace {
using ::clang::ast_matchers::MatchFinder;
using ::clang::ast_matchers::StatementMatcher;
} // namespace
static bool namespaceEquals(const NamespaceDecl *NS,
clang::ArrayRef<clang::StringRef> NamespaceNames) {
while (!NamespaceNames.empty() && NS) {
if (NS->getName() != NamespaceNames.consume_back())
return false;
NS = dyn_cast_or_null<NamespaceDecl>(NS->getParent());
}
return NamespaceNames.empty() && !NS;
}
// TODO: move this to a proper place to share with the rest of clang
static bool isTypeNamed(QualType Type, clang::ArrayRef<clang::StringRef> NS,
StringRef Name) {
if (Type.isNull())
return false;
if (auto *RD = Type->getAsRecordDecl())
if (RD->getName() == Name)
if (const auto *N = dyn_cast_or_null<NamespaceDecl>(RD->getDeclContext()))
return namespaceEquals(N, NS);
return false;
}
static bool isStatusOrOperatorBaseType(QualType Type) {
return isTypeNamed(Type, {"absl", "internal_statusor"}, "OperatorBase");
}
static bool isSafeUnwrap(RecordStorageLocation *StatusOrLoc,
const Environment &Env) {
if (!StatusOrLoc)
return false;
auto &StatusLoc = locForStatus(*StatusOrLoc);
auto *OkVal = Env.get<BoolValue>(locForOk(StatusLoc));
return OkVal != nullptr && Env.proves(OkVal->formula());
}
static ClassTemplateSpecializationDecl *
getStatusOrBaseClass(const QualType &Ty) {
auto *RD = Ty->getAsCXXRecordDecl();
if (RD == nullptr)
return nullptr;
if (isStatusOrType(Ty) ||
// In case we are analyzing code under OperatorBase itself that uses
// operator* (e.g. to implement operator->).
isStatusOrOperatorBaseType(Ty))
return cast<ClassTemplateSpecializationDecl>(RD);
if (!RD->hasDefinition())
return nullptr;
for (const auto &Base : RD->bases())
if (auto *QT = getStatusOrBaseClass(Base.getType()))
return QT;
return nullptr;
}
static QualType getStatusOrValueType(ClassTemplateSpecializationDecl *TRD) {
return TRD->getTemplateArgs().get(0).getAsType();
}
static auto ofClassStatus() {
using namespace ::clang::ast_matchers; // NOLINT: Too many names
return ofClass(hasName("::absl::Status"));
}
static auto isStatusMemberCallWithName(llvm::StringRef member_name) {
using namespace ::clang::ast_matchers; // NOLINT: Too many names
return cxxMemberCallExpr(
on(expr(unless(cxxThisExpr()))),
callee(cxxMethodDecl(hasName(member_name), ofClassStatus())));
}
static auto isStatusOrMemberCallWithName(llvm::StringRef member_name) {
using namespace ::clang::ast_matchers; // NOLINT: Too many names
return cxxMemberCallExpr(
on(expr(unless(cxxThisExpr()))),
callee(cxxMethodDecl(
hasName(member_name),
ofClass(anyOf(statusOrClass(), statusOrOperatorBaseClass())))));
}
static auto isStatusOrOperatorCallWithName(llvm::StringRef operator_name) {
using namespace ::clang::ast_matchers; // NOLINT: Too many names
return cxxOperatorCallExpr(
hasOverloadedOperatorName(operator_name),
callee(cxxMethodDecl(
ofClass(anyOf(statusOrClass(), statusOrOperatorBaseClass())))));
}
static auto valueCall() {
using namespace ::clang::ast_matchers; // NOLINT: Too many names
return anyOf(isStatusOrMemberCallWithName("value"),
isStatusOrMemberCallWithName("ValueOrDie"));
}
static auto valueOperatorCall() {
using namespace ::clang::ast_matchers; // NOLINT: Too many names
return expr(anyOf(isStatusOrOperatorCallWithName("*"),
isStatusOrOperatorCallWithName("->")));
}
static auto
buildDiagnoseMatchSwitch(const UncheckedStatusOrAccessModelOptions &Options) {
return CFGMatchSwitchBuilder<const Environment,
llvm::SmallVector<SourceLocation>>()
// StatusOr::value, StatusOr::ValueOrDie
.CaseOfCFGStmt<CXXMemberCallExpr>(
valueCall(),
[](const CXXMemberCallExpr *E,
const ast_matchers::MatchFinder::MatchResult &,
const Environment &Env) {
if (!isSafeUnwrap(getImplicitObjectLocation(*E, Env), Env))
return llvm::SmallVector<SourceLocation>({E->getExprLoc()});
return llvm::SmallVector<SourceLocation>();
})
// StatusOr::operator*, StatusOr::operator->
.CaseOfCFGStmt<CXXOperatorCallExpr>(
valueOperatorCall(),
[](const CXXOperatorCallExpr *E,
const ast_matchers::MatchFinder::MatchResult &,
const Environment &Env) {
RecordStorageLocation *StatusOrLoc =
Env.get<RecordStorageLocation>(*E->getArg(0));
if (!isSafeUnwrap(StatusOrLoc, Env))
return llvm::SmallVector<SourceLocation>({E->getOperatorLoc()});
return llvm::SmallVector<SourceLocation>();
})
.Build();
}
UncheckedStatusOrAccessDiagnoser::UncheckedStatusOrAccessDiagnoser(
UncheckedStatusOrAccessModelOptions Options)
: DiagnoseMatchSwitch(buildDiagnoseMatchSwitch(Options)) {}
llvm::SmallVector<SourceLocation> UncheckedStatusOrAccessDiagnoser::operator()(
const CFGElement &Elt, ASTContext &Ctx,
const TransferStateForDiagnostics<UncheckedStatusOrAccessModel::Lattice>
&State) {
return DiagnoseMatchSwitch(Elt, Ctx, State.Env);
}
BoolValue &initializeStatus(RecordStorageLocation &StatusLoc,
Environment &Env) {
auto &OkVal = Env.makeAtomicBoolValue();
Env.setValue(locForOk(StatusLoc), OkVal);
return OkVal;
}
BoolValue &initializeStatusOr(RecordStorageLocation &StatusOrLoc,
Environment &Env) {
return initializeStatus(locForStatus(StatusOrLoc), Env);
}
clang::ast_matchers::DeclarationMatcher statusOrClass() {
using namespace ::clang::ast_matchers; // NOLINT: Too many names
return classTemplateSpecializationDecl(
hasName("absl::StatusOr"),
hasTemplateArgument(0, refersToType(type().bind("T"))));
}
clang::ast_matchers::DeclarationMatcher statusClass() {
using namespace ::clang::ast_matchers; // NOLINT: Too many names
return cxxRecordDecl(hasName("absl::Status"));
}
clang::ast_matchers::DeclarationMatcher statusOrOperatorBaseClass() {
using namespace ::clang::ast_matchers; // NOLINT: Too many names
return classTemplateSpecializationDecl(
hasName("absl::internal_statusor::OperatorBase"));
}
clang::ast_matchers::TypeMatcher statusOrType() {
using namespace ::clang::ast_matchers; // NOLINT: Too many names
return hasCanonicalType(qualType(hasDeclaration(statusOrClass())));
}
bool isStatusOrType(QualType Type) {
return isTypeNamed(Type, {"absl"}, "StatusOr");
}
bool isStatusType(QualType Type) {
return isTypeNamed(Type, {"absl"}, "Status");
}
llvm::StringMap<QualType> getSyntheticFields(QualType Ty, QualType StatusType,
const CXXRecordDecl &RD) {
if (auto *TRD = getStatusOrBaseClass(Ty))
return {{"status", StatusType}, {"value", getStatusOrValueType(TRD)}};
if (isStatusType(Ty) || (RD.hasDefinition() &&
RD.isDerivedFrom(StatusType->getAsCXXRecordDecl())))
return {{"ok", RD.getASTContext().BoolTy}};
return {};
}
RecordStorageLocation &locForStatus(RecordStorageLocation &StatusOrLoc) {
return cast<RecordStorageLocation>(StatusOrLoc.getSyntheticField("status"));
}
StorageLocation &locForOk(RecordStorageLocation &StatusLoc) {
return StatusLoc.getSyntheticField("ok");
}
BoolValue &valForOk(RecordStorageLocation &StatusLoc, Environment &Env) {
if (auto *Val = Env.get<BoolValue>(locForOk(StatusLoc)))
return *Val;
return initializeStatus(StatusLoc, Env);
}
static void transferStatusOrOkCall(const CXXMemberCallExpr *Expr,
const MatchFinder::MatchResult &,
LatticeTransferState &State) {
RecordStorageLocation *StatusOrLoc =
getImplicitObjectLocation(*Expr, State.Env);
if (StatusOrLoc == nullptr)
return;
auto &OkVal = valForOk(locForStatus(*StatusOrLoc), State.Env);
State.Env.setValue(*Expr, OkVal);
}
static void transferStatusCall(const CXXMemberCallExpr *Expr,
const MatchFinder::MatchResult &,
LatticeTransferState &State) {
RecordStorageLocation *StatusOrLoc =
getImplicitObjectLocation(*Expr, State.Env);
if (StatusOrLoc == nullptr)
return;
RecordStorageLocation &StatusLoc = locForStatus(*StatusOrLoc);
if (State.Env.getValue(locForOk(StatusLoc)) == nullptr)
initializeStatusOr(*StatusOrLoc, State.Env);
if (Expr->isPRValue())
copyRecord(StatusLoc, State.Env.getResultObjectLocation(*Expr), State.Env);
else
State.Env.setStorageLocation(*Expr, StatusLoc);
}
static void transferStatusOkCall(const CXXMemberCallExpr *Expr,
const MatchFinder::MatchResult &,
LatticeTransferState &State) {
RecordStorageLocation *StatusLoc =
getImplicitObjectLocation(*Expr, State.Env);
if (StatusLoc == nullptr)
return;
if (Value *Val = State.Env.getValue(locForOk(*StatusLoc)))
State.Env.setValue(*Expr, *Val);
}
static void transferStatusUpdateCall(const CXXMemberCallExpr *Expr,
const MatchFinder::MatchResult &,
LatticeTransferState &State) {
// S.Update(OtherS) sets S to the error code of OtherS if it is OK,
// otherwise does nothing.
assert(Expr->getNumArgs() == 1);
auto *Arg = Expr->getArg(0);
RecordStorageLocation *ArgRecord =
Arg->isPRValue() ? &State.Env.getResultObjectLocation(*Arg)
: State.Env.get<RecordStorageLocation>(*Arg);
RecordStorageLocation *ThisLoc = getImplicitObjectLocation(*Expr, State.Env);
if (ThisLoc == nullptr || ArgRecord == nullptr)
return;
auto &ThisOkVal = valForOk(*ThisLoc, State.Env);
auto &ArgOkVal = valForOk(*ArgRecord, State.Env);
auto &A = State.Env.arena();
auto &NewVal = State.Env.makeAtomicBoolValue();
State.Env.assume(A.makeImplies(A.makeNot(ThisOkVal.formula()),
A.makeNot(NewVal.formula())));
State.Env.assume(A.makeImplies(NewVal.formula(), ArgOkVal.formula()));
State.Env.setValue(locForOk(*ThisLoc), NewVal);
}
CFGMatchSwitch<LatticeTransferState>
buildTransferMatchSwitch(ASTContext &Ctx,
CFGMatchSwitchBuilder<LatticeTransferState> Builder) {
using namespace ::clang::ast_matchers; // NOLINT: Too many names
return std::move(Builder)
.CaseOfCFGStmt<CXXMemberCallExpr>(isStatusOrMemberCallWithName("ok"),
transferStatusOrOkCall)
.CaseOfCFGStmt<CXXMemberCallExpr>(isStatusOrMemberCallWithName("status"),
transferStatusCall)
.CaseOfCFGStmt<CXXMemberCallExpr>(isStatusMemberCallWithName("ok"),
transferStatusOkCall)
.CaseOfCFGStmt<CXXMemberCallExpr>(isStatusMemberCallWithName("Update"),
transferStatusUpdateCall)
.Build();
}
QualType findStatusType(const ASTContext &Ctx) {
for (Type *Ty : Ctx.getTypes())
if (isStatusType(QualType(Ty, 0)))
return QualType(Ty, 0);
return QualType();
}
UncheckedStatusOrAccessModel::UncheckedStatusOrAccessModel(ASTContext &Ctx,
Environment &Env)
: DataflowAnalysis<UncheckedStatusOrAccessModel,
UncheckedStatusOrAccessModel::Lattice>(Ctx),
TransferMatchSwitch(buildTransferMatchSwitch(Ctx, {})) {
QualType StatusType = findStatusType(Ctx);
Env.getDataflowAnalysisContext().setSyntheticFieldCallback(
[StatusType](QualType Ty) -> llvm::StringMap<QualType> {
CXXRecordDecl *RD = Ty->getAsCXXRecordDecl();
if (RD == nullptr)
return {};
if (auto Fields = getSyntheticFields(Ty, StatusType, *RD);
!Fields.empty())
return Fields;
return {};
});
}
void UncheckedStatusOrAccessModel::transfer(const CFGElement &Elt, Lattice &L,
Environment &Env) {
LatticeTransferState State(L, Env);
TransferMatchSwitch(Elt, getASTContext(), State);
}
} // namespace clang::dataflow::statusor_model