[clang] [clang-tools-extra] [llvm] User-defined annotations for clang-tidy analysis POC (PR #195054)
Kay Hicketts via cfe-commits
cfe-commits at lists.llvm.org
Fri May 1 09:51:38 PDT 2026
https://github.com/KHicketts updated https://github.com/llvm/llvm-project/pull/195054
>From 52d11c1082709188e7fa4d8b6995febf1bcf19fa Mon Sep 17 00:00:00 2001
From: khickett <khicketts at bloomberg.net>
Date: Tue, 28 Apr 2026 14:37:52 +0100
Subject: [PATCH 01/10] add sema changes for class attribute
---
clang/lib/Sema/SemaDeclAttr.cpp | 26 ++++++++++++++++++++++++++
1 file changed, 26 insertions(+)
diff --git a/clang/lib/Sema/SemaDeclAttr.cpp b/clang/lib/Sema/SemaDeclAttr.cpp
index e47a30193567f..243c66b474384 100644
--- a/clang/lib/Sema/SemaDeclAttr.cpp
+++ b/clang/lib/Sema/SemaDeclAttr.cpp
@@ -6462,6 +6462,29 @@ 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 isValidAnalyseAsClassAttr(Decl *D, StringRef Tag) {
+ if (Tag == "std::optional")
+ return true;
+ return false;
+}
+
+static void handleAnalyseAsClass(Sema &S, Decl *D, const ParsedAttr &AL) {
+ StringRef Str;
+ if (!S.checkStringLiteralArgumentAttr(AL, 0, Str))
+ return;
+ if (D->hasAttr<AnalyseAsClassAttr>()) {
+ S.Diag(AL.getLoc(), diag::err_duplicate_attribute) << AL;
+ return;
+ }
+ if (!isValidAnalyseAsClassAttr(D, Str)) {
+ S.Diag(AL.getLoc(), diag::warn_attribute_type_not_supported) << AL;
+ return;
+ }
+
+ D->addAttr(::new (S.Context) AnalyseAsClassAttr(S.Context, AL, Str));
+}
+
static bool hasBTFDeclTagAttr(Decl *D, StringRef Tag) {
for (const auto *I : D->specific_attrs<BTFDeclTagAttr>()) {
if (I->getBTFDeclTag() == Tag)
@@ -7551,6 +7574,9 @@ ProcessDeclAttribute(Sema &S, Scope *scope, Decl *D, const ParsedAttr &AL,
case ParsedAttr::AT_BPFPreserveStaticOffset:
handleSimpleAttribute<BPFPreserveStaticOffsetAttr>(S, D, AL);
break;
+ case ParsedAttr::AT_AnalyseAsClass:
+ handleAnalyseAsClass(S, D, AL);
+ break;
case ParsedAttr::AT_BTFDeclTag:
handleBTFDeclTagAttr(S, D, AL);
break;
>From fe028c7292be79ba6f753ebae1039e65aa0032e9 Mon Sep 17 00:00:00 2001
From: khickett <khicketts at bloomberg.net>
Date: Tue, 28 Apr 2026 15:43:52 +0100
Subject: [PATCH 02/10] analyse_as_class works ok
---
.../Models/UncheckedOptionalAccessModel.cpp | 4 +
hicketts_test/hicketts_optional.h | 83 +++++++++++++++++++
hicketts_test/test_hicketts_optional.cpp | 83 +++++++++++++++++++
3 files changed, 170 insertions(+)
create mode 100644 hicketts_test/hicketts_optional.h
create mode 100644 hicketts_test/test_hicketts_optional.cpp
diff --git a/clang/lib/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.cpp b/clang/lib/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.cpp
index 568564fb361f4..317f5034b0145 100644
--- a/clang/lib/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.cpp
+++ b/clang/lib/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.cpp
@@ -12,6 +12,7 @@
//===----------------------------------------------------------------------===//
#include "clang/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.h"
+#include "clang/AST/Attr.h"
#include "clang/AST/ASTContext.h"
#include "clang/AST/DeclCXX.h"
#include "clang/AST/Expr.h"
@@ -89,6 +90,9 @@ static bool hasOptionalClassName(const CXXRecordDecl &RD) {
isFullyQualifiedNamespaceEqualTo(*N, "bdlb", "BloombergLP");
}
+ if (RD.hasAttr<AnalyseAsClassAttr>())
+ return true;
+
return false;
}
diff --git a/hicketts_test/hicketts_optional.h b/hicketts_test/hicketts_optional.h
new file mode 100644
index 0000000000000..b915b01f70290
--- /dev/null
+++ b/hicketts_test/hicketts_optional.h
@@ -0,0 +1,83 @@
+#ifndef HICKETTS_OPTIONAL_H_
+#define HICKETTS_OPTIONAL_H_
+
+/// A custom optional-like type with differently named functions.
+/// Mirrors std::optional semantics but uses its own vocabulary
+/// In order to test implementation of attributes for clang-tidy
+namespace mylib {
+
+struct nothing_t {
+ constexpr explicit nothing_t() {}
+};
+
+constexpr nothing_t nothing;
+
+template <typename T>
+class [[clang::analyse_as_class("std::optional")]] HickettsOptional {
+ T *storage_ = nullptr;
+
+public:
+ constexpr HickettsOptional() noexcept {}
+
+ constexpr HickettsOptional(nothing_t) noexcept {}
+
+ HickettsOptional(const HickettsOptional &) = default;
+
+ HickettsOptional(HickettsOptional &&) = default;
+
+ // Equivalent to std::optional::value()
+ //[[clang_analyse_as_method(std::optional::value)]]
+ const T &unwrap() const & { return *storage_; }
+ T &unwrap() & { return *storage_; }
+ const T &&unwrap() const && { return static_cast<const T &&>(*storage_); }
+ T &&unwrap() && { return static_cast<T &&>(*storage_); }
+
+ const T &value() const & { return *storage_; }
+ T &value() & { return *storage_; }
+ const T &&value() const && { return static_cast<const T &&>(*storage_); }
+ T &&value() && { return static_cast<T &&>(*storage_); }
+
+ // Equivalent to std::optional::operator*()
+ const T &deref() const & { return *storage_; }
+ T &deref() & { return *storage_; }
+
+ // Equivalent to std::optional::operator->()
+ const T* operator ->() const { return storage_; }
+ T* operator ->() { return storage_; }
+ const T *arrow() const { return storage_; }
+ T *arrow() { return storage_; }
+
+ // Equivalent to std::optional::operator bool / has_value()
+ constexpr bool hasValue() const noexcept { return storage_ != nullptr; }
+ constexpr explicit operator bool() const noexcept { return storage_ != nullptr; }
+ constexpr bool isPresent() const noexcept { return storage_ != nullptr; }
+ constexpr bool isEmpty() const noexcept { return storage_ == nullptr; }
+
+ // Equivalent to std::optional::value_or()
+ template <typename U>
+ constexpr T unwrapOr(U &&fallback) const & {
+ return storage_ ? *storage_ : static_cast<T>(fallback);
+ }
+
+ // Equivalent to std::optional::emplace()
+ template <typename... Args>
+ T &construct(Args &&...args) { return *storage_; }
+
+ // Equivalent to std::optional::reset()
+ void clear() noexcept { storage_ = nullptr; }
+
+ // Equivalent to std::optional::swap()
+ void exchange(HickettsOptional &other) noexcept {
+ T *tmp = storage_;
+ storage_ = other.storage_;
+ other.storage_ = tmp;
+ }
+
+ // Assignment
+ template <typename U>
+ HickettsOptional &operator=(const U &u) { return *this; }
+};
+
+} // namespace mylib
+
+#endif // HICKETTS_OPTIONAL_H_
diff --git a/hicketts_test/test_hicketts_optional.cpp b/hicketts_test/test_hicketts_optional.cpp
new file mode 100644
index 0000000000000..fa0153a79f341
--- /dev/null
+++ b/hicketts_test/test_hicketts_optional.cpp
@@ -0,0 +1,83 @@
+// Test cases for mylib::HickettsOptional — a custom optional-like type
+// with differently named functions.
+//
+// Run from hicketts_test/ with:
+// clang-tidy -checks='bugprone-unchecked-optional-access' \
+// test_hicketts_optional.cpp -- -I . -Wno-undefined-inline
+
+#include "hicketts_optional.h"
+
+// --- Unchecked access (should warn if the checker recognises HickettsOptional) ---
+
+static void uncheckedUnwrap(mylib::HickettsOptional<int> &Val) {
+ Val.unwrap(); // unchecked access — may be empty
+}
+
+static void uncheckedValue(mylib::HickettsOptional<int> &Val) {
+ Val.value(); // unchecked access — may be empty
+}
+
+static void uncheckedDeref(mylib::HickettsOptional<int> &Val) {
+ Val.deref(); // unchecked access — may be empty
+}
+
+// --- Checked access (should NOT warn) ---
+
+static void checkedWithBool(mylib::HickettsOptional<int> &Val) {
+ if (Val) {
+ Val.unwrap(); // safe — checked via operator bool
+ }
+}
+
+static void checkedValueWithBool(mylib::HickettsOptional<int> &Val) {
+ if (Val.hasValue()) {
+ Val.value(); // safe — checked via operator bool
+ }
+}
+
+static void checkedWithIsPresent(mylib::HickettsOptional<int> &Val) {
+ if (Val.isPresent()) {
+ Val.unwrap(); // safe — checked via isPresent()
+ }
+}
+
+static void checkedWithIsEmpty(mylib::HickettsOptional<int> &Val) {
+ if (!Val.isEmpty()) {
+ Val.unwrap(); // safe — checked via !isEmpty()
+ }
+}
+
+// --- State changes ---
+
+static void safeAfterConstruct(mylib::HickettsOptional<int> &Val) {
+ Val.construct(42);
+ Val.unwrap(); // safe — just constructed a value
+}
+
+static void unsafeAfterClear(mylib::HickettsOptional<int> &Val) {
+ Val.construct(42);
+ Val.clear();
+ Val.unwrap(); // unsafe — value was cleared
+}
+
+static void unsafeAfterExchange(mylib::HickettsOptional<int> &A,
+ mylib::HickettsOptional<int> &B) {
+ if (A) {
+ A.exchange(B);
+ A.unwrap(); // unsafe — a's state is now unknown
+ }
+}
+
+// --- Guarded paths ---
+
+static void constructCoversEmptyBranch(mylib::HickettsOptional<int> &Val) {
+ if (Val.isEmpty()) {
+ Val.construct(99);
+ }
+ Val.unwrap(); // safe — either was present, or construct filled it
+}
+
+static void unwrapOrIsAlwaysSafe(mylib::HickettsOptional<int> &Val) {
+ int X = Val.unwrapOr(0); // safe — fallback provided
+ (void)X;
+}
>From ec4ea5a3de75bcc71b0140a11f24298d4681fdb0 Mon Sep 17 00:00:00 2001
From: khickett <khicketts at bloomberg.net>
Date: Tue, 28 Apr 2026 16:35:01 +0100
Subject: [PATCH 03/10] partially working for per method analysis
---
.../Models/UncheckedOptionalAccessModel.cpp | 19 +++++++++++--
clang/lib/Sema/SemaDeclAttr.cpp | 27 +++++++++++++++++++
hicketts_test/hicketts_optional.h | 17 ++++++------
3 files changed, 52 insertions(+), 11 deletions(-)
diff --git a/clang/lib/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.cpp b/clang/lib/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.cpp
index 317f5034b0145..f2c9aa756943e 100644
--- a/clang/lib/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.cpp
+++ b/clang/lib/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.cpp
@@ -230,6 +230,20 @@ AST_MATCHER(CXXOperatorCallExpr, hasOptionalOperatorObjectType) {
return hasReceiverTypeDesugaringToOptional(Node.getArg(0));
}
+AST_MATCHER_P(NamedDecl, hasAnalyseAsMethodName, std::string, MethodName) {
+ if (const auto *MD = dyn_cast<CXXMethodDecl>(&Node)) {
+ if (const auto *Attr = MD->getAttr<AnalyseAsMethodAttr>()) {
+ StringRef AttrValue = Attr->getMethodName();
+ auto Pos = AttrValue.rfind("::");
+ StringRef AttrMethodName = (Pos != StringRef::npos)
+ ? AttrValue.substr(Pos + 2)
+ : AttrValue;
+ return AttrMethodName == MethodName;
+ }
+ }
+ return false;
+}
+
auto isOptionalMemberCallWithNameMatcher(
ast_matchers::internal::Matcher<NamedDecl> matcher,
const std::optional<StatementMatcher> &Ignorable = std::nullopt) {
@@ -982,8 +996,9 @@ ignorableOptional(const UncheckedOptionalAccessModelOptions &Options) {
StatementMatcher
valueCall(const std::optional<StatementMatcher> &IgnorableOptional) {
- return isOptionalMemberCallWithNameMatcher(hasName("value"),
- IgnorableOptional);
+ return isOptionalMemberCallWithNameMatcher(
+ anyOf(hasName("value"), hasAnalyseAsMethodName("value")),
+ IgnorableOptional);
}
StatementMatcher
diff --git a/clang/lib/Sema/SemaDeclAttr.cpp b/clang/lib/Sema/SemaDeclAttr.cpp
index 243c66b474384..f999c307b7111 100644
--- a/clang/lib/Sema/SemaDeclAttr.cpp
+++ b/clang/lib/Sema/SemaDeclAttr.cpp
@@ -6485,6 +6485,30 @@ static void handleAnalyseAsClass(Sema &S, Decl *D, const ParsedAttr &AL) {
D->addAttr(::new (S.Context) AnalyseAsClassAttr(S.Context, AL, Str));
}
+// for now this only handles std::optional (POC)
+static bool isValidAnalyseAsMethodAttr(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 handleAnalyseAsMethod(Sema &S, Decl *D, const ParsedAttr &AL) {
+ StringRef Str;
+ if (!S.checkStringLiteralArgumentAttr(AL, 0, Str))
+ return;
+ if (D->hasAttr<AnalyseAsMethodAttr>()) {
+ S.Diag(AL.getLoc(), diag::err_duplicate_attribute) << AL;
+ return;
+ }
+ if (!isValidAnalyseAsMethodAttr(D, Str)) {
+ S.Diag(AL.getLoc(), diag::warn_attribute_type_not_supported) << AL;
+ return;
+ }
+
+ D->addAttr(::new (S.Context) AnalyseAsMethodAttr(S.Context, AL, Str));
+}
+
static bool hasBTFDeclTagAttr(Decl *D, StringRef Tag) {
for (const auto *I : D->specific_attrs<BTFDeclTagAttr>()) {
if (I->getBTFDeclTag() == Tag)
@@ -7577,6 +7601,9 @@ ProcessDeclAttribute(Sema &S, Scope *scope, Decl *D, const ParsedAttr &AL,
case ParsedAttr::AT_AnalyseAsClass:
handleAnalyseAsClass(S, D, AL);
break;
+ case ParsedAttr::AT_AnalyseAsMethod:
+ handleAnalyseAsMethod(S, D, AL);
+ break;
case ParsedAttr::AT_BTFDeclTag:
handleBTFDeclTagAttr(S, D, AL);
break;
diff --git a/hicketts_test/hicketts_optional.h b/hicketts_test/hicketts_optional.h
index b915b01f70290..43ec98831a74b 100644
--- a/hicketts_test/hicketts_optional.h
+++ b/hicketts_test/hicketts_optional.h
@@ -26,11 +26,10 @@ class [[clang::analyse_as_class("std::optional")]] HickettsOptional {
HickettsOptional(HickettsOptional &&) = default;
// Equivalent to std::optional::value()
- //[[clang_analyse_as_method(std::optional::value)]]
- const T &unwrap() const & { return *storage_; }
- T &unwrap() & { return *storage_; }
- const T &&unwrap() const && { return static_cast<const T &&>(*storage_); }
- T &&unwrap() && { return static_cast<T &&>(*storage_); }
+ [[clang::analyse_as_method("std::optional::value")]] const T &unwrap() const & { return *storage_; }
+ [[clang::analyse_as_method("std::optional::value")]] T &unwrap() & { return *storage_; }
+ [[clang::analyse_as_method("std::optional::value")]] const T &&unwrap() const && { return static_cast<const T &&>(*storage_); }
+ [[clang::analyse_as_method("std::optional::value")]] T &&unwrap() && { return static_cast<T &&>(*storage_); }
const T &value() const & { return *storage_; }
T &value() & { return *storage_; }
@@ -38,8 +37,8 @@ class [[clang::analyse_as_class("std::optional")]] HickettsOptional {
T &&value() && { return static_cast<T &&>(*storage_); }
// Equivalent to std::optional::operator*()
- const T &deref() const & { return *storage_; }
- T &deref() & { return *storage_; }
+ [[clang::analyse_as_method("std::optional::operator*")]] const T &deref() const & { return *storage_; }
+ [[clang::analyse_as_method("std::optional::operator*")]] T &deref() & { return *storage_; }
// Equivalent to std::optional::operator->()
const T* operator ->() const { return storage_; }
@@ -47,10 +46,10 @@ class [[clang::analyse_as_class("std::optional")]] HickettsOptional {
const T *arrow() const { return storage_; }
T *arrow() { return storage_; }
- // Equivalent to std::optional::operator bool / has_value()
+ // Equivalent to std::optional::operator bool / hasValue()
constexpr bool hasValue() const noexcept { return storage_ != nullptr; }
constexpr explicit operator bool() const noexcept { return storage_ != nullptr; }
- constexpr bool isPresent() const noexcept { return storage_ != nullptr; }
+ [[clang::analyse_as_method("std::optional::hasValue")]] constexpr bool isPresent() const noexcept { return storage_ != nullptr; }
constexpr bool isEmpty() const noexcept { return storage_ == nullptr; }
// Equivalent to std::optional::value_or()
>From 9d6520a6cac0f322cd67ac5a1619a6b8fd0128cd Mon Sep 17 00:00:00 2001
From: khickett <khicketts at bloomberg.net>
Date: Tue, 28 Apr 2026 17:07:21 +0100
Subject: [PATCH 04/10] extend method matching
.
---
.../Models/UncheckedOptionalAccessModel.cpp | 11 ++++++-----
hicketts_test/hicketts_optional.h | 15 +++++++--------
hicketts_test/test_hicketts_optional.cpp | 10 +++++-----
3 files changed, 18 insertions(+), 18 deletions(-)
diff --git a/clang/lib/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.cpp b/clang/lib/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.cpp
index f2c9aa756943e..137b44560c529 100644
--- a/clang/lib/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.cpp
+++ b/clang/lib/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.cpp
@@ -358,7 +358,7 @@ auto isValueOrStringEmptyCall() {
callee(cxxMethodDecl(hasName("empty"))),
onImplicitObjectArgument(ignoringImplicit(
cxxMemberCallExpr(on(expr(unless(cxxThisExpr()))),
- callee(cxxMethodDecl(hasName("value_or"),
+ callee(cxxMethodDecl(anyOf(hasName("value_or"), hasAnalyseAsMethodName("value_or")),
ofClass(optionalClass()))),
hasArgument(0, stringLiteral(hasSize(0))))
.bind(ValueOrCallID))));
@@ -1071,7 +1071,8 @@ auto buildTransferMatchSwitch() {
// will also pass for other types
.CaseOfCFGStmt<CXXMemberCallExpr>(
isOptionalMemberCallWithNameMatcher(
- hasAnyName("has_value", "hasValue")),
+ anyOf(hasAnyName("has_value", "hasValue"),
+ hasAnalyseAsMethodName("has_value"))),
transferOptionalHasValueCall)
// optional::operator bool
@@ -1101,7 +1102,7 @@ auto buildTransferMatchSwitch() {
// optional::emplace
.CaseOfCFGStmt<CXXMemberCallExpr>(
- isOptionalMemberCallWithNameMatcher(hasName("emplace")),
+ isOptionalMemberCallWithNameMatcher(anyOf(hasName("emplace"), hasAnalyseAsMethodName("emplace"))),
[](const CXXMemberCallExpr *E, const MatchFinder::MatchResult &,
LatticeTransferState &State) {
if (RecordStorageLocation *Loc =
@@ -1112,7 +1113,7 @@ auto buildTransferMatchSwitch() {
// optional::reset
.CaseOfCFGStmt<CXXMemberCallExpr>(
- isOptionalMemberCallWithNameMatcher(hasName("reset")),
+ isOptionalMemberCallWithNameMatcher(anyOf(hasName("reset"), hasAnalyseAsMethodName("reset"))),
[](const CXXMemberCallExpr *E, const MatchFinder::MatchResult &,
LatticeTransferState &State) {
if (RecordStorageLocation *Loc =
@@ -1124,7 +1125,7 @@ auto buildTransferMatchSwitch() {
// optional::swap
.CaseOfCFGStmt<CXXMemberCallExpr>(
- isOptionalMemberCallWithNameMatcher(hasName("swap")),
+ isOptionalMemberCallWithNameMatcher(anyOf(hasName("swap"), hasAnalyseAsMethodName("swap"))),
transferSwapCall)
// std::swap
diff --git a/hicketts_test/hicketts_optional.h b/hicketts_test/hicketts_optional.h
index 43ec98831a74b..2c6978a7e2339 100644
--- a/hicketts_test/hicketts_optional.h
+++ b/hicketts_test/hicketts_optional.h
@@ -37,8 +37,8 @@ class [[clang::analyse_as_class("std::optional")]] HickettsOptional {
T &&value() && { return static_cast<T &&>(*storage_); }
// Equivalent to std::optional::operator*()
- [[clang::analyse_as_method("std::optional::operator*")]] const T &deref() const & { return *storage_; }
- [[clang::analyse_as_method("std::optional::operator*")]] T &deref() & { return *storage_; }
+ [[clang::analyse_as_method("std::optional::value")]] const T &deref() const & { return *storage_; }
+ [[clang::analyse_as_method("std::optional::value")]] T &deref() & { return *storage_; }
// Equivalent to std::optional::operator->()
const T* operator ->() const { return storage_; }
@@ -47,10 +47,9 @@ class [[clang::analyse_as_class("std::optional")]] HickettsOptional {
T *arrow() { return storage_; }
// Equivalent to std::optional::operator bool / hasValue()
- constexpr bool hasValue() const noexcept { return storage_ != nullptr; }
+ constexpr bool has_value() const noexcept { return storage_ != nullptr; }
constexpr explicit operator bool() const noexcept { return storage_ != nullptr; }
- [[clang::analyse_as_method("std::optional::hasValue")]] constexpr bool isPresent() const noexcept { return storage_ != nullptr; }
- constexpr bool isEmpty() const noexcept { return storage_ == nullptr; }
+ [[clang::analyse_as_method("std::optional::has_value")]] constexpr bool isPresent() const noexcept { return storage_ != nullptr; }
// Equivalent to std::optional::value_or()
template <typename U>
@@ -60,13 +59,13 @@ class [[clang::analyse_as_class("std::optional")]] HickettsOptional {
// Equivalent to std::optional::emplace()
template <typename... Args>
- T &construct(Args &&...args) { return *storage_; }
+ [[clang::analyse_as_method("std::optional::emplace")]] T &construct(Args &&...args) { return *storage_; }
// Equivalent to std::optional::reset()
- void clear() noexcept { storage_ = nullptr; }
+ [[clang::analyse_as_method("std::optional::reset")]] void clear() noexcept { storage_ = nullptr; }
// Equivalent to std::optional::swap()
- void exchange(HickettsOptional &other) noexcept {
+ [[clang::analyse_as_method("std::optional::swap")]] void exchange(HickettsOptional &other) noexcept {
T *tmp = storage_;
storage_ = other.storage_;
other.storage_ = tmp;
diff --git a/hicketts_test/test_hicketts_optional.cpp b/hicketts_test/test_hicketts_optional.cpp
index fa0153a79f341..1ff16d13fe174 100644
--- a/hicketts_test/test_hicketts_optional.cpp
+++ b/hicketts_test/test_hicketts_optional.cpp
@@ -30,7 +30,7 @@ static void checkedWithBool(mylib::HickettsOptional<int> &Val) {
}
static void checkedValueWithBool(mylib::HickettsOptional<int> &Val) {
- if (Val.hasValue()) {
+ if (Val.has_value()) {
Val.value(); // safe — checked via operator bool
}
}
@@ -41,11 +41,11 @@ static void checkedWithIsPresent(mylib::HickettsOptional<int> &Val) {
}
}
-static void checkedWithIsEmpty(mylib::HickettsOptional<int> &Val) {
+/* static void checkedWithIsEmpty(mylib::HickettsOptional<int> &Val) {
if (!Val.isEmpty()) {
Val.unwrap(); // safe — checked via !isEmpty()
}
-}
+} NYI */
// --- State changes ---
@@ -70,12 +70,12 @@ static void unsafeAfterExchange(mylib::HickettsOptional<int> &A,
// --- Guarded paths ---
-static void constructCoversEmptyBranch(mylib::HickettsOptional<int> &Val) {
+/*static void constructCoversEmptyBranch(mylib::HickettsOptional<int> &Val) {
if (Val.isEmpty()) {
Val.construct(99);
}
Val.unwrap(); // safe — either was present, or construct filled it
-}
+}*/
static void unwrapOrIsAlwaysSafe(mylib::HickettsOptional<int> &Val) {
int X = Val.unwrapOr(0); // safe — fallback provided
>From 34ce7b2b289e5e5a4cb0d708d34f4675d7bdc426 Mon Sep 17 00:00:00 2001
From: khickett <khicketts at bloomberg.net>
Date: Thu, 30 Apr 2026 12:48:57 +0100
Subject: [PATCH 05/10] missed a bit
---
clang/include/clang/Basic/Attr.td | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
diff --git a/clang/include/clang/Basic/Attr.td b/clang/include/clang/Basic/Attr.td
index 97536ac7a1966..adc127df5756e 100644
--- a/clang/include/clang/Basic/Attr.td
+++ b/clang/include/clang/Basic/Attr.td
@@ -921,6 +921,22 @@ def AlignValue : Attr {
let Documentation = [AlignValueDocs];
}
+def AnalyseAsClass : InheritableAttr {
+ let Spellings = [Clang<"analyse_as_class">];
+ let Args = [StringArgument<"ClassName">];
+ let Subjects = SubjectList<[Record],
+ ErrorDiag>;
+ let Documentation = [Undocumented];
+}
+
+def AnalyseAsMethod : InheritableAttr {
+ let Spellings = [Clang<"analyse_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 = [];
>From 3532f8f3ae825e185f7f5c3506e3b96fbd16dda3 Mon Sep 17 00:00:00 2001
From: khickett <khicketts at bloomberg.net>
Date: Fri, 1 May 2026 15:40:38 +0100
Subject: [PATCH 06/10] add unit tests
---
.../UncheckedOptionalAccessModelTest.cpp | 50 +++++++++++++++++--
1 file changed, 46 insertions(+), 4 deletions(-)
diff --git a/clang/unittests/Analysis/FlowSensitive/UncheckedOptionalAccessModelTest.cpp b/clang/unittests/Analysis/FlowSensitive/UncheckedOptionalAccessModelTest.cpp
index 074dc95e2c388..72cf12bcd0817 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();
@@ -2995,6 +2995,48 @@ TEST_P(UncheckedOptionalAccessTest, AssertFalseGtestMacroWithNullableValue) {
)cc");
}
+TEST_P(UncheckedOptionalAccessTest, TestCustomAttributeBalik) {
+ ExpectDiagnosticsFor(R"cc(
+ #include "unchecked_optional_access_test.h"
+
+ template <typename T>
+ class __attribute__((analyse_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__((analyse_as_class("std::optional"))) MyOptional {
+ public:
+ __attribute__((analyse_as_method("std::optional::has_value"))) bool isNotNull() const;
+ __attribute__((analyse_as_method("std::optional::value"))) T& unwrap();
+ __attribute__((analyse_as_method("std::optional::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)
>From 7320c8c063be143928434c9046eb85cf7e22ed78 Mon Sep 17 00:00:00 2001
From: khickett <khicketts at bloomberg.net>
Date: Fri, 1 May 2026 16:01:22 +0100
Subject: [PATCH 07/10] .
---
.../bugprone/unchecked-optional-access.cpp | 69 +++++++++++++++++++
1 file changed, 69 insertions(+)
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..cdd8fdb82174b 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,72 @@ void foo() {
if (!vec.empty())
vec[0].x = 0;
}
+
+// Custom optional-like type using analyse_as_class / analyse_as_method attributes.
+template <typename T>
+class [[clang::analyse_as_class("std::optional")]] CustomOptional {
+public:
+ [[clang::analyse_as_method("std::optional::has_value")]] bool isEngaged() const;
+ [[clang::analyse_as_method("std::optional::value")]] T &retrieve();
+ [[clang::analyse_as_method("std::optional::value")]] const T &retrieve() const;
+ [[clang::analyse_as_method("std::optional::emplace")]] T &fill(T val);
+ [[clang::analyse_as_method("std::optional::reset")]] void clear();
+};
+
+void custom_unchecked_access(CustomOptional<int> opt) {
+ opt.retrieve();
+ // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: unchecked access to optional value [bugprone-unchecked-optional-access]
+}
+
+void custom_checked_access(CustomOptional<int> opt) {
+ if (opt.isEngaged()) {
+ opt.retrieve();
+ }
+}
+
+void custom_emplace_then_access(CustomOptional<int> opt) {
+ opt.fill(42);
+ opt.retrieve();
+}
+
+void custom_clear_then_access(CustomOptional<int> opt) {
+ opt.fill(42);
+ opt.clear();
+ opt.retrieve();
+ // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: unchecked access to optional value [bugprone-unchecked-optional-access]
+}
+
+// Custom optional-like type using standard method names — recognised via
+// analyse_as_class alone, without analyse_as_method attributes.
+template <typename T>
+class [[clang::analyse_as_class("std::optional")]] StdNamedOptional {
+public:
+ bool has_value() const;
+ T &value();
+ const T &value() const;
+ T &emplace(T val);
+ void reset();
+};
+
+void std_named_unchecked_access(StdNamedOptional<int> opt) {
+ opt.value();
+ // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: unchecked access to optional value [bugprone-unchecked-optional-access]
+}
+
+void std_named_checked_access(StdNamedOptional<int> opt) {
+ if (opt.has_value()) {
+ opt.value();
+ }
+}
+
+void std_named_emplace_then_access(StdNamedOptional<int> opt) {
+ opt.emplace(42);
+ opt.value();
+}
+
+void std_named_reset_then_access(StdNamedOptional<int> opt) {
+ opt.emplace(42);
+ opt.reset();
+ opt.value();
+ // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: unchecked access to optional value [bugprone-unchecked-optional-access]
+}
>From dc6922262ffdaba0b0202b93e82f320286249bca Mon Sep 17 00:00:00 2001
From: khickett <khicketts at bloomberg.net>
Date: Fri, 1 May 2026 16:16:29 +0100
Subject: [PATCH 08/10] .
---
hicketts_test/hicketts_optional.h | 81 -----------------------
hicketts_test/test_hicketts_optional.cpp | 83 ------------------------
2 files changed, 164 deletions(-)
delete mode 100644 hicketts_test/hicketts_optional.h
delete mode 100644 hicketts_test/test_hicketts_optional.cpp
diff --git a/hicketts_test/hicketts_optional.h b/hicketts_test/hicketts_optional.h
deleted file mode 100644
index 2c6978a7e2339..0000000000000
--- a/hicketts_test/hicketts_optional.h
+++ /dev/null
@@ -1,81 +0,0 @@
-#ifndef HICKETTS_OPTIONAL_H_
-#define HICKETTS_OPTIONAL_H_
-
-/// A custom optional-like type with differently named functions.
-/// Mirrors std::optional semantics but uses its own vocabulary
-/// In order to test implementation of attributes for clang-tidy
-namespace mylib {
-
-struct nothing_t {
- constexpr explicit nothing_t() {}
-};
-
-constexpr nothing_t nothing;
-
-template <typename T>
-class [[clang::analyse_as_class("std::optional")]] HickettsOptional {
- T *storage_ = nullptr;
-
-public:
- constexpr HickettsOptional() noexcept {}
-
- constexpr HickettsOptional(nothing_t) noexcept {}
-
- HickettsOptional(const HickettsOptional &) = default;
-
- HickettsOptional(HickettsOptional &&) = default;
-
- // Equivalent to std::optional::value()
- [[clang::analyse_as_method("std::optional::value")]] const T &unwrap() const & { return *storage_; }
- [[clang::analyse_as_method("std::optional::value")]] T &unwrap() & { return *storage_; }
- [[clang::analyse_as_method("std::optional::value")]] const T &&unwrap() const && { return static_cast<const T &&>(*storage_); }
- [[clang::analyse_as_method("std::optional::value")]] T &&unwrap() && { return static_cast<T &&>(*storage_); }
-
- const T &value() const & { return *storage_; }
- T &value() & { return *storage_; }
- const T &&value() const && { return static_cast<const T &&>(*storage_); }
- T &&value() && { return static_cast<T &&>(*storage_); }
-
- // Equivalent to std::optional::operator*()
- [[clang::analyse_as_method("std::optional::value")]] const T &deref() const & { return *storage_; }
- [[clang::analyse_as_method("std::optional::value")]] T &deref() & { return *storage_; }
-
- // Equivalent to std::optional::operator->()
- const T* operator ->() const { return storage_; }
- T* operator ->() { return storage_; }
- const T *arrow() const { return storage_; }
- T *arrow() { return storage_; }
-
- // Equivalent to std::optional::operator bool / hasValue()
- constexpr bool has_value() const noexcept { return storage_ != nullptr; }
- constexpr explicit operator bool() const noexcept { return storage_ != nullptr; }
- [[clang::analyse_as_method("std::optional::has_value")]] constexpr bool isPresent() const noexcept { return storage_ != nullptr; }
-
- // Equivalent to std::optional::value_or()
- template <typename U>
- constexpr T unwrapOr(U &&fallback) const & {
- return storage_ ? *storage_ : static_cast<T>(fallback);
- }
-
- // Equivalent to std::optional::emplace()
- template <typename... Args>
- [[clang::analyse_as_method("std::optional::emplace")]] T &construct(Args &&...args) { return *storage_; }
-
- // Equivalent to std::optional::reset()
- [[clang::analyse_as_method("std::optional::reset")]] void clear() noexcept { storage_ = nullptr; }
-
- // Equivalent to std::optional::swap()
- [[clang::analyse_as_method("std::optional::swap")]] void exchange(HickettsOptional &other) noexcept {
- T *tmp = storage_;
- storage_ = other.storage_;
- other.storage_ = tmp;
- }
-
- // Assignment
- template <typename U>
- HickettsOptional &operator=(const U &u) { return *this; }
-};
-
-} // namespace mylib
-
-#endif // HICKETTS_OPTIONAL_H_
diff --git a/hicketts_test/test_hicketts_optional.cpp b/hicketts_test/test_hicketts_optional.cpp
deleted file mode 100644
index 1ff16d13fe174..0000000000000
--- a/hicketts_test/test_hicketts_optional.cpp
+++ /dev/null
@@ -1,83 +0,0 @@
-// Test cases for mylib::HickettsOptional — a custom optional-like type
-// with differently named functions.
-//
-// Run from hicketts_test/ with:
-// clang-tidy -checks='bugprone-unchecked-optional-access' \
-// test_hicketts_optional.cpp -- -I . -Wno-undefined-inline
-
-#include "hicketts_optional.h"
-
-// --- Unchecked access (should warn if the checker recognises HickettsOptional) ---
-
-static void uncheckedUnwrap(mylib::HickettsOptional<int> &Val) {
- Val.unwrap(); // unchecked access — may be empty
-}
-
-static void uncheckedValue(mylib::HickettsOptional<int> &Val) {
- Val.value(); // unchecked access — may be empty
-}
-
-static void uncheckedDeref(mylib::HickettsOptional<int> &Val) {
- Val.deref(); // unchecked access — may be empty
-}
-
-// --- Checked access (should NOT warn) ---
-
-static void checkedWithBool(mylib::HickettsOptional<int> &Val) {
- if (Val) {
- Val.unwrap(); // safe — checked via operator bool
- }
-}
-
-static void checkedValueWithBool(mylib::HickettsOptional<int> &Val) {
- if (Val.has_value()) {
- Val.value(); // safe — checked via operator bool
- }
-}
-
-static void checkedWithIsPresent(mylib::HickettsOptional<int> &Val) {
- if (Val.isPresent()) {
- Val.unwrap(); // safe — checked via isPresent()
- }
-}
-
-/* static void checkedWithIsEmpty(mylib::HickettsOptional<int> &Val) {
- if (!Val.isEmpty()) {
- Val.unwrap(); // safe — checked via !isEmpty()
- }
-} NYI */
-
-// --- State changes ---
-
-static void safeAfterConstruct(mylib::HickettsOptional<int> &Val) {
- Val.construct(42);
- Val.unwrap(); // safe — just constructed a value
-}
-
-static void unsafeAfterClear(mylib::HickettsOptional<int> &Val) {
- Val.construct(42);
- Val.clear();
- Val.unwrap(); // unsafe — value was cleared
-}
-
-static void unsafeAfterExchange(mylib::HickettsOptional<int> &A,
- mylib::HickettsOptional<int> &B) {
- if (A) {
- A.exchange(B);
- A.unwrap(); // unsafe — a's state is now unknown
- }
-}
-
-// --- Guarded paths ---
-
-/*static void constructCoversEmptyBranch(mylib::HickettsOptional<int> &Val) {
- if (Val.isEmpty()) {
- Val.construct(99);
- }
- Val.unwrap(); // safe — either was present, or construct filled it
-}*/
-
-static void unwrapOrIsAlwaysSafe(mylib::HickettsOptional<int> &Val) {
- int X = Val.unwrapOr(0); // safe — fallback provided
- (void)X;
-}
>From e50be59fa64622856902e3b0a80d1e8c1699d9ec Mon Sep 17 00:00:00 2001
From: khickett <khicketts at bloomberg.net>
Date: Fri, 1 May 2026 17:28:47 +0100
Subject: [PATCH 09/10] comment possible cody tidy up locations
---
.../Models/UncheckedOptionalAccessModel.cpp | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/clang/lib/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.cpp b/clang/lib/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.cpp
index 137b44560c529..1fee746ff7a97 100644
--- a/clang/lib/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.cpp
+++ b/clang/lib/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.cpp
@@ -71,6 +71,8 @@ static bool hasOptionalClassName(const CXXRecordDecl &RD) {
return false;
}
+ // this code could be removed if base::Optional and folly::Optional used
+ // [[clang::analyse_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());
@@ -78,12 +80,16 @@ static bool hasOptionalClassName(const CXXRecordDecl &RD) {
isFullyQualifiedNamespaceEqualTo(*N, "folly"));
}
+ // this code could be removed if Optional_Base used
+ // [[clang::analyse_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::analyse_as_class("std::optional")]]
if (RD.getName() == "NullableValue") {
const auto *N = dyn_cast_or_null<NamespaceDecl>(RD.getDeclContext());
return N != nullptr &&
@@ -1069,6 +1075,8 @@ auto buildTransferMatchSwitch() {
// 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::analyse_as_method("std::optional::has_value")]] on hasValue()
.CaseOfCFGStmt<CXXMemberCallExpr>(
isOptionalMemberCallWithNameMatcher(
anyOf(hasAnyName("has_value", "hasValue"),
@@ -1080,12 +1088,16 @@ auto buildTransferMatchSwitch() {
isOptionalMemberCallWithNameMatcher(hasName("operator bool")),
transferOptionalHasValueCall)
+ // this code could be removed if NullableValue used
+ // [[clang::analyse_as_inverse_method("std::optional::has_value")]] on isNull() *NYI
// NullableValue::isNull
// Only NullableValue has isNull
.CaseOfCFGStmt<CXXMemberCallExpr>(
isOptionalMemberCallWithNameMatcher(hasName("isNull")),
transferOptionalIsNullCall)
+ // this code could be removed if NullableValue used
+ // [[clang::analyse_as_method("std::optional::emplace")]] on makeValue() and makeValueInplace()
// NullableValue::makeValue, NullableValue::makeValueInplace
// Only NullableValue has these methods, but this
// will also pass for other types
>From a0d0dd9c93a309410c15030774368af98c425e1e Mon Sep 17 00:00:00 2001
From: khickett <khicketts at bloomberg.net>
Date: Fri, 1 May 2026 17:51:13 +0100
Subject: [PATCH 10/10] remove fully qualified names for method attributes
---
.../checkers/bugprone/unchecked-optional-access.cpp | 10 +++++-----
.../Models/UncheckedOptionalAccessModel.cpp | 10 +++-------
.../FlowSensitive/UncheckedOptionalAccessModelTest.cpp | 6 +++---
3 files changed, 11 insertions(+), 15 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 cdd8fdb82174b..bea941ff2104d 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
@@ -389,11 +389,11 @@ void foo() {
template <typename T>
class [[clang::analyse_as_class("std::optional")]] CustomOptional {
public:
- [[clang::analyse_as_method("std::optional::has_value")]] bool isEngaged() const;
- [[clang::analyse_as_method("std::optional::value")]] T &retrieve();
- [[clang::analyse_as_method("std::optional::value")]] const T &retrieve() const;
- [[clang::analyse_as_method("std::optional::emplace")]] T &fill(T val);
- [[clang::analyse_as_method("std::optional::reset")]] void clear();
+ [[clang::analyse_as_method("has_value")]] bool isEngaged() const;
+ [[clang::analyse_as_method("value")]] T &retrieve();
+ [[clang::analyse_as_method("value")]] const T &retrieve() const;
+ [[clang::analyse_as_method("emplace")]] T &fill(T val);
+ [[clang::analyse_as_method("reset")]] void clear();
};
void custom_unchecked_access(CustomOptional<int> opt) {
diff --git a/clang/lib/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.cpp b/clang/lib/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.cpp
index 1fee746ff7a97..2264c7e60f190 100644
--- a/clang/lib/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.cpp
+++ b/clang/lib/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.cpp
@@ -240,11 +240,7 @@ AST_MATCHER_P(NamedDecl, hasAnalyseAsMethodName, std::string, MethodName) {
if (const auto *MD = dyn_cast<CXXMethodDecl>(&Node)) {
if (const auto *Attr = MD->getAttr<AnalyseAsMethodAttr>()) {
StringRef AttrValue = Attr->getMethodName();
- auto Pos = AttrValue.rfind("::");
- StringRef AttrMethodName = (Pos != StringRef::npos)
- ? AttrValue.substr(Pos + 2)
- : AttrValue;
- return AttrMethodName == MethodName;
+ return AttrValue == MethodName;
}
}
return false;
@@ -1076,7 +1072,7 @@ auto buildTransferMatchSwitch() {
// 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::analyse_as_method("std::optional::has_value")]] on hasValue()
+ // [[clang::analyse_as_method("has_value")]] on hasValue()
.CaseOfCFGStmt<CXXMemberCallExpr>(
isOptionalMemberCallWithNameMatcher(
anyOf(hasAnyName("has_value", "hasValue"),
@@ -1097,7 +1093,7 @@ auto buildTransferMatchSwitch() {
transferOptionalIsNullCall)
// this code could be removed if NullableValue used
- // [[clang::analyse_as_method("std::optional::emplace")]] on makeValue() and makeValueInplace()
+ // [[clang::analyse_as_method("emplace")]] on makeValue() and makeValueInplace()
// NullableValue::makeValue, NullableValue::makeValueInplace
// Only NullableValue has these methods, but this
// will also pass for other types
diff --git a/clang/unittests/Analysis/FlowSensitive/UncheckedOptionalAccessModelTest.cpp b/clang/unittests/Analysis/FlowSensitive/UncheckedOptionalAccessModelTest.cpp
index 72cf12bcd0817..ac67bda300b65 100644
--- a/clang/unittests/Analysis/FlowSensitive/UncheckedOptionalAccessModelTest.cpp
+++ b/clang/unittests/Analysis/FlowSensitive/UncheckedOptionalAccessModelTest.cpp
@@ -3023,9 +3023,9 @@ TEST_P(UncheckedOptionalAccessTest, TestCustomAttributeHicketts) {
template <typename T>
class __attribute__((analyse_as_class("std::optional"))) MyOptional {
public:
- __attribute__((analyse_as_method("std::optional::has_value"))) bool isNotNull() const;
- __attribute__((analyse_as_method("std::optional::value"))) T& unwrap();
- __attribute__((analyse_as_method("std::optional::value"))) const T& unwrap() const;
+ __attribute__((analyse_as_method("has_value"))) bool isNotNull() const;
+ __attribute__((analyse_as_method("value"))) T& unwrap();
+ __attribute__((analyse_as_method("value"))) const T& unwrap() const;
};
void target(MyOptional<int> opt) {
More information about the cfe-commits
mailing list