https://github.com/KHicketts updated https://github.com/llvm/llvm-project/pull/224038
>From 611591f1dbda2e1a60894d86bac6b3167b6496bd Mon Sep 17 00:00:00 2001 From: khickett <[email protected]> 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) _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
