Skip to content

Commit 6761b24

Browse files
authored
[clang][dataflow] Cache accessors for bugprone-unchecked-optional-access (#112605)
Treat calls to zero-param const methods as having stable return values (with a cache) to address issue #58510. The cache is invalidated when non-const methods are called. This uses the infrastructure from PR #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 b81d8e9 commit 6761b24

File tree

4 files changed

+349
-11
lines changed

4 files changed

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

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

@@ -958,7 +1082,8 @@ UncheckedOptionalAccessModel::optionalClassDecl() {
9581082

9591083
UncheckedOptionalAccessModel::UncheckedOptionalAccessModel(ASTContext &Ctx,
9601084
Environment &Env)
961-
: DataflowAnalysis<UncheckedOptionalAccessModel, NoopLattice>(Ctx),
1085+
: DataflowAnalysis<UncheckedOptionalAccessModel,
1086+
UncheckedOptionalAccessLattice>(Ctx),
9621087
TransferMatchSwitch(buildTransferMatchSwitch()) {
9631088
Env.getDataflowAnalysisContext().setSyntheticFieldCallback(
9641089
[&Ctx](QualType Ty) -> llvm::StringMap<QualType> {
@@ -972,7 +1097,8 @@ UncheckedOptionalAccessModel::UncheckedOptionalAccessModel(ASTContext &Ctx,
9721097
}
9731098

9741099
void UncheckedOptionalAccessModel::transfer(const CFGElement &Elt,
975-
NoopLattice &L, Environment &Env) {
1100+
UncheckedOptionalAccessLattice &L,
1101+
Environment &Env) {
9761102
LatticeTransferState State(L, Env);
9771103
TransferMatchSwitch(Elt, getASTContext(), State);
9781104
}

0 commit comments

Comments
 (0)