[clang] [clang-tools-extra] Typestate and class/method attributes working together for std::optional like classes (PR #224038)
Kay Hicketts via cfe-commits
cfe-commits at lists.llvm.org
Tue Oct 6 04:03:27 PDT 2026
https://github.com/KHicketts updated https://github.com/llvm/llvm-project/pull/224038
>From 611591f1dbda2e1a60894d86bac6b3167b6496bd Mon Sep 17 00:00:00 2001
From: khickett <khicketts at bloomberg.net>
Date: Mon, 5 Oct 2026 13:23:20 +0100
Subject: [PATCH] analyze_as_class / analyze_as_method with typestate
---
.../bugprone/unchecked-optional-access.cpp | 67 +++++++
clang/include/clang/Basic/Attr.td | 16 ++
.../Models/UncheckedOptionalAccessModel.cpp | 188 +++++++++++++++---
clang/lib/Sema/SemaDeclAttr.cpp | 53 +++++
...a-attribute-supported-attributes-list.test | 2 +
.../UncheckedOptionalAccessModelTest.cpp | 50 ++++-
6 files changed, 349 insertions(+), 27 deletions(-)
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access.cpp b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access.cpp
index 337474bdf7535..5d90f0f6a8978 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access.cpp
@@ -384,3 +384,70 @@ void foo() {
if (!vec.empty())
vec[0].x = 0;
}
+
+// Custom optional-like type using analyze_as_class / analyze_as_method attributes.
+template <typename T>
+class [[clang::analyze_as_class("std::optional")]] MyCustomOptional {
+public:
+ bool has_value() const;
+
+ [[clang::analyze_as_method("has_value")]] bool HasValue() const;
+
+ const T& value();
+
+ [[clang::analyze_as_method("value")]] const T &GetValue();
+
+ void reset();
+
+ [[clang::analyze_as_method("reset")]] void RemoveValue();
+
+ T &emplace(T val);
+
+ [[clang::analyze_as_method("emplace")]] T &Fill(T val);
+};
+
+void custom_unchecked_access(MyCustomOptional<int> opt) {
+ opt.GetValue();
+ // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: unchecked access to optional value [bugprone-unchecked-optional-access]
+}
+
+void std_named_unchecked_access(MyCustomOptional<int> opt) {
+ opt.value();
+ // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: unchecked access to optional value [bugprone-unchecked-optional-access]
+}
+
+void custom_checked_access(MyCustomOptional<int> opt) {
+ if (opt.HasValue()) {
+ opt.GetValue();
+ }
+}
+
+void std_named_checked_access(MyCustomOptional<int> opt) {
+ if (opt.has_value()) {
+ opt.value();
+ }
+}
+
+void custom_emplace_then_access(MyCustomOptional<int> opt) {
+ opt.Fill(42);
+ opt.GetValue();
+}
+
+void std_named_emplace_then_access(MyCustomOptional<int> opt) {
+ opt.emplace(42);
+ opt.value();
+}
+
+void custom_clear_then_access(MyCustomOptional<int> opt) {
+ opt.Fill(42);
+ opt.RemoveValue();
+ opt.GetValue();
+ // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: unchecked access to optional value [bugprone-unchecked-optional-access]
+}
+
+void std_named_reset_then_access(MyCustomOptional<int> opt) {
+ opt.emplace(42);
+ opt.reset();
+ opt.value();
+ // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: unchecked access to optional value [bugprone-unchecked-optional-access]
+}
diff --git a/clang/include/clang/Basic/Attr.td b/clang/include/clang/Basic/Attr.td
index 4ef0bd2b5ca7c..b453447a12275 100644
--- a/clang/include/clang/Basic/Attr.td
+++ b/clang/include/clang/Basic/Attr.td
@@ -928,6 +928,22 @@ def AlignValue : Attr {
let Documentation = [AlignValueDocs];
}
+def AnalyzeAsClass : InheritableAttr {
+ let Spellings = [Clang<"analyze_as_class">];
+ let Args = [StringArgument<"ClassName">];
+ let Subjects = SubjectList<[Record],
+ ErrorDiag>;
+ let Documentation = [Undocumented];
+}
+
+def AnalyzeAsMethod : InheritableAttr {
+ let Spellings = [Clang<"analyze_as_method">];
+ let Args = [StringArgument<"MethodName">];
+ let Subjects = SubjectList<[CXXMethod],
+ ErrorDiag>;
+ let Documentation = [Undocumented];
+}
+
def AlignMac68k : InheritableAttr {
// This attribute has no spellings as it is only ever created implicitly.
let Spellings = [];
diff --git a/clang/lib/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.cpp b/clang/lib/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.cpp
index 568564fb361f4..375e15371e36a 100644
--- a/clang/lib/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.cpp
+++ b/clang/lib/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.cpp
@@ -13,6 +13,7 @@
#include "clang/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.h"
#include "clang/AST/ASTContext.h"
+#include "clang/AST/Attr.h"
#include "clang/AST/DeclCXX.h"
#include "clang/AST/Expr.h"
#include "clang/AST/ExprCXX.h"
@@ -30,6 +31,7 @@
#include "clang/Analysis/FlowSensitive/Value.h"
#include "clang/Basic/OperatorKinds.h"
#include "clang/Basic/SourceLocation.h"
+#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/ErrorHandling.h"
#include <cassert>
@@ -70,6 +72,8 @@ static bool hasOptionalClassName(const CXXRecordDecl &RD) {
return false;
}
+ // this code could be removed if base::Optional and folly::Optional used
+ // [[clang::analyze_as_class("std::optional")]]
if (RD.getName() == "Optional") {
// Check whether namespace is "::base" or "::folly".
const auto *N = dyn_cast_or_null<NamespaceDecl>(RD.getDeclContext());
@@ -77,18 +81,25 @@ static bool hasOptionalClassName(const CXXRecordDecl &RD) {
isFullyQualifiedNamespaceEqualTo(*N, "folly"));
}
+ // this code could be removed if Optional_Base used
+ // [[clang::analyze_as_class("std::optional")]]
if (RD.getName() == "Optional_Base") {
const auto *N = dyn_cast_or_null<NamespaceDecl>(RD.getDeclContext());
return N != nullptr &&
isFullyQualifiedNamespaceEqualTo(*N, "bslstl", "BloombergLP");
}
+ // this code could be removed if NullableValue used
+ // [[clang::analyze_as_class("std::optional")]]
if (RD.getName() == "NullableValue") {
const auto *N = dyn_cast_or_null<NamespaceDecl>(RD.getDeclContext());
return N != nullptr &&
isFullyQualifiedNamespaceEqualTo(*N, "bdlb", "BloombergLP");
}
+ if (RD.hasAttr<AnalyzeAsClassAttr>())
+ return true;
+
return false;
}
@@ -226,6 +237,55 @@ AST_MATCHER(CXXOperatorCallExpr, hasOptionalOperatorObjectType) {
return hasReceiverTypeDesugaringToOptional(Node.getArg(0));
}
+AST_MATCHER_P(NamedDecl, hasAnalyzeAsMethodName, std::string, MethodName) {
+ if (const auto *MD = dyn_cast<CXXMethodDecl>(&Node)) {
+ if (const auto *Attr = MD->getAttr<AnalyzeAsMethodAttr>()) {
+ StringRef AttrValue = Attr->getMethodName();
+ return AttrValue == MethodName;
+ }
+ }
+ return false;
+}
+
+AST_MATCHER_P(NamedDecl, hasSetTypestateAttr, SetTypestateAttr::ConsumedState,
+ State) {
+ if (const auto *MD = dyn_cast<CXXMethodDecl>(&Node)) {
+ if (const auto *Attr = MD->getAttr<SetTypestateAttr>()) {
+ return Attr->getNewState() == State;
+ }
+ }
+ return false;
+}
+
+AST_MATCHER_P(NamedDecl, hasTestTypestateAttr, TestTypestateAttr::ConsumedState,
+ State) {
+ if (const auto *MD = dyn_cast<CXXMethodDecl>(&Node)) {
+ if (const auto *Attr = MD->getAttr<TestTypestateAttr>()) {
+ return Attr->getTestState() == State;
+ }
+ }
+ return false;
+}
+
+AST_MATCHER_P(NamedDecl, hasReturnTypestateAttr,
+ ReturnTypestateAttr::ConsumedState, State) {
+ if (const auto *MD = dyn_cast<CXXMethodDecl>(&Node)) {
+ if (const auto *Attr = MD->getAttr<ReturnTypestateAttr>()) {
+ return Attr->getState() == State;
+ }
+ }
+ return false;
+}
+
+AST_MATCHER(NamedDecl, hasCallableWhenAttr) {
+ if (const auto *MD = dyn_cast<CXXMethodDecl>(&Node)) {
+ if (MD->getAttr<CallableWhenAttr>()) {
+ return true;
+ }
+ }
+ return false;
+}
+
auto isOptionalMemberCallWithNameMatcher(
ast_matchers::internal::Matcher<NamedDecl> matcher,
const std::optional<StatementMatcher> &Ignorable = std::nullopt) {
@@ -266,14 +326,18 @@ auto inPlaceClass() {
auto isOptionalNulloptConstructor() {
return cxxConstructExpr(
- hasDeclaration(cxxConstructorDecl(parameterCountIs(1),
- hasParameter(0, hasNulloptType()))),
+ hasDeclaration(cxxConstructorDecl(
+ anyOf(allOf(parameterCountIs(1), hasParameter(0, hasNulloptType())),
+ hasReturnTypestateAttr(ReturnTypestateAttr::Consumed)))),
hasOptionalOrDerivedType());
}
auto isOptionalInPlaceConstructor() {
- return cxxConstructExpr(hasArgument(0, hasType(inPlaceClass())),
- hasOptionalOrDerivedType());
+ return cxxConstructExpr(
+ anyOf(hasArgument(0, hasType(inPlaceClass())),
+ hasDeclaration(cxxConstructorDecl(
+ hasReturnTypestateAttr(ReturnTypestateAttr::Unconsumed)))),
+ hasOptionalOrDerivedType());
}
auto isOptionalValueOrConversionConstructor() {
@@ -289,7 +353,8 @@ auto isOptionalValueOrConversionAssignment() {
hasOverloadedOperatorName("="),
callee(cxxMethodDecl(ofClass(optionalOrDerivedClass()))),
unless(hasDeclaration(cxxMethodDecl(
- anyOf(isCopyAssignmentOperator(), isMoveAssignmentOperator())))),
+ anyOf(isCopyAssignmentOperator(), isMoveAssignmentOperator(),
+ hasSetTypestateAttr(SetTypestateAttr::Consumed))))),
argumentCountIs(2), hasArgument(1, unless(hasNulloptType())));
}
@@ -297,7 +362,10 @@ auto isOptionalNulloptAssignment() {
return cxxOperatorCallExpr(
hasOverloadedOperatorName("="),
callee(cxxMethodDecl(ofClass(optionalOrDerivedClass()))),
- argumentCountIs(2), hasArgument(1, hasNulloptType()));
+ argumentCountIs(2),
+ anyOf(hasArgument(1, hasNulloptType()),
+ callee(cxxMethodDecl(
+ hasSetTypestateAttr(SetTypestateAttr::Consumed)))));
}
auto isStdSwapCall() {
@@ -339,10 +407,12 @@ auto isValueOrStringEmptyCall() {
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))))
+ cxxMemberCallExpr(
+ on(expr(unless(cxxThisExpr()))),
+ callee(cxxMethodDecl(anyOf(hasName("value_or"),
+ hasAnalyzeAsMethodName("value_or")),
+ ofClass(optionalClass()))),
+ hasArgument(0, stringLiteral(hasSize(0))))
.bind(ValueOrCallID))));
}
@@ -978,8 +1048,9 @@ ignorableOptional(const UncheckedOptionalAccessModelOptions &Options) {
StatementMatcher
valueCall(const std::optional<StatementMatcher> &IgnorableOptional) {
- return isOptionalMemberCallWithNameMatcher(hasName("value"),
- IgnorableOptional);
+ return isOptionalMemberCallWithNameMatcher(
+ anyOf(hasName("value"), hasAnalyzeAsMethodName("value")),
+ IgnorableOptional);
}
StatementMatcher
@@ -1016,7 +1087,8 @@ auto buildTransferMatchSwitch() {
.CaseOfCFGStmt<CXXConstructExpr>(isOptionalValueOrConversionConstructor(),
transferValueOrConversionConstructor)
- // optional::operator=
+ // optional::operator= // for e.g. opt<T> = other_opt<T> - need to check
+ // engagement
.CaseOfCFGStmt<CXXOperatorCallExpr>(
isOptionalValueOrConversionAssignment(),
transferValueOrConversionAssignment)
@@ -1047,12 +1119,48 @@ auto buildTransferMatchSwitch() {
transferArrowOpCall(E, E->getArg(0), State);
})
+ .CaseOfCFGStmt<CXXMemberCallExpr>(
+ isOptionalMemberCallWithNameMatcher(
+ hasSetTypestateAttr(SetTypestateAttr::Unconsumed)),
+ [](const CXXMemberCallExpr *E, const MatchFinder::MatchResult &,
+ LatticeTransferState &State) {
+ if (RecordStorageLocation *Loc =
+ getImplicitObjectLocation(*E, State.Env)) {
+ setHasValue(*Loc, State.Env.getBoolLiteralValue(true), State.Env);
+ }
+ })
+
+ .CaseOfCFGStmt<CXXMemberCallExpr>(
+ isOptionalMemberCallWithNameMatcher(
+ hasSetTypestateAttr(SetTypestateAttr::Consumed)),
+ [](const CXXMemberCallExpr *E, const MatchFinder::MatchResult &,
+ LatticeTransferState &State) {
+ if (RecordStorageLocation *Loc =
+ getImplicitObjectLocation(*E, State.Env)) {
+ setHasValue(*Loc, State.Env.getBoolLiteralValue(false),
+ State.Env);
+ }
+ })
+
+ .CaseOfCFGStmt<CXXMemberCallExpr>(
+ isOptionalMemberCallWithNameMatcher(
+ hasTestTypestateAttr(TestTypestateAttr::Unconsumed)),
+ transferOptionalHasValueCall)
+
+ .CaseOfCFGStmt<CXXMemberCallExpr>(
+ isOptionalMemberCallWithNameMatcher(
+ hasTestTypestateAttr(TestTypestateAttr::Consumed)),
+ transferOptionalIsNullCall)
+
// optional::has_value, optional::hasValue
// Of the supported optionals only folly::Optional uses hasValue, but this
// will also pass for other types
+ // "hasValue" could be removed if folly::Optional used
+ // [[clang::analyze_as_method("has_value")]] on hasValue()
.CaseOfCFGStmt<CXXMemberCallExpr>(
isOptionalMemberCallWithNameMatcher(
- hasAnyName("has_value", "hasValue")),
+ anyOf(hasAnyName("has_value", "hasValue"),
+ hasAnalyzeAsMethodName("has_value"))),
transferOptionalHasValueCall)
// optional::operator bool
@@ -1060,15 +1168,18 @@ auto buildTransferMatchSwitch() {
isOptionalMemberCallWithNameMatcher(hasName("operator bool")),
transferOptionalHasValueCall)
- // NullableValue::isNull
- // Only NullableValue has isNull
+ // this code could be removed if NullableValue used
+ // [[clang::analyze_as_inverse_method("std::optional::has_value")]] on
+ // isNull() *NYI NullableValue::isNull Only NullableValue has isNull
.CaseOfCFGStmt<CXXMemberCallExpr>(
isOptionalMemberCallWithNameMatcher(hasName("isNull")),
transferOptionalIsNullCall)
- // NullableValue::makeValue, NullableValue::makeValueInplace
- // Only NullableValue has these methods, but this
- // will also pass for other types
+ // this code could be removed if NullableValue used
+ // [[clang::analyze_as_method("emplace")]] on makeValue() and
+ // makeValueInplace() NullableValue::makeValue,
+ // NullableValue::makeValueInplace Only NullableValue has these methods,
+ // but this will also pass for other types
.CaseOfCFGStmt<CXXMemberCallExpr>(
isOptionalMemberCallWithNameMatcher(
hasAnyName("makeValue", "makeValueInplace")),
@@ -1082,7 +1193,8 @@ auto buildTransferMatchSwitch() {
// optional::emplace
.CaseOfCFGStmt<CXXMemberCallExpr>(
- isOptionalMemberCallWithNameMatcher(hasName("emplace")),
+ isOptionalMemberCallWithNameMatcher(
+ anyOf(hasName("emplace"), hasAnalyzeAsMethodName("emplace"))),
[](const CXXMemberCallExpr *E, const MatchFinder::MatchResult &,
LatticeTransferState &State) {
if (RecordStorageLocation *Loc =
@@ -1093,7 +1205,8 @@ auto buildTransferMatchSwitch() {
// optional::reset
.CaseOfCFGStmt<CXXMemberCallExpr>(
- isOptionalMemberCallWithNameMatcher(hasName("reset")),
+ isOptionalMemberCallWithNameMatcher(
+ anyOf(hasName("reset"), hasAnalyzeAsMethodName("reset"))),
[](const CXXMemberCallExpr *E, const MatchFinder::MatchResult &,
LatticeTransferState &State) {
if (RecordStorageLocation *Loc =
@@ -1105,7 +1218,8 @@ auto buildTransferMatchSwitch() {
// optional::swap
.CaseOfCFGStmt<CXXMemberCallExpr>(
- isOptionalMemberCallWithNameMatcher(hasName("swap")),
+ isOptionalMemberCallWithNameMatcher(
+ anyOf(hasName("swap"), hasAnalyzeAsMethodName("swap"))),
transferSwapCall)
// std::swap
@@ -1224,6 +1338,28 @@ auto buildTransferMatchSwitch() {
.Build();
}
+llvm::SmallVector<UncheckedOptionalAccessDiagnostic>
+isCallableInState(const CXXMemberCallExpr *E, const Environment &Env) {
+ if (auto *OptionalLoc = cast_or_null<RecordStorageLocation>(
+ getLocBehindPossiblePointer(*E->getImplicitObjectArgument(), Env))) {
+ auto *Prop = Env.getValue(locForHasValue(*OptionalLoc));
+ if (auto *HasValueVal = cast_or_null<BoolValue>(Prop)) {
+ const auto &f = HasValueVal->formula();
+ if (Env.proves(f)) // engaged
+ return {};
+ const auto &Range = CharSourceRange::getTokenRange(E->getSourceRange());
+ if (Env.proves(Env.arena().makeNot(f))) // empty
+ {
+ return {UncheckedOptionalAccessDiagnostic{Range}};
+ }
+ // unknown
+ return {UncheckedOptionalAccessDiagnostic{Range}};
+ }
+ return {};
+ }
+ return {};
+}
+
llvm::SmallVector<UncheckedOptionalAccessDiagnostic>
diagnoseUnwrapCall(const Expr *ObjectExpr, const Environment &Env) {
if (auto *OptionalLoc = cast_or_null<RecordStorageLocation>(
@@ -1258,7 +1394,13 @@ auto buildDiagnoseMatchSwitch(
[](const CallExpr *E, const MatchFinder::MatchResult &,
const Environment &Env) {
return diagnoseUnwrapCall(E->getArg(0), Env);
- });
+ })
+ //
+ .CaseOfCFGStmt<CXXMemberCallExpr>(
+ isOptionalMemberCallWithNameMatcher(hasCallableWhenAttr(),
+ IgnorableOptional),
+ [](const CXXMemberCallExpr *E, const MatchFinder::MatchResult &,
+ const Environment &Env) { return isCallableInState(E, Env); });
auto Builder = Options.IgnoreValueCalls
? std::move(DiagBuilder)
diff --git a/clang/lib/Sema/SemaDeclAttr.cpp b/clang/lib/Sema/SemaDeclAttr.cpp
index eb4a8c2ab9ae0..7db92ab64ee75 100644
--- a/clang/lib/Sema/SemaDeclAttr.cpp
+++ b/clang/lib/Sema/SemaDeclAttr.cpp
@@ -6647,6 +6647,53 @@ static void handleAbiTagAttr(Sema &S, Decl *D, const ParsedAttr &AL) {
AbiTagAttr(S.Context, AL, Tags.data(), Tags.size()));
}
+// for now this only handles std::optional (POC)
+static bool isValidAnalyzeAsClassAttr(Decl *D, StringRef Tag) {
+ if (Tag == "std::optional")
+ return true;
+ return false;
+}
+
+static void handleAnalyzeAsClass(Sema &S, Decl *D, const ParsedAttr &AL) {
+ StringRef Str;
+ if (!S.checkStringLiteralArgumentAttr(AL, 0, Str))
+ return;
+ if (D->hasAttr<AnalyzeAsClassAttr>()) {
+ S.Diag(AL.getLoc(), diag::err_duplicate_attribute) << AL;
+ return;
+ }
+ if (!isValidAnalyzeAsClassAttr(D, Str)) {
+ S.Diag(AL.getLoc(), diag::warn_attribute_type_not_supported) << AL;
+ return;
+ }
+
+ D->addAttr(::new (S.Context) AnalyzeAsClassAttr(S.Context, AL, Str));
+}
+
+// for now this only handles std::optional (POC)
+static bool isValidAnalyzeAsMethodAttr(Decl *D, StringRef Tag) {
+ // no validation is done currently. if someone writes something with a
+ // nonsense name, it simply won't be validated but also no warning will be
+ // emitted would be nice to do something smarter in the real implementation
+ return true;
+}
+
+static void handleAnalyzeAsMethod(Sema &S, Decl *D, const ParsedAttr &AL) {
+ StringRef Str;
+ if (!S.checkStringLiteralArgumentAttr(AL, 0, Str))
+ return;
+ if (D->hasAttr<AnalyzeAsMethodAttr>()) {
+ S.Diag(AL.getLoc(), diag::err_duplicate_attribute) << AL;
+ return;
+ }
+ if (!isValidAnalyzeAsMethodAttr(D, Str)) {
+ S.Diag(AL.getLoc(), diag::warn_attribute_type_not_supported) << AL;
+ return;
+ }
+
+ D->addAttr(::new (S.Context) AnalyzeAsMethodAttr(S.Context, AL, Str));
+}
+
static bool hasBTFDeclTagAttr(Decl *D, StringRef Tag) {
for (const auto *I : D->specific_attrs<BTFDeclTagAttr>()) {
if (I->getBTFDeclTag() == Tag)
@@ -7738,6 +7785,12 @@ ProcessDeclAttribute(Sema &S, Decl *D, const ParsedAttr &AL,
case ParsedAttr::AT_BPFPreserveStaticOffset:
handleSimpleAttribute<BPFPreserveStaticOffsetAttr>(S, D, AL);
break;
+ case ParsedAttr::AT_AnalyzeAsClass:
+ handleAnalyzeAsClass(S, D, AL);
+ break;
+ case ParsedAttr::AT_AnalyzeAsMethod:
+ handleAnalyzeAsMethod(S, D, AL);
+ break;
case ParsedAttr::AT_BTFDeclTag:
handleBTFDeclTagAttr(S, D, AL);
break;
diff --git a/clang/test/Misc/pragma-attribute-supported-attributes-list.test b/clang/test/Misc/pragma-attribute-supported-attributes-list.test
index 8f8c1aa3d3f9c..6116679f4ea0a 100644
--- a/clang/test/Misc/pragma-attribute-supported-attributes-list.test
+++ b/clang/test/Misc/pragma-attribute-supported-attributes-list.test
@@ -15,6 +15,8 @@
// CHECK-NEXT: AlignValue (SubjectMatchRule_variable, SubjectMatchRule_type_alias)
// CHECK-NEXT: AlwaysDestroy (SubjectMatchRule_variable)
// CHECK-NEXT: AlwaysInline (SubjectMatchRule_function)
+// CHECK-NEXT: AnalyzeAsClass (SubjectMatchRule_record)
+// CHECK-NEXT: AnalyzeAsMethod (SubjectMatchRule_function_is_member)
// CHECK-NEXT: Annotate ()
// CHECK-NEXT: AnyX86NoCfCheck (SubjectMatchRule_hasType_functionType)
// CHECK-NEXT: ArcWeakrefUnavailable (SubjectMatchRule_objc_interface)
diff --git a/clang/unittests/Analysis/FlowSensitive/UncheckedOptionalAccessModelTest.cpp b/clang/unittests/Analysis/FlowSensitive/UncheckedOptionalAccessModelTest.cpp
index f34568a072c32..75c12c6883f02 100644
--- a/clang/unittests/Analysis/FlowSensitive/UncheckedOptionalAccessModelTest.cpp
+++ b/clang/unittests/Analysis/FlowSensitive/UncheckedOptionalAccessModelTest.cpp
@@ -946,7 +946,7 @@ TEST_P(UncheckedOptionalAccessTest, OptionalReturnedFromFuntionCall) {
ExpectDiagnosticsFor(
R"(
#include "unchecked_optional_access_test.h"
-
+
struct S {
$ns::$optional<float> x;
} s;
@@ -2860,14 +2860,14 @@ TEST_P(
struct B {
const A& getA() const { return a; }
- void callWithoutChanges() const {
- // no-op
+ void callWithoutChanges() const {
+ // no-op
}
A a;
};
- void target(B& b) {
+ void target(B& b) {
if (b.getA().get().has_value()) {
b.callWithoutChanges(); // calling const method which cannot change A
b.getA().get().value();
@@ -3016,6 +3016,48 @@ TEST_P(UncheckedOptionalAccessTest, AssertFalseGtestMacroWithNullableValue) {
)cc");
}
+TEST_P(UncheckedOptionalAccessTest, TestCustomAttributeBalik) {
+ ExpectDiagnosticsFor(R"cc(
+ #include "unchecked_optional_access_test.h"
+
+ template <typename T>
+ class __attribute__((analyze_as_class("std::optional"))) MyOptional {
+ public:
+ bool has_value() const;
+ T& value();
+ const T& value() const;
+ };
+
+ void target(MyOptional<int> opt) {
+ opt.value(); // [[unsafe]]
+ if (opt.has_value()) {
+ opt.value();
+ }
+ }
+ )cc");
+}
+
+TEST_P(UncheckedOptionalAccessTest, TestCustomAttributeHicketts) {
+ ExpectDiagnosticsFor(R"cc(
+ #include "unchecked_optional_access_test.h"
+
+ template <typename T>
+ class __attribute__((analyze_as_class("std::optional"))) MyOptional {
+ public:
+ __attribute__((analyze_as_method("has_value"))) bool isNotNull() const;
+ __attribute__((analyze_as_method("value"))) T& unwrap();
+ __attribute__((analyze_as_method("value"))) const T& unwrap() const;
+ };
+
+ void target(MyOptional<int> opt) {
+ opt.unwrap(); // [[unsafe]]
+ if (opt.isNotNull()) {
+ opt.unwrap();
+ }
+ }
+ )cc");
+}
+
// FIXME: Add support for:
// - constructors (copy, move)
// - assignment operators (default, copy, move)
More information about the cfe-commits
mailing list