Skip to content

Commit 57a913c

Browse files
committed
[clang][dataflow] Cache accessors for bugprone-unchecked-optional-access
Treat calls to zero-param const methods as having stable return values (with a cache) to address issue llvm#58510. The cache is invalidated when non-const methods are called. This uses the infrastructure from PR llvm#111006. For now we cache methods returning: - ref to optional - optional by value - booleans We can extend that to pointers to optional in a next change.
1 parent ae778ae commit 57a913c

File tree

4 files changed

+330
-11
lines changed

4 files changed

+330
-11
lines changed

clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-optional-access.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,16 @@ For example:
7676
}
7777
}
7878
79+
Exception: accessor methods
80+
```````````````````````````
81+
82+
The check assumes *accessor* methods of a class are stable, with a heuristic to
83+
determine which methods are accessors. Specifically, parameter-free ``const``
84+
methods are treated as accessors. Note that this is not guaranteed to be safe
85+
-- but, it is widely used (safely) in practice, and so we have chosen to treat
86+
it as generally safe. Calls to non ``const`` methods are assumed to modify
87+
the state of the object and affect the stability of earlier accessor calls.
88+
7989
Rely on invariants of uncommon APIs
8090
-----------------------------------
8191

clang/include/clang/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.h

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include "clang/AST/ASTContext.h"
1818
#include "clang/Analysis/CFG.h"
1919
#include "clang/Analysis/FlowSensitive/CFGMatchSwitch.h"
20+
#include "clang/Analysis/FlowSensitive/CachedConstAccessorsLattice.h"
2021
#include "clang/Analysis/FlowSensitive/DataflowAnalysis.h"
2122
#include "clang/Analysis/FlowSensitive/DataflowEnvironment.h"
2223
#include "clang/Analysis/FlowSensitive/NoopLattice.h"
@@ -39,23 +40,28 @@ struct UncheckedOptionalAccessModelOptions {
3940
bool IgnoreSmartPointerDereference = false;
4041
};
4142

43+
using UncheckedOptionalAccessLattice = CachedConstAccessorsLattice<NoopLattice>;
44+
4245
/// Dataflow analysis that models whether optionals hold values or not.
4346
///
4447
/// Models the `std::optional`, `absl::optional`, and `base::Optional` types.
4548
class UncheckedOptionalAccessModel
46-
: public DataflowAnalysis<UncheckedOptionalAccessModel, NoopLattice> {
49+
: public DataflowAnalysis<UncheckedOptionalAccessModel,
50+
UncheckedOptionalAccessLattice> {
4751
public:
4852
UncheckedOptionalAccessModel(ASTContext &Ctx, dataflow::Environment &Env);
4953

5054
/// Returns a matcher for the optional classes covered by this model.
5155
static ast_matchers::DeclarationMatcher optionalClassDecl();
5256

53-
static NoopLattice initialElement() { return {}; }
57+
static UncheckedOptionalAccessLattice initialElement() { return {}; }
5458

55-
void transfer(const CFGElement &Elt, NoopLattice &L, Environment &Env);
59+
void transfer(const CFGElement &Elt, UncheckedOptionalAccessLattice &L,
60+
Environment &Env);
5661

5762
private:
58-
CFGMatchSwitch<TransferState<NoopLattice>> TransferMatchSwitch;
63+
CFGMatchSwitch<TransferState<UncheckedOptionalAccessLattice>>
64+
TransferMatchSwitch;
5965
};
6066

6167
class UncheckedOptionalAccessDiagnoser {
@@ -65,7 +71,8 @@ class UncheckedOptionalAccessDiagnoser {
6571

6672
llvm::SmallVector<SourceLocation>
6773
operator()(const CFGElement &Elt, ASTContext &Ctx,
68-
const TransferStateForDiagnostics<NoopLattice> &State) {
74+
const TransferStateForDiagnostics<UncheckedOptionalAccessLattice>
75+
&State) {
6976
return DiagnoseMatchSwitch(Elt, Ctx, State.Env);
7077
}
7178

clang/lib/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.cpp

Lines changed: 132 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,14 @@
1717
#include "clang/AST/Expr.h"
1818
#include "clang/AST/ExprCXX.h"
1919
#include "clang/AST/Stmt.h"
20+
#include "clang/AST/Type.h"
2021
#include "clang/ASTMatchers/ASTMatchers.h"
2122
#include "clang/ASTMatchers/ASTMatchersMacros.h"
2223
#include "clang/Analysis/CFG.h"
2324
#include "clang/Analysis/FlowSensitive/CFGMatchSwitch.h"
2425
#include "clang/Analysis/FlowSensitive/DataflowEnvironment.h"
2526
#include "clang/Analysis/FlowSensitive/Formula.h"
26-
#include "clang/Analysis/FlowSensitive/NoopLattice.h"
27+
#include "clang/Analysis/FlowSensitive/RecordOps.h"
2728
#include "clang/Analysis/FlowSensitive/StorageLocation.h"
2829
#include "clang/Analysis/FlowSensitive/Value.h"
2930
#include "clang/Basic/SourceLocation.h"
@@ -104,10 +105,17 @@ static const CXXRecordDecl *getOptionalBaseClass(const CXXRecordDecl *RD) {
104105
return nullptr;
105106
}
106107

108+
static bool isSupportedOptionalType(QualType Ty) {
109+
const CXXRecordDecl *Optional =
110+
getOptionalBaseClass(Ty->getAsCXXRecordDecl());
111+
return Optional != nullptr;
112+
}
113+
107114
namespace {
108115

109116
using namespace ::clang::ast_matchers;
110-
using LatticeTransferState = TransferState<NoopLattice>;
117+
118+
using LatticeTransferState = TransferState<UncheckedOptionalAccessLattice>;
111119

112120
AST_MATCHER(CXXRecordDecl, optionalClass) { return hasOptionalClassName(Node); }
113121

@@ -325,6 +333,19 @@ auto isValueOrNotEqX() {
325333
ComparesToSame(integerLiteral(equals(0)))));
326334
}
327335

336+
auto isZeroParamConstMemberCall() {
337+
return cxxMemberCallExpr(
338+
callee(cxxMethodDecl(parameterCountIs(0), isConst())));
339+
}
340+
341+
auto isNonConstMemberCall() {
342+
return cxxMemberCallExpr(callee(cxxMethodDecl(unless(isConst()))));
343+
}
344+
345+
auto isNonConstMemberOperatorCall() {
346+
return cxxOperatorCallExpr(callee(cxxMethodDecl(unless(isConst()))));
347+
}
348+
328349
auto isCallReturningOptional() {
329350
return callExpr(hasType(qualType(
330351
anyOf(desugarsToOptionalOrDerivedType(),
@@ -523,6 +544,100 @@ void transferCallReturningOptional(const CallExpr *E,
523544
setHasValue(*Loc, State.Env.makeAtomicBoolValue(), State.Env);
524545
}
525546

547+
void handleConstMemberCall(const CallExpr *CE,
548+
dataflow::RecordStorageLocation *RecordLoc,
549+
const MatchFinder::MatchResult &Result,
550+
LatticeTransferState &State) {
551+
// If the const method returns an optional or reference to an optional.
552+
if (RecordLoc != nullptr && isSupportedOptionalType(CE->getType())) {
553+
StorageLocation *Loc =
554+
State.Lattice.getOrCreateConstMethodReturnStorageLocation(
555+
*RecordLoc, CE, State.Env, [&](StorageLocation &Loc) {
556+
setHasValue(cast<RecordStorageLocation>(Loc),
557+
State.Env.makeAtomicBoolValue(), State.Env);
558+
});
559+
if (Loc == nullptr)
560+
return;
561+
if (CE->isGLValue()) {
562+
// If the call to the const method returns a reference to an optional,
563+
// we can use link the call expression to the optional via
564+
// setStorageLocation.
565+
State.Env.setStorageLocation(*CE, *Loc);
566+
} else {
567+
// If the call to the const method returns an optional by value, we
568+
// need to use CopyRecord to link the optional to the result object
569+
// of the call expression.
570+
auto &ResultLoc = State.Env.getResultObjectLocation(*CE);
571+
copyRecord(*cast<RecordStorageLocation>(Loc), ResultLoc, State.Env);
572+
}
573+
return;
574+
}
575+
576+
// Cache if the const method returns a boolean type.
577+
// We may decide to cache other return types in the future.
578+
if (RecordLoc != nullptr && CE->getType()->isBooleanType()) {
579+
Value *Val = State.Lattice.getOrCreateConstMethodReturnValue(*RecordLoc, CE,
580+
State.Env);
581+
if (Val == nullptr)
582+
return;
583+
State.Env.setValue(*CE, *Val);
584+
return;
585+
}
586+
587+
// Perform default handling if the call returns an optional
588+
// but wasn't handled above (if RecordLoc is nullptr).
589+
if (isSupportedOptionalType(CE->getType())) {
590+
transferCallReturningOptional(CE, Result, State);
591+
}
592+
}
593+
594+
void transferValue_ConstMemberCall(const CXXMemberCallExpr *MCE,
595+
const MatchFinder::MatchResult &Result,
596+
LatticeTransferState &State) {
597+
handleConstMemberCall(
598+
MCE, dataflow::getImplicitObjectLocation(*MCE, State.Env), Result, State);
599+
}
600+
601+
void handleNonConstMemberCall(const CallExpr *CE,
602+
dataflow::RecordStorageLocation *RecordLoc,
603+
const MatchFinder::MatchResult &Result,
604+
LatticeTransferState &State) {
605+
// When a non-const member function is called, reset some state.
606+
if (RecordLoc != nullptr) {
607+
for (const auto [Field, FieldLoc] : RecordLoc->children()) {
608+
if (isSupportedOptionalType(Field->getType())) {
609+
auto *FieldRecordLoc = cast_or_null<RecordStorageLocation>(FieldLoc);
610+
if (FieldRecordLoc) {
611+
setHasValue(*FieldRecordLoc, State.Env.makeAtomicBoolValue(),
612+
State.Env);
613+
}
614+
}
615+
}
616+
State.Lattice.clearConstMethodReturnValues(*RecordLoc);
617+
State.Lattice.clearConstMethodReturnStorageLocations(*RecordLoc);
618+
}
619+
620+
// Perform default handling if the call returns an optional.
621+
if (isSupportedOptionalType(CE->getType())) {
622+
transferCallReturningOptional(CE, Result, State);
623+
}
624+
}
625+
626+
void transferValue_NonConstMemberCall(const CXXMemberCallExpr *MCE,
627+
const MatchFinder::MatchResult &Result,
628+
LatticeTransferState &State) {
629+
handleNonConstMemberCall(
630+
MCE, dataflow::getImplicitObjectLocation(*MCE, State.Env), Result, State);
631+
}
632+
633+
void transferValue_NonConstMemberOperatorCall(
634+
const CXXOperatorCallExpr *OCE, const MatchFinder::MatchResult &Result,
635+
LatticeTransferState &State) {
636+
auto *RecordLoc = cast_or_null<dataflow::RecordStorageLocation>(
637+
State.Env.getStorageLocation(*OCE->getArg(0)));
638+
handleNonConstMemberCall(OCE, RecordLoc, Result, State);
639+
}
640+
526641
void constructOptionalValue(const Expr &E, Environment &Env,
527642
BoolValue &HasValueVal) {
528643
RecordStorageLocation &Loc = Env.getResultObjectLocation(E);
@@ -899,7 +1014,17 @@ auto buildTransferMatchSwitch() {
8991014
transferOptionalAndValueCmp(Cmp, Cmp->getArg(1), State.Env);
9001015
})
9011016

902-
// returns optional
1017+
// const accessor calls
1018+
.CaseOfCFGStmt<CXXMemberCallExpr>(isZeroParamConstMemberCall(),
1019+
transferValue_ConstMemberCall)
1020+
// non-const member calls that may modify the state of an object.
1021+
.CaseOfCFGStmt<CXXMemberCallExpr>(isNonConstMemberCall(),
1022+
transferValue_NonConstMemberCall)
1023+
.CaseOfCFGStmt<CXXOperatorCallExpr>(
1024+
isNonConstMemberOperatorCall(),
1025+
transferValue_NonConstMemberOperatorCall)
1026+
1027+
// other cases of returning optional
9031028
.CaseOfCFGStmt<CallExpr>(isCallReturningOptional(),
9041029
transferCallReturningOptional)
9051030

@@ -958,7 +1083,8 @@ UncheckedOptionalAccessModel::optionalClassDecl() {
9581083

9591084
UncheckedOptionalAccessModel::UncheckedOptionalAccessModel(ASTContext &Ctx,
9601085
Environment &Env)
961-
: DataflowAnalysis<UncheckedOptionalAccessModel, NoopLattice>(Ctx),
1086+
: DataflowAnalysis<UncheckedOptionalAccessModel,
1087+
UncheckedOptionalAccessLattice>(Ctx),
9621088
TransferMatchSwitch(buildTransferMatchSwitch()) {
9631089
Env.getDataflowAnalysisContext().setSyntheticFieldCallback(
9641090
[&Ctx](QualType Ty) -> llvm::StringMap<QualType> {
@@ -972,7 +1098,8 @@ UncheckedOptionalAccessModel::UncheckedOptionalAccessModel(ASTContext &Ctx,
9721098
}
9731099

9741100
void UncheckedOptionalAccessModel::transfer(const CFGElement &Elt,
975-
NoopLattice &L, Environment &Env) {
1101+
UncheckedOptionalAccessLattice &L,
1102+
Environment &Env) {
9761103
LatticeTransferState State(L, Env);
9771104
TransferMatchSwitch(Elt, getASTContext(), State);
9781105
}

0 commit comments

Comments
 (0)