[clang] [clang-tools-extra] [clang-tidy] Add `bugprone-unchecked-expected-access` check (PR #233219)
Valentyn Yukhymenko via cfe-commits
cfe-commits at lists.llvm.org
Sat Oct 24 13:24:19 PDT 2026
https://github.com/BaLiKfromUA updated https://github.com/llvm/llvm-project/pull/233219
>From 60ea770b22c8c6962bbbe32e1796ba20ea96bc21 Mon Sep 17 00:00:00 2001
From: BaLiKfromUA <valentin.yukhymenko at gmail.com>
Date: Thu, 24 Sep 2026 21:24:30 +0100
Subject: [PATCH 01/25] Add boilerplate
---
.../bugprone/BugproneTidyModule.cpp | 3 ++
.../clang-tidy/bugprone/CMakeLists.txt | 1 +
.../bugprone/UncheckedExpectedAccessCheck.cpp | 32 ++++++++++++++++++
.../bugprone/UncheckedExpectedAccessCheck.h | 33 +++++++++++++++++++
clang-tools-extra/docs/ReleaseNotes.md | 5 +++
.../bugprone/unchecked-expected-access.md | 6 ++++
.../docs/clang-tidy/checks/list.md | 1 +
.../bugprone/unchecked-expected-access.cpp | 14 ++++++++
8 files changed, 95 insertions(+)
create mode 100644 clang-tools-extra/clang-tidy/bugprone/UncheckedExpectedAccessCheck.cpp
create mode 100644 clang-tools-extra/clang-tidy/bugprone/UncheckedExpectedAccessCheck.h
create mode 100644 clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md
create mode 100644 clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp
diff --git a/clang-tools-extra/clang-tidy/bugprone/BugproneTidyModule.cpp b/clang-tools-extra/clang-tidy/bugprone/BugproneTidyModule.cpp
index 3aa39d10ceb5dc..54798935926eda 100644
--- a/clang-tools-extra/clang-tidy/bugprone/BugproneTidyModule.cpp
+++ b/clang-tools-extra/clang-tidy/bugprone/BugproneTidyModule.cpp
@@ -101,6 +101,7 @@
#include "ThrowKeywordMissingCheck.h"
#include "ThrowingStaticInitializationCheck.h"
#include "TooSmallLoopVariableCheck.h"
+#include "UncheckedExpectedAccessCheck.h"
#include "UncheckedOptionalAccessCheck.h"
#include "UncheckedStringToNumberConversionCheck.h"
#include "UndefinedMemoryManipulationCheck.h"
@@ -184,6 +185,8 @@ class BugproneModule : public ClangTidyModule {
"bugprone-incorrect-enable-if");
CheckFactories.registerCheck<IncorrectEnableSharedFromThisCheck>(
"bugprone-incorrect-enable-shared-from-this");
+ CheckFactories.registerCheck<UncheckedExpectedAccessCheck>(
+ "bugprone-unchecked-expected-access");
CheckFactories.registerCheck<UnintendedCharOstreamOutputCheck>(
"bugprone-unintended-char-ostream-output");
CheckFactories.registerCheck<ReturnConstRefFromParameterCheck>(
diff --git a/clang-tools-extra/clang-tidy/bugprone/CMakeLists.txt b/clang-tools-extra/clang-tidy/bugprone/CMakeLists.txt
index 43e85b1407f21a..7791aa96ba1270 100644
--- a/clang-tools-extra/clang-tidy/bugprone/CMakeLists.txt
+++ b/clang-tools-extra/clang-tidy/bugprone/CMakeLists.txt
@@ -39,6 +39,7 @@ add_clang_library(clangTidyBugproneModule STATIC
IncorrectEnableSharedFromThisCheck.cpp
InvalidEnumDefaultInitializationCheck.cpp
MissingEndComparisonCheck.cpp
+ UncheckedExpectedAccessCheck.cpp
UnintendedCharOstreamOutputCheck.cpp
ReturnConstRefFromParameterCheck.cpp
StdExceptionBaseclassCheck.cpp
diff --git a/clang-tools-extra/clang-tidy/bugprone/UncheckedExpectedAccessCheck.cpp b/clang-tools-extra/clang-tidy/bugprone/UncheckedExpectedAccessCheck.cpp
new file mode 100644
index 00000000000000..c4dff1afa64803
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/bugprone/UncheckedExpectedAccessCheck.cpp
@@ -0,0 +1,32 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "UncheckedExpectedAccessCheck.h"
+#include "clang/ASTMatchers/ASTMatchFinder.h"
+
+using namespace clang::ast_matchers;
+
+namespace clang::tidy::bugprone {
+
+void UncheckedExpectedAccessCheck::registerMatchers(MatchFinder *Finder) {
+ // FIXME: Add matchers.
+ Finder->addMatcher(functionDecl().bind("x"), this);
+}
+
+void UncheckedExpectedAccessCheck::check(const MatchFinder::MatchResult &Result) {
+ // FIXME: Add callback implementation.
+ const auto *MatchedDecl = Result.Nodes.getNodeAs<FunctionDecl>("x");
+ if (!MatchedDecl->getIdentifier() || MatchedDecl->getName().starts_with("awesome_"))
+ return;
+ diag(MatchedDecl->getLocation(), "function %0 is insufficiently awesome")
+ << MatchedDecl
+ << FixItHint::CreateInsertion(MatchedDecl->getLocation(), "awesome_");
+ diag(MatchedDecl->getLocation(), "insert 'awesome'", DiagnosticIDs::Note);
+}
+
+} // namespace clang::tidy::bugprone
diff --git a/clang-tools-extra/clang-tidy/bugprone/UncheckedExpectedAccessCheck.h b/clang-tools-extra/clang-tidy/bugprone/UncheckedExpectedAccessCheck.h
new file mode 100644
index 00000000000000..986fe17399773e
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/bugprone/UncheckedExpectedAccessCheck.h
@@ -0,0 +1,33 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_BUGPRONE_UNCHECKEDEXPECTEDACCESSCHECK_H
+#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_BUGPRONE_UNCHECKEDEXPECTEDACCESSCHECK_H
+
+#include "../ClangTidyCheck.h"
+
+namespace clang::tidy::bugprone {
+
+/// FIXME: Write a short description.
+///
+/// For the user-facing documentation see:
+/// https://clang.llvm.org/extra/clang-tidy/checks/bugprone/unchecked-expected-access.html
+class UncheckedExpectedAccessCheck : public ClangTidyCheck {
+public:
+ UncheckedExpectedAccessCheck(StringRef Name, ClangTidyContext *Context)
+ : ClangTidyCheck(Name, Context) {}
+ void registerMatchers(ast_matchers::MatchFinder *Finder) override;
+ void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
+ bool isLanguageVersionSupported(const LangOptions &LangOpts) const override {
+ return LangOpts.CPlusPlus;
+ }
+};
+
+} // namespace clang::tidy::bugprone
+
+#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_BUGPRONE_UNCHECKEDEXPECTEDACCESSCHECK_H
diff --git a/clang-tools-extra/docs/ReleaseNotes.md b/clang-tools-extra/docs/ReleaseNotes.md
index 833638a47abc63..c0cef06e995e0f 100644
--- a/clang-tools-extra/docs/ReleaseNotes.md
+++ b/clang-tools-extra/docs/ReleaseNotes.md
@@ -129,6 +129,11 @@ infrastructure are described first, followed by tool-specific sections.
#### New checks
+- New {doc}`bugprone-unchecked-expected-access
+ <clang-tidy/checks/bugprone/unchecked-expected-access>` check.
+
+ FIXME: Write a short description.
+
- New {doc}`llvm-invalid-regex-pattern
<clang-tidy/checks/llvm/invalid-regex-pattern>` check.
diff --git a/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md b/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md
new file mode 100644
index 00000000000000..886ec8a01adabe
--- /dev/null
+++ b/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md
@@ -0,0 +1,6 @@
+```{title} clang-tidy - bugprone-unchecked-expected-access
+```
+
+# bugprone-unchecked-expected-access
+
+FIXME: Describe what patterns does the check detect and why. Give examples.
diff --git a/clang-tools-extra/docs/clang-tidy/checks/list.md b/clang-tools-extra/docs/clang-tidy/checks/list.md
index a74a26e691053d..8e0b2639e3ad42 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/list.md
+++ b/clang-tools-extra/docs/clang-tidy/checks/list.md
@@ -173,6 +173,7 @@ readability/*
| {doc}`bugprone-throw-keyword-missing <bugprone/throw-keyword-missing>` | |
| {doc}`bugprone-throwing-static-initialization <bugprone/throwing-static-initialization>` | |
| {doc}`bugprone-too-small-loop-variable <bugprone/too-small-loop-variable>` | |
+| {doc}`bugprone-unchecked-expected-access <bugprone/unchecked-expected-access>` | Yes |
| {doc}`bugprone-unchecked-optional-access <bugprone/unchecked-optional-access>` | |
| {doc}`bugprone-unchecked-string-to-number-conversion <bugprone/unchecked-string-to-number-conversion>` | |
| {doc}`bugprone-undefined-memory-manipulation <bugprone/undefined-memory-manipulation>` | |
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp
new file mode 100644
index 00000000000000..5efb7c2f62d9e1
--- /dev/null
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp
@@ -0,0 +1,14 @@
+// RUN: %check_clang_tidy %s bugprone-unchecked-expected-access %t
+
+// FIXME: Add something that triggers the check here.
+void f();
+// CHECK-MESSAGES: :[[@LINE-1]]:6: warning: function 'f' is insufficiently awesome [bugprone-unchecked-expected-access]
+
+// FIXME: Verify the applied fix.
+// * Make the CHECK patterns specific enough and try to make verified lines
+// unique to avoid incorrect matches.
+// * Use {{}} for regular expressions.
+// CHECK-FIXES: {{^}}void awesome_f();{{$}}
+
+// FIXME: Add something that doesn't trigger the check here.
+void awesome_f2();
>From bdeb2db4546e393b7206d01a516047f37a4d59bf Mon Sep 17 00:00:00 2001
From: BaLiKfromUA <valentin.yukhymenko at gmail.com>
Date: Thu, 24 Sep 2026 21:41:50 +0100
Subject: [PATCH 02/25] Add mock and basic tests to start with
---
.../std/types/expected.h | 118 ++++++++++++++++
.../bugprone/unchecked-expected-access.cpp | 128 ++++++++++++++++--
2 files changed, 235 insertions(+), 11 deletions(-)
create mode 100644 clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-expected-access/std/types/expected.h
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-expected-access/std/types/expected.h b/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-expected-access/std/types/expected.h
new file mode 100644
index 00000000000000..5cff26560980bc
--- /dev/null
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-expected-access/std/types/expected.h
@@ -0,0 +1,118 @@
+#ifndef LLVM_CLANG_TOOLS_EXTRA_TEST_CLANG_TIDY_CHECKERS_INPUTS_STD_TYPES_EXPECTED_H_
+#define LLVM_CLANG_TOOLS_EXTRA_TEST_CLANG_TIDY_CHECKERS_INPUTS_STD_TYPES_EXPECTED_H_
+
+/// Mock of `std::expected`.
+// TODO: move to checkers/Inputs/Headers/std/expected?
+namespace std {
+
+template <class E> class unexpected {
+public:
+ unexpected(const unexpected &) = default;
+ unexpected(unexpected &&) = default;
+
+ template <class Err = E> explicit unexpected(Err &&e);
+
+ const E &error() const & noexcept;
+ E &error() & noexcept;
+ const E &&error() const && noexcept;
+ E &&error() && noexcept;
+};
+
+template <class E> unexpected(E) -> unexpected<E>;
+
+struct unexpect_t {
+ explicit unexpect_t() = default;
+};
+
+inline constexpr unexpect_t unexpect{};
+
+template <class T, class E> class expected {
+public:
+ using value_type = T;
+ using error_type = E;
+ using unexpected_type = unexpected<E>;
+
+ expected();
+ expected(const expected &);
+ expected(expected &&) noexcept;
+
+ template <class U = T> expected(U &&v);
+
+ template <class G> expected(const unexpected<G> &e);
+ template <class G> expected(unexpected<G> &&e);
+
+ template <class... Args>
+ explicit expected(unexpect_t, Args &&...args);
+
+ expected &operator=(const expected &);
+ expected &operator=(expected &&) noexcept;
+
+ const T *operator->() const noexcept;
+ T *operator->() noexcept;
+
+ const T &operator*() const & noexcept;
+ T &operator*() & noexcept;
+ const T &&operator*() const && noexcept;
+ T &&operator*() && noexcept;
+
+ explicit operator bool() const noexcept;
+ bool has_value() const noexcept;
+
+ const T &value() const &;
+ T &value() &;
+ const T &&value() const &&;
+ T &&value() &&;
+
+ const E &error() const & noexcept;
+ E &error() & noexcept;
+ const E &&error() const && noexcept;
+ E &&error() && noexcept;
+
+ template <class U> T value_or(U &&v) const &;
+ template <class U> T value_or(U &&v) &&;
+
+ template <class... Args> T &emplace(Args &&...args) noexcept;
+
+ void swap(expected &rhs) noexcept;
+};
+
+template <class E> class expected<void, E> {
+public:
+ using value_type = void;
+ using error_type = E;
+ using unexpected_type = unexpected<E>;
+
+ expected() noexcept;
+ expected(const expected &);
+ expected(expected &&) noexcept;
+
+ template <class G> expected(const unexpected<G> &e);
+ template <class G> expected(unexpected<G> &&e);
+
+ template <class... Args>
+ explicit expected(unexpect_t, Args &&...args);
+
+ expected &operator=(const expected &);
+ expected &operator=(expected &&) noexcept;
+
+ explicit operator bool() const noexcept;
+ bool has_value() const noexcept;
+
+ void operator*() const noexcept;
+
+ void value() const &;
+ void value() &&;
+
+ const E &error() const & noexcept;
+ E &error() & noexcept;
+ const E &&error() const && noexcept;
+ E &&error() && noexcept;
+
+ void emplace() noexcept;
+
+ void swap(expected &rhs) noexcept;
+};
+
+} // namespace std
+
+#endif // LLVM_CLANG_TOOLS_EXTRA_TEST_CLANG_TIDY_CHECKERS_INPUTS_STD_TYPES_EXPECTED_H_
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp
index 5efb7c2f62d9e1..5ef426a24f157b 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp
@@ -1,14 +1,120 @@
-// RUN: %check_clang_tidy %s bugprone-unchecked-expected-access %t
+// RUN: %check_clang_tidy -std=c++23-or-later %s bugprone-unchecked-expected-access %t -- -- -I %S/Inputs/unchecked-expected-access
-// FIXME: Add something that triggers the check here.
-void f();
-// CHECK-MESSAGES: :[[@LINE-1]]:6: warning: function 'f' is insufficiently awesome [bugprone-unchecked-expected-access]
+#include "std/types/expected.h"
-// FIXME: Verify the applied fix.
-// * Make the CHECK patterns specific enough and try to make verified lines
-// unique to avoid incorrect matches.
-// * Use {{}} for regular expressions.
-// CHECK-FIXES: {{^}}void awesome_f();{{$}}
+struct Foo {
+ void foo() const {}
+};
-// FIXME: Add something that doesn't trigger the check here.
-void awesome_f2();
+std::expected<int, int> make();
+
+// Unchecked accesses.
+
+void unchecked_value_access(std::expected<int, int> e) {
+ e.value();
+ // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: unchecked access to 'std::expected' value [bugprone-unchecked-expected-access]
+}
+
+void unchecked_deref_operator_access(std::expected<int, int> e) {
+ *e;
+ // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: unchecked access to 'std::expected' value [bugprone-unchecked-expected-access]
+}
+
+void unchecked_arrow_operator_access(std::expected<Foo, int> e) {
+ e->foo();
+ // CHECK-MESSAGES: :[[@LINE-1]]:4: warning: unchecked access to 'std::expected' value [bugprone-unchecked-expected-access]
+}
+
+void const_ref_unchecked_value_access(const std::expected<int, int> &e) {
+ e.value();
+ // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: unchecked access to 'std::expected' value [bugprone-unchecked-expected-access]
+}
+
+void const_ref_unchecked_deref_operator_access(
+ const std::expected<int, int> &e) {
+ *e;
+ // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: unchecked access to 'std::expected' value [bugprone-unchecked-expected-access]
+}
+
+void const_ref_unchecked_arrow_operator_access(
+ const std::expected<Foo, int> &e) {
+ e->foo();
+ // CHECK-MESSAGES: :[[@LINE-1]]:4: warning: unchecked access to 'std::expected' value [bugprone-unchecked-expected-access]
+}
+
+void unchecked_access_to_function_result() {
+ auto e = make();
+ e.value();
+ // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: unchecked access to 'std::expected' value [bugprone-unchecked-expected-access]
+}
+
+void access_in_else_branch(std::expected<int, int> e) {
+ if (e.has_value()) {
+ } else {
+ e.value();
+ // CHECK-MESSAGES: :[[@LINE-1]]:7: warning: unchecked access to 'std::expected' value [bugprone-unchecked-expected-access]
+ }
+}
+
+void access_in_negated_check(std::expected<int, int> e) {
+ if (!e) {
+ *e;
+ // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: unchecked access to 'std::expected' value [bugprone-unchecked-expected-access]
+ }
+}
+
+int access_in_ternary_with_negated_check(std::expected<int, int> e) {
+ return !e ? *e : 0;
+ // CHECK-MESSAGES: :[[@LINE-1]]:15: warning: unchecked access to 'std::expected' value [bugprone-unchecked-expected-access]
+}
+
+void void_unchecked_value_access(std::expected<void, int> e) {
+ e.value();
+ // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: unchecked access to 'std::expected' value [bugprone-unchecked-expected-access]
+}
+
+// Checked accesses.
+
+void checked_access_with_has_value(std::expected<Foo, int> e) {
+ if (e.has_value()) {
+ e.value();
+ *e;
+ e->foo();
+ }
+}
+
+void checked_access_with_operator_bool(std::expected<int, int> e) {
+ if (e) {
+ e.value();
+ *e;
+ }
+}
+
+void checked_access_after_early_return(std::expected<int, int> e) {
+ if (!e)
+ return;
+ e.value();
+}
+
+int checked_access_after_early_return_with_has_value(
+ std::expected<int, int> e) {
+ if (!e.has_value())
+ return 0;
+ return *e;
+}
+
+int checked_access_in_ternary(std::expected<int, int> e) {
+ return e ? *e : 0;
+}
+
+int checked_access_in_ternary_with_has_value(std::expected<int, int> e) {
+ return e.has_value() ? e.value() : 0;
+}
+
+void value_or_is_not_an_access(std::expected<int, int> e) {
+ e.value_or(0);
+}
+
+void error_is_not_diagnosed(std::expected<int, int> e) {
+ e.error();
+}
>From b02b8068e2839ad3b387e73872f6ad1d14dce306 Mon Sep 17 00:00:00 2001
From: BaLiKfromUA <valentin.yukhymenko at gmail.com>
Date: Thu, 24 Sep 2026 22:39:53 +0100
Subject: [PATCH 03/25] Implementation that passes simple optional-like tests
---
.../bugprone/UncheckedExpectedAccessCheck.cpp | 55 ++++--
.../bugprone/UncheckedExpectedAccessCheck.h | 3 +-
clang-tools-extra/docs/ReleaseNotes.md | 3 +-
.../bugprone/unchecked-expected-access.md | 59 ++++++-
.../docs/clang-tidy/checks/list.md | 2 +-
.../Models/UncheckedExpectedAccessModel.h | 67 ++++++++
.../FlowSensitive/Models/CMakeLists.txt | 1 +
.../Models/UncheckedExpectedAccessModel.cpp | 160 ++++++++++++++++++
8 files changed, 334 insertions(+), 16 deletions(-)
create mode 100644 clang/include/clang/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.h
create mode 100644 clang/lib/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.cpp
diff --git a/clang-tools-extra/clang-tidy/bugprone/UncheckedExpectedAccessCheck.cpp b/clang-tools-extra/clang-tidy/bugprone/UncheckedExpectedAccessCheck.cpp
index c4dff1afa64803..460b506143be6d 100644
--- a/clang-tools-extra/clang-tidy/bugprone/UncheckedExpectedAccessCheck.cpp
+++ b/clang-tools-extra/clang-tidy/bugprone/UncheckedExpectedAccessCheck.cpp
@@ -7,26 +7,57 @@
//===----------------------------------------------------------------------===//
#include "UncheckedExpectedAccessCheck.h"
+#include "clang/AST/ASTContext.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
-
-using namespace clang::ast_matchers;
+#include "clang/ASTMatchers/ASTMatchers.h"
+#include "clang/Analysis/FlowSensitive/DataflowAnalysis.h"
+#include "clang/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.h"
+#include "clang/Basic/SourceLocation.h"
+#include "llvm/ADT/SmallVector.h"
+#include "llvm/Support/Error.h"
namespace clang::tidy::bugprone {
+using ast_matchers::MatchFinder;
+using dataflow::UncheckedExpectedAccessDiagnoser;
+using dataflow::UncheckedExpectedAccessModel;
+
+static constexpr StringRef FuncID = "fun";
void UncheckedExpectedAccessCheck::registerMatchers(MatchFinder *Finder) {
- // FIXME: Add matchers.
- Finder->addMatcher(functionDecl().bind("x"), this);
+ using namespace ast_matchers;
+
+ const auto HasExpectedCallDescendant =
+ hasDescendant(UncheckedExpectedAccessModel::callToExpectedClass());
+ Finder->addMatcher(
+ decl(anyOf(functionDecl(
+ // FIXME: Remove the filter below when lambdas are
+ // well supported by the check.
+ unless(hasDeclContext(cxxRecordDecl(isLambda()))),
+ hasBody(HasExpectedCallDescendant)),
+ cxxConstructorDecl(hasAnyConstructorInitializer(
+ withInitializer(HasExpectedCallDescendant)))))
+ .bind(FuncID),
+ this);
}
-void UncheckedExpectedAccessCheck::check(const MatchFinder::MatchResult &Result) {
- // FIXME: Add callback implementation.
- const auto *MatchedDecl = Result.Nodes.getNodeAs<FunctionDecl>("x");
- if (!MatchedDecl->getIdentifier() || MatchedDecl->getName().starts_with("awesome_"))
+void UncheckedExpectedAccessCheck::check(
+ const MatchFinder::MatchResult &Result) {
+ if (Result.SourceManager->getDiagnostics().hasUncompilableErrorOccurred())
return;
- diag(MatchedDecl->getLocation(), "function %0 is insufficiently awesome")
- << MatchedDecl
- << FixItHint::CreateInsertion(MatchedDecl->getLocation(), "awesome_");
- diag(MatchedDecl->getLocation(), "insert 'awesome'", DiagnosticIDs::Note);
+
+ const auto *FuncDecl = Result.Nodes.getNodeAs<FunctionDecl>(FuncID);
+ if (FuncDecl->isTemplated())
+ return;
+
+ UncheckedExpectedAccessDiagnoser Diagnoser;
+ if (llvm::Expected<SmallVector<SourceLocation>> Locs =
+ dataflow::diagnoseFunction<UncheckedExpectedAccessModel,
+ SourceLocation>(*FuncDecl, *Result.Context,
+ Diagnoser))
+ for (const SourceLocation &Loc : *Locs)
+ diag(Loc, "unchecked access to 'std::expected' value");
+ else
+ llvm::consumeError(Locs.takeError());
}
} // namespace clang::tidy::bugprone
diff --git a/clang-tools-extra/clang-tidy/bugprone/UncheckedExpectedAccessCheck.h b/clang-tools-extra/clang-tidy/bugprone/UncheckedExpectedAccessCheck.h
index 986fe17399773e..a28df33ea861d7 100644
--- a/clang-tools-extra/clang-tidy/bugprone/UncheckedExpectedAccessCheck.h
+++ b/clang-tools-extra/clang-tidy/bugprone/UncheckedExpectedAccessCheck.h
@@ -13,7 +13,8 @@
namespace clang::tidy::bugprone {
-/// FIXME: Write a short description.
+/// Warns when the code is accessing the value of a `std::expected<T, E>`
+/// object without assuring that it contains a value.
///
/// For the user-facing documentation see:
/// https://clang.llvm.org/extra/clang-tidy/checks/bugprone/unchecked-expected-access.html
diff --git a/clang-tools-extra/docs/ReleaseNotes.md b/clang-tools-extra/docs/ReleaseNotes.md
index c0cef06e995e0f..326933f09df1ab 100644
--- a/clang-tools-extra/docs/ReleaseNotes.md
+++ b/clang-tools-extra/docs/ReleaseNotes.md
@@ -132,7 +132,8 @@ infrastructure are described first, followed by tool-specific sections.
- New {doc}`bugprone-unchecked-expected-access
<clang-tidy/checks/bugprone/unchecked-expected-access>` check.
- FIXME: Write a short description.
+ Finds accesses to the value of a `std::expected` object that are not
+ guarded by a check that the object holds a value.
- New {doc}`llvm-invalid-regex-pattern
<clang-tidy/checks/llvm/invalid-regex-pattern>` check.
diff --git a/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md b/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md
index 886ec8a01adabe..2ea2d42d7c6f02 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md
+++ b/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md
@@ -3,4 +3,61 @@
# bugprone-unchecked-expected-access
-FIXME: Describe what patterns does the check detect and why. Give examples.
+*Note*: This check uses a flow-sensitive static analysis to produce its
+results. Therefore, it may be more resource intensive (RAM, CPU) than the
+average clang-tidy check.
+
+This check identifies unsafe accesses to values contained in
+`std::expected<T, E>` objects.
+
+An access to the value of a `std::expected<T, E>` occurs when one of its
+`value`, `operator*`, or `operator->` member functions is invoked. The check
+considers these member functions as equivalent, even though `value` throws
+`std::bad_expected_access` while `operator*` and `operator->` have undefined
+behavior when the object holds an error.
+
+An access to the value of a `std::expected<T, E>` is considered safe if and
+only if code in the local scope (for example, a function body) ensures that
+the object holds a value in all possible execution paths that can reach the
+access. That should happen through an explicit check, using the `has_value`
+member function or the conversion to `bool`.
+
+## Unsafe access patterns
+
+```cpp
+void f(std::expected<int, Error> e) {
+ use(*e); // unsafe: it is unclear whether `e` holds a value.
+}
+
+void g(std::expected<int, Error> e) {
+ if (!e) {
+ use(e.value()); // unsafe: `e` holds an error here.
+ }
+}
+```
+
+## Safe access patterns
+
+```cpp
+void f(std::expected<int, Error> e) {
+ if (e.has_value()) {
+ use(*e);
+ }
+}
+
+void g(std::expected<int, Error> e) {
+ if (!e)
+ return;
+ use(e.value());
+}
+
+int h(std::expected<int, Error> e) {
+ return e ? *e : 0;
+}
+```
+
+## Limitations
+
+The check does not yet model the constructors, assignment operators,
+`emplace` or `swap` of `std::expected`, so it does not know, for example,
+that a default-constructed `std::expected` holds a value.
diff --git a/clang-tools-extra/docs/clang-tidy/checks/list.md b/clang-tools-extra/docs/clang-tidy/checks/list.md
index 8e0b2639e3ad42..d5e73fb86932a7 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/list.md
+++ b/clang-tools-extra/docs/clang-tidy/checks/list.md
@@ -173,7 +173,7 @@ readability/*
| {doc}`bugprone-throw-keyword-missing <bugprone/throw-keyword-missing>` | |
| {doc}`bugprone-throwing-static-initialization <bugprone/throwing-static-initialization>` | |
| {doc}`bugprone-too-small-loop-variable <bugprone/too-small-loop-variable>` | |
-| {doc}`bugprone-unchecked-expected-access <bugprone/unchecked-expected-access>` | Yes |
+| {doc}`bugprone-unchecked-expected-access <bugprone/unchecked-expected-access>` | |
| {doc}`bugprone-unchecked-optional-access <bugprone/unchecked-optional-access>` | |
| {doc}`bugprone-unchecked-string-to-number-conversion <bugprone/unchecked-string-to-number-conversion>` | |
| {doc}`bugprone-undefined-memory-manipulation <bugprone/undefined-memory-manipulation>` | |
diff --git a/clang/include/clang/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.h b/clang/include/clang/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.h
new file mode 100644
index 00000000000000..1e9390ee190c3e
--- /dev/null
+++ b/clang/include/clang/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.h
@@ -0,0 +1,67 @@
+//===-- UncheckedExpectedAccessModel.h --------------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// This file defines a dataflow analysis that detects unsafe accesses to the
+// values of `std::expected` objects.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef CLANG_ANALYSIS_FLOWSENSITIVE_MODELS_UNCHECKEDEXPECTEDACCESSMODEL_H
+#define CLANG_ANALYSIS_FLOWSENSITIVE_MODELS_UNCHECKEDEXPECTEDACCESSMODEL_H
+
+#include "clang/AST/ASTContext.h"
+#include "clang/ASTMatchers/ASTMatchers.h"
+#include "clang/Analysis/CFG.h"
+#include "clang/Analysis/FlowSensitive/CFGMatchSwitch.h"
+#include "clang/Analysis/FlowSensitive/DataflowAnalysis.h"
+#include "clang/Analysis/FlowSensitive/DataflowEnvironment.h"
+#include "clang/Analysis/FlowSensitive/NoopLattice.h"
+#include "clang/Basic/SourceLocation.h"
+#include "llvm/ADT/SmallVector.h"
+
+namespace clang {
+namespace dataflow {
+
+/// Dataflow analysis that models whether `std::expected` objects hold values.
+class UncheckedExpectedAccessModel
+ : public DataflowAnalysis<UncheckedExpectedAccessModel, NoopLattice> {
+public:
+ UncheckedExpectedAccessModel(ASTContext &Ctx, Environment &Env);
+
+ /// Returns a matcher for calls to member functions of `std::expected`.
+ static ast_matchers::StatementMatcher callToExpectedClass();
+
+ static NoopLattice initialElement() { return {}; }
+
+ void transfer(const CFGElement &Elt, NoopLattice &L, Environment &Env);
+
+private:
+ CFGMatchSwitch<TransferState<NoopLattice>> TransferMatchSwitch;
+};
+
+/// Reports the locations of accesses to `std::expected` values that are not
+/// provably preceded by a check that the object holds a value.
+class UncheckedExpectedAccessDiagnoser {
+public:
+ UncheckedExpectedAccessDiagnoser();
+
+ llvm::SmallVector<SourceLocation>
+ operator()(const CFGElement &Elt, ASTContext &Ctx,
+ const TransferStateForDiagnostics<NoopLattice> &State) {
+ return DiagnoseMatchSwitch(Elt, Ctx, State.Env);
+ }
+
+private:
+ CFGMatchSwitch<const Environment, llvm::SmallVector<SourceLocation>>
+ DiagnoseMatchSwitch;
+};
+
+} // namespace dataflow
+} // namespace clang
+
+#endif // CLANG_ANALYSIS_FLOWSENSITIVE_MODELS_UNCHECKEDEXPECTEDACCESSMODEL_H
diff --git a/clang/lib/Analysis/FlowSensitive/Models/CMakeLists.txt b/clang/lib/Analysis/FlowSensitive/Models/CMakeLists.txt
index d1236f57148817..de286c3b90026f 100644
--- a/clang/lib/Analysis/FlowSensitive/Models/CMakeLists.txt
+++ b/clang/lib/Analysis/FlowSensitive/Models/CMakeLists.txt
@@ -1,5 +1,6 @@
add_clang_library(clangAnalysisFlowSensitiveModels
ChromiumCheckModel.cpp
+ UncheckedExpectedAccessModel.cpp
UncheckedOptionalAccessModel.cpp
UncheckedStatusOrAccessModel.cpp
diff --git a/clang/lib/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.cpp b/clang/lib/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.cpp
new file mode 100644
index 00000000000000..9bbb2f6e5b394d
--- /dev/null
+++ b/clang/lib/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.cpp
@@ -0,0 +1,160 @@
+//===-- UncheckedExpectedAccessModel.cpp ------------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// This file defines a dataflow analysis that detects unsafe accesses to the
+// values of `std::expected` objects.
+//
+//===----------------------------------------------------------------------===//
+
+#include "clang/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.h"
+#include "clang/AST/DeclCXX.h"
+#include "clang/AST/ExprCXX.h"
+#include "clang/ASTMatchers/ASTMatchersMacros.h"
+#include "clang/Analysis/FlowSensitive/StorageLocation.h"
+#include "clang/Analysis/FlowSensitive/Value.h"
+
+namespace clang {
+namespace dataflow {
+
+static bool isExpectedClass(const CXXRecordDecl &RD) {
+ return RD.getDeclName().isIdentifier() && RD.getName() == "expected" &&
+ RD.getDeclContext()->isStdNamespace();
+}
+
+// Classes derived from `std::expected` are deliberately not supported.
+static bool isExpectedType(QualType Ty) {
+ const CXXRecordDecl *RD = Ty->getAsCXXRecordDecl();
+ return RD != nullptr && isExpectedClass(*RD);
+}
+
+namespace {
+
+using namespace ::clang::ast_matchers;
+
+using LatticeTransferState = TransferState<NoopLattice>;
+
+AST_MATCHER(CXXRecordDecl, expectedClass) { return isExpectedClass(Node); }
+
+auto expectedMemberCall(ast_matchers::internal::Matcher<NamedDecl> Name) {
+ return cxxMemberCallExpr(
+ callee(cxxMethodDecl(Name, ofClass(expectedClass()))));
+}
+
+auto expectedOperatorCall(ArrayRef<StringRef> Names) {
+ return cxxOperatorCallExpr(hasAnyOverloadedOperatorName(Names),
+ callee(cxxMethodDecl(ofClass(expectedClass()))));
+}
+
+StorageLocation &locForHasValue(const RecordStorageLocation &ExpectedLoc) {
+ return ExpectedLoc.getSyntheticField("has_value");
+}
+
+/// Returns the symbolic value that represents the "has_value" property of the
+/// expected at `ExpectedLoc`. Returns null if `ExpectedLoc` is null or is not
+/// the location of a `std::expected` object (e.g. of a derived class object).
+BoolValue *getHasValue(Environment &Env, RecordStorageLocation *ExpectedLoc) {
+ if (ExpectedLoc == nullptr || !isExpectedType(ExpectedLoc->getType()))
+ return nullptr;
+ StorageLocation &HasValueLoc = locForHasValue(*ExpectedLoc);
+ auto *HasValueVal = Env.get<BoolValue>(HasValueLoc);
+ if (HasValueVal == nullptr) {
+ HasValueVal = &Env.makeAtomicBoolValue();
+ Env.setValue(HasValueLoc, *HasValueVal);
+ }
+ return HasValueVal;
+}
+
+void transferHasValueCall(const CXXMemberCallExpr *E,
+ const MatchFinder::MatchResult &,
+ LatticeTransferState &State) {
+ if (BoolValue *HasValueVal =
+ getHasValue(State.Env, getImplicitObjectLocation(*E, State.Env)))
+ State.Env.setValue(*E, *HasValueVal);
+}
+
+// FIXME: Model the constructors, assignments, `emplace` and `swap`.
+auto buildTransferMatchSwitch() {
+ return CFGMatchSwitchBuilder<LatticeTransferState>()
+ // expected::has_value, expected::operator bool
+ .CaseOfCFGStmt<CXXMemberCallExpr>(
+ expectedMemberCall(hasAnyName("has_value", "operator bool")),
+ transferHasValueCall)
+ .Build();
+}
+
+llvm::SmallVector<SourceLocation>
+diagnoseAccess(SourceLocation AccessLoc,
+ const RecordStorageLocation *ExpectedLoc,
+ const Environment &Env) {
+ if (ExpectedLoc != nullptr) {
+ // Accesses through classes derived from `std::expected` are not modeled.
+ if (!isExpectedType(ExpectedLoc->getType()))
+ return {};
+ if (auto *HasValueVal = Env.get<BoolValue>(locForHasValue(*ExpectedLoc)))
+ if (Env.proves(HasValueVal->formula()))
+ return {};
+ }
+
+ return {AccessLoc};
+}
+
+auto buildDiagnoseMatchSwitch() {
+ return CFGMatchSwitchBuilder<const Environment,
+ llvm::SmallVector<SourceLocation>>()
+ // expected::value
+ .CaseOfCFGStmt<CXXMemberCallExpr>(
+ expectedMemberCall(hasName("value")),
+ [](const CXXMemberCallExpr *E, const MatchFinder::MatchResult &,
+ const Environment &Env) {
+ return diagnoseAccess(E->getExprLoc(),
+ getImplicitObjectLocation(*E, Env), Env);
+ })
+ // expected::operator*, expected::operator->
+ .CaseOfCFGStmt<CXXOperatorCallExpr>(
+ expectedOperatorCall({"*", "->"}),
+ [](const CXXOperatorCallExpr *E, const MatchFinder::MatchResult &,
+ const Environment &Env) {
+ // `getExprLoc` of `operator->` is the start of the object
+ // expression, so point at the operator token instead.
+ return diagnoseAccess(E->getOperatorLoc(),
+ Env.get<RecordStorageLocation>(*E->getArg(0)),
+ Env);
+ })
+ .Build();
+}
+
+} // namespace
+
+ast_matchers::StatementMatcher
+UncheckedExpectedAccessModel::callToExpectedClass() {
+ return callExpr(callee(cxxMethodDecl(ofClass(expectedClass()))));
+}
+
+UncheckedExpectedAccessModel::UncheckedExpectedAccessModel(ASTContext &Ctx,
+ Environment &Env)
+ : DataflowAnalysis<UncheckedExpectedAccessModel, NoopLattice>(Ctx),
+ TransferMatchSwitch(buildTransferMatchSwitch()) {
+ Env.getDataflowAnalysisContext().setSyntheticFieldCallback(
+ [&Ctx](QualType Ty) -> llvm::StringMap<QualType> {
+ if (!isExpectedType(Ty))
+ return {};
+ return {{"has_value", Ctx.BoolTy}};
+ });
+}
+
+void UncheckedExpectedAccessModel::transfer(const CFGElement &Elt,
+ NoopLattice &L, Environment &Env) {
+ LatticeTransferState State(L, Env);
+ TransferMatchSwitch(Elt, getASTContext(), State);
+}
+
+UncheckedExpectedAccessDiagnoser::UncheckedExpectedAccessDiagnoser()
+ : DiagnoseMatchSwitch(buildDiagnoseMatchSwitch()) {}
+
+} // namespace dataflow
+} // namespace clang
>From 434cd0a9906a54f888817ca3cf841d4bb259ace3 Mon Sep 17 00:00:00 2001
From: BaLiKfromUA <valentin.yukhymenko at gmail.com>
Date: Thu, 24 Sep 2026 22:44:00 +0100
Subject: [PATCH 04/25] Add basic tests for `.error()` access
---
.../std/types/expected.h | 6 ++
.../bugprone/unchecked-expected-access.cpp | 100 +++++++++++++++++-
2 files changed, 105 insertions(+), 1 deletion(-)
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-expected-access/std/types/expected.h b/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-expected-access/std/types/expected.h
index 5cff26560980bc..0c350ab78e6aed 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-expected-access/std/types/expected.h
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-expected-access/std/types/expected.h
@@ -71,6 +71,9 @@ template <class T, class E> class expected {
template <class U> T value_or(U &&v) const &;
template <class U> T value_or(U &&v) &&;
+ template <class G> E error_or(G &&e) const &;
+ template <class G> E error_or(G &&e) &&;
+
template <class... Args> T &emplace(Args &&...args) noexcept;
void swap(expected &rhs) noexcept;
@@ -108,6 +111,9 @@ template <class E> class expected<void, E> {
const E &&error() const && noexcept;
E &&error() && noexcept;
+ template <class G> E error_or(G &&e) const &;
+ template <class G> E error_or(G &&e) &&;
+
void emplace() noexcept;
void swap(expected &rhs) noexcept;
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp
index 5ef426a24f157b..abb8dd22219e4f 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp
@@ -115,6 +115,104 @@ void value_or_is_not_an_access(std::expected<int, int> e) {
e.value_or(0);
}
-void error_is_not_diagnosed(std::expected<int, int> e) {
+// Unchecked error accesses.
+
+void unchecked_error_access(std::expected<int, int> e) {
+ e.error();
+ // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: unchecked access to 'std::expected' error [bugprone-unchecked-expected-access]
+}
+
+void const_ref_unchecked_error_access(const std::expected<int, int> &e) {
+ e.error();
+ // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: unchecked access to 'std::expected' error [bugprone-unchecked-expected-access]
+}
+
+void unchecked_error_access_to_function_result() {
+ auto e = make();
e.error();
+ // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: unchecked access to 'std::expected' error [bugprone-unchecked-expected-access]
+}
+
+void error_access_in_operator_bool_check(std::expected<int, int> e) {
+ if (e) {
+ e.error();
+ // CHECK-MESSAGES: :[[@LINE-1]]:7: warning: unchecked access to 'std::expected' error [bugprone-unchecked-expected-access]
+ }
+}
+
+void error_access_in_has_value_check(std::expected<int, int> e) {
+ if (e.has_value()) {
+ e.error();
+ // CHECK-MESSAGES: :[[@LINE-1]]:7: warning: unchecked access to 'std::expected' error [bugprone-unchecked-expected-access]
+ }
+}
+
+void error_access_after_early_return(std::expected<int, int> e) {
+ if (!e)
+ return;
+ e.error();
+ // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: unchecked access to 'std::expected' error [bugprone-unchecked-expected-access]
+}
+
+int error_access_in_ternary(std::expected<int, int> e) {
+ return e ? e.error() : 0;
+ // CHECK-MESSAGES: :[[@LINE-1]]:16: warning: unchecked access to 'std::expected' error [bugprone-unchecked-expected-access]
+}
+
+void void_unchecked_error_access(std::expected<void, int> e) {
+ e.error();
+ // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: unchecked access to 'std::expected' error [bugprone-unchecked-expected-access]
+}
+
+// Checked error accesses.
+
+void checked_error_access_with_operator_bool(std::expected<int, int> e) {
+ if (!e) {
+ e.error();
+ }
+}
+
+void checked_error_access_with_has_value(std::expected<int, int> e) {
+ if (!e.has_value()) {
+ e.error();
+ }
+}
+
+void checked_error_access_in_else_branch(std::expected<int, int> e) {
+ if (e) {
+ } else {
+ e.error();
+ }
+}
+
+int checked_error_access_after_early_return(std::expected<int, int> e) {
+ if (e)
+ return 0;
+ return e.error();
+}
+
+int checked_error_access_in_ternary(std::expected<int, int> e) {
+ return e ? 0 : e.error();
+}
+
+void checked_value_and_error_access(std::expected<int, int> e) {
+ if (e) {
+ *e;
+ } else {
+ e.error();
+ }
+}
+
+void void_checked_error_access(std::expected<void, int> e) {
+ if (!e)
+ e.error();
+}
+
+void error_or_is_not_an_access(std::expected<int, int> e) {
+ e.error_or(0);
+}
+
+void unexpected_error_is_not_diagnosed() {
+ std::unexpected<int> u(1);
+ u.error();
}
>From ad66657e56de469cfbff6b5cfe6cba41780cf08d Mon Sep 17 00:00:00 2001
From: BaLiKfromUA <valentin.yukhymenko at gmail.com>
Date: Thu, 24 Sep 2026 23:14:57 +0100
Subject: [PATCH 05/25] Add basic of `.error()`
---
.../bugprone/UncheckedExpectedAccessCheck.cpp | 16 +++++---
.../bugprone/UncheckedExpectedAccessCheck.h | 5 ++-
clang-tools-extra/docs/ReleaseNotes.md | 4 +-
.../bugprone/unchecked-expected-access.md | 17 +++++++-
.../Models/UncheckedExpectedAccessModel.h | 26 +++++++++++--
.../Models/UncheckedExpectedAccessModel.cpp | 39 ++++++++++++++-----
6 files changed, 82 insertions(+), 25 deletions(-)
diff --git a/clang-tools-extra/clang-tidy/bugprone/UncheckedExpectedAccessCheck.cpp b/clang-tools-extra/clang-tidy/bugprone/UncheckedExpectedAccessCheck.cpp
index 460b506143be6d..f35ce8c74bfca1 100644
--- a/clang-tools-extra/clang-tidy/bugprone/UncheckedExpectedAccessCheck.cpp
+++ b/clang-tools-extra/clang-tidy/bugprone/UncheckedExpectedAccessCheck.cpp
@@ -13,12 +13,14 @@
#include "clang/Analysis/FlowSensitive/DataflowAnalysis.h"
#include "clang/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.h"
#include "clang/Basic/SourceLocation.h"
+#include "llvm/ADT/STLForwardCompat.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/Support/Error.h"
namespace clang::tidy::bugprone {
using ast_matchers::MatchFinder;
using dataflow::UncheckedExpectedAccessDiagnoser;
+using dataflow::UncheckedExpectedAccessDiagnostic;
using dataflow::UncheckedExpectedAccessModel;
static constexpr StringRef FuncID = "fun";
@@ -50,14 +52,16 @@ void UncheckedExpectedAccessCheck::check(
return;
UncheckedExpectedAccessDiagnoser Diagnoser;
- if (llvm::Expected<SmallVector<SourceLocation>> Locs =
+ if (llvm::Expected<SmallVector<UncheckedExpectedAccessDiagnostic>> Diags =
dataflow::diagnoseFunction<UncheckedExpectedAccessModel,
- SourceLocation>(*FuncDecl, *Result.Context,
- Diagnoser))
- for (const SourceLocation &Loc : *Locs)
- diag(Loc, "unchecked access to 'std::expected' value");
+ UncheckedExpectedAccessDiagnostic>(
+ *FuncDecl, *Result.Context, Diagnoser))
+ for (const UncheckedExpectedAccessDiagnostic &Diag : *Diags)
+ diag(Diag.Loc,
+ "unchecked access to 'std::expected' %select{value|error}0")
+ << llvm::to_underlying(Diag.Kind);
else
- llvm::consumeError(Locs.takeError());
+ llvm::consumeError(Diags.takeError());
}
} // namespace clang::tidy::bugprone
diff --git a/clang-tools-extra/clang-tidy/bugprone/UncheckedExpectedAccessCheck.h b/clang-tools-extra/clang-tidy/bugprone/UncheckedExpectedAccessCheck.h
index a28df33ea861d7..b64362ba7b9d0b 100644
--- a/clang-tools-extra/clang-tidy/bugprone/UncheckedExpectedAccessCheck.h
+++ b/clang-tools-extra/clang-tidy/bugprone/UncheckedExpectedAccessCheck.h
@@ -13,8 +13,9 @@
namespace clang::tidy::bugprone {
-/// Warns when the code is accessing the value of a `std::expected<T, E>`
-/// object without assuring that it contains a value.
+/// Warns when the code is accessing the value (or error) of a
+/// `std::expected<T, E>` object without assuring that it contains a value
+/// (or error).
///
/// For the user-facing documentation see:
/// https://clang.llvm.org/extra/clang-tidy/checks/bugprone/unchecked-expected-access.html
diff --git a/clang-tools-extra/docs/ReleaseNotes.md b/clang-tools-extra/docs/ReleaseNotes.md
index 326933f09df1ab..fea94a044febff 100644
--- a/clang-tools-extra/docs/ReleaseNotes.md
+++ b/clang-tools-extra/docs/ReleaseNotes.md
@@ -132,8 +132,8 @@ infrastructure are described first, followed by tool-specific sections.
- New {doc}`bugprone-unchecked-expected-access
<clang-tidy/checks/bugprone/unchecked-expected-access>` check.
- Finds accesses to the value of a `std::expected` object that are not
- guarded by a check that the object holds a value.
+ Finds accesses to the value (or error) of a `std::expected` object that are
+ not guarded by a check that the object holds a value (or error).
- New {doc}`llvm-invalid-regex-pattern
<clang-tidy/checks/llvm/invalid-regex-pattern>` check.
diff --git a/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md b/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md
index 2ea2d42d7c6f02..b7d431382d1efe 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md
+++ b/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md
@@ -7,7 +7,7 @@
results. Therefore, it may be more resource intensive (RAM, CPU) than the
average clang-tidy check.
-This check identifies unsafe accesses to values contained in
+This check identifies unsafe accesses to values and errors contained in
`std::expected<T, E>` objects.
An access to the value of a `std::expected<T, E>` occurs when one of its
@@ -22,6 +22,12 @@ the object holds a value in all possible execution paths that can reach the
access. That should happen through an explicit check, using the `has_value`
member function or the conversion to `bool`.
+Likewise, an access to the error of a `std::expected<T, E>` occurs when its
+`error` member function is invoked, which has undefined behavior when the
+object holds a value. Such an access is considered safe if and only if code in
+the local scope ensures that the object does *not* hold a value in all
+possible execution paths that can reach the access.
+
## Unsafe access patterns
```cpp
@@ -34,6 +40,10 @@ void g(std::expected<int, Error> e) {
use(e.value()); // unsafe: `e` holds an error here.
}
}
+
+void h(std::expected<int, Error> e) {
+ report(e.error()); // unsafe: it is unclear whether `e` holds an error.
+}
```
## Safe access patterns
@@ -54,6 +64,11 @@ void g(std::expected<int, Error> e) {
int h(std::expected<int, Error> e) {
return e ? *e : 0;
}
+
+void i(std::expected<int, Error> e) {
+ if (!e)
+ report(e.error());
+}
```
## Limitations
diff --git a/clang/include/clang/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.h b/clang/include/clang/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.h
index 1e9390ee190c3e..b6d6b0e5e118a6 100644
--- a/clang/include/clang/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.h
+++ b/clang/include/clang/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.h
@@ -44,20 +44,38 @@ class UncheckedExpectedAccessModel
CFGMatchSwitch<TransferState<NoopLattice>> TransferMatchSwitch;
};
-/// Reports the locations of accesses to `std::expected` values that are not
-/// provably preceded by a check that the object holds a value.
+/// The part of a `std::expected` object that is accessed.
+///
+/// The values are used as `%select` indices in diagnostics, so they must not
+/// be reordered.
+enum class UncheckedExpectedAccessKind : int {
+ /// `value()`, `operator*` or `operator->`, which require a value.
+ Value = 0,
+ /// `error()`, which requires an error.
+ Error = 1,
+};
+
+/// Diagnostic information for an unchecked `std::expected` access.
+struct UncheckedExpectedAccessDiagnostic {
+ SourceLocation Loc;
+ UncheckedExpectedAccessKind Kind;
+};
+
+/// Reports accesses to the value (or error) of `std::expected` objects that are
+/// not provably preceded by a check that the object holds a value (or error).
class UncheckedExpectedAccessDiagnoser {
public:
UncheckedExpectedAccessDiagnoser();
- llvm::SmallVector<SourceLocation>
+ llvm::SmallVector<UncheckedExpectedAccessDiagnostic>
operator()(const CFGElement &Elt, ASTContext &Ctx,
const TransferStateForDiagnostics<NoopLattice> &State) {
return DiagnoseMatchSwitch(Elt, Ctx, State.Env);
}
private:
- CFGMatchSwitch<const Environment, llvm::SmallVector<SourceLocation>>
+ CFGMatchSwitch<const Environment,
+ llvm::SmallVector<UncheckedExpectedAccessDiagnostic>>
DiagnoseMatchSwitch;
};
diff --git a/clang/lib/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.cpp b/clang/lib/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.cpp
index 9bbb2f6e5b394d..f9376bafd98d0b 100644
--- a/clang/lib/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.cpp
+++ b/clang/lib/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.cpp
@@ -15,6 +15,8 @@
#include "clang/AST/DeclCXX.h"
#include "clang/AST/ExprCXX.h"
#include "clang/ASTMatchers/ASTMatchersMacros.h"
+#include "clang/Analysis/FlowSensitive/Arena.h"
+#include "clang/Analysis/FlowSensitive/Formula.h"
#include "clang/Analysis/FlowSensitive/StorageLocation.h"
#include "clang/Analysis/FlowSensitive/Value.h"
@@ -87,31 +89,39 @@ auto buildTransferMatchSwitch() {
.Build();
}
-llvm::SmallVector<SourceLocation>
-diagnoseAccess(SourceLocation AccessLoc,
+llvm::SmallVector<UncheckedExpectedAccessDiagnostic>
+diagnoseAccess(SourceLocation AccessLoc, UncheckedExpectedAccessKind Kind,
const RecordStorageLocation *ExpectedLoc,
const Environment &Env) {
if (ExpectedLoc != nullptr) {
// Accesses through classes derived from `std::expected` are not modeled.
if (!isExpectedType(ExpectedLoc->getType()))
return {};
- if (auto *HasValueVal = Env.get<BoolValue>(locForHasValue(*ExpectedLoc)))
- if (Env.proves(HasValueVal->formula()))
+ if (auto *HasValueVal = Env.get<BoolValue>(locForHasValue(*ExpectedLoc))) {
+ // The value may only be accessed if the expected holds a value, and the
+ // error only if it does not.
+ const Formula &Safe = Kind == UncheckedExpectedAccessKind::Value
+ ? HasValueVal->formula()
+ : Env.arena().makeNot(HasValueVal->formula());
+ if (Env.proves(Safe))
return {};
+ }
}
- return {AccessLoc};
+ return {{AccessLoc, Kind}};
}
auto buildDiagnoseMatchSwitch() {
- return CFGMatchSwitchBuilder<const Environment,
- llvm::SmallVector<SourceLocation>>()
+ return CFGMatchSwitchBuilder<
+ const Environment,
+ llvm::SmallVector<UncheckedExpectedAccessDiagnostic>>()
// expected::value
.CaseOfCFGStmt<CXXMemberCallExpr>(
expectedMemberCall(hasName("value")),
[](const CXXMemberCallExpr *E, const MatchFinder::MatchResult &,
const Environment &Env) {
return diagnoseAccess(E->getExprLoc(),
+ UncheckedExpectedAccessKind::Value,
getImplicitObjectLocation(*E, Env), Env);
})
// expected::operator*, expected::operator->
@@ -121,9 +131,18 @@ auto buildDiagnoseMatchSwitch() {
const Environment &Env) {
// `getExprLoc` of `operator->` is the start of the object
// expression, so point at the operator token instead.
- return diagnoseAccess(E->getOperatorLoc(),
- Env.get<RecordStorageLocation>(*E->getArg(0)),
- Env);
+ return diagnoseAccess(
+ E->getOperatorLoc(), UncheckedExpectedAccessKind::Value,
+ Env.get<RecordStorageLocation>(*E->getArg(0)), Env);
+ })
+ // expected::error
+ .CaseOfCFGStmt<CXXMemberCallExpr>(
+ expectedMemberCall(hasName("error")),
+ [](const CXXMemberCallExpr *E, const MatchFinder::MatchResult &,
+ const Environment &Env) {
+ return diagnoseAccess(E->getExprLoc(),
+ UncheckedExpectedAccessKind::Error,
+ getImplicitObjectLocation(*E, Env), Env);
})
.Build();
}
>From 9ddd30466505f7264fda9790f758ed5e334ebc07 Mon Sep 17 00:00:00 2001
From: BaLiKfromUA <valentin.yukhymenko at gmail.com>
Date: Thu, 24 Sep 2026 23:23:45 +0100
Subject: [PATCH 06/25] basic tests for emplace
---
.../bugprone/unchecked-expected-access.cpp | 55 +++++++++++++++++++
1 file changed, 55 insertions(+)
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp
index abb8dd22219e4f..c7dd287264c7d4 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp
@@ -216,3 +216,58 @@ void unexpected_error_is_not_diagnosed() {
std::unexpected<int> u(1);
u.error();
}
+
+// emplace.
+
+void emplace_then_access(std::expected<int, int> e) {
+ e.emplace(1);
+ e.value();
+ *e;
+}
+
+void emplace_then_arrow_access(std::expected<Foo, int> e) {
+ e.emplace();
+ e->foo();
+}
+
+int emplace_if_empty(std::expected<int, int> e) {
+ if (!e)
+ e.emplace(0);
+ return *e;
+}
+
+int emplace_through_pointer(std::expected<int, int> *p) {
+ p->emplace(1);
+ return **p;
+}
+
+void void_emplace_then_access(std::expected<void, int> e) {
+ e.emplace();
+ e.value();
+}
+
+void error_access_after_emplace(std::expected<int, int> e) {
+ e.emplace(1);
+ e.error();
+ // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: unchecked access to 'std::expected' error [bugprone-unchecked-expected-access]
+}
+
+int emplace_on_other_object(std::expected<int, int> a,
+ std::expected<int, int> b) {
+ a.emplace(1);
+ return *b;
+ // CHECK-MESSAGES: :[[@LINE-1]]:10: warning: unchecked access to 'std::expected' value [bugprone-unchecked-expected-access]
+}
+
+int emplace_on_one_path(std::expected<int, int> e, bool cond) {
+ if (cond)
+ e.emplace(1);
+ return *e;
+ // CHECK-MESSAGES: :[[@LINE-1]]:10: warning: unchecked access to 'std::expected' value [bugprone-unchecked-expected-access]
+}
+
+void void_error_access_after_emplace(std::expected<void, int> e) {
+ e.emplace();
+ e.error();
+ // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: unchecked access to 'std::expected' error [bugprone-unchecked-expected-access]
+}
>From 5ea54083caf0e13635b9291aeb4c96f48bb035c0 Mon Sep 17 00:00:00 2001
From: BaLiKfromUA <valentin.yukhymenko at gmail.com>
Date: Thu, 24 Sep 2026 23:29:23 +0100
Subject: [PATCH 07/25] support `emplace` for basic scenarios
---
.../bugprone/unchecked-expected-access.md | 12 +++++++---
.../Models/UncheckedExpectedAccessModel.cpp | 24 ++++++++++++++++++-
2 files changed, 32 insertions(+), 4 deletions(-)
diff --git a/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md b/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md
index b7d431382d1efe..0c348ec4563b0c 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md
+++ b/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md
@@ -69,10 +69,16 @@ void i(std::expected<int, Error> e) {
if (!e)
report(e.error());
}
+
+int j(std::expected<int, Error> e) {
+ if (!e)
+ e.emplace(0);
+ return *e;
+}
```
## Limitations
-The check does not yet model the constructors, assignment operators,
-`emplace` or `swap` of `std::expected`, so it does not know, for example,
-that a default-constructed `std::expected` holds a value.
+The check does not yet model the constructors, assignment operators or
+`swap` of `std::expected`, so it does not know, for example, that a
+default-constructed `std::expected` holds a value.
diff --git a/clang/lib/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.cpp b/clang/lib/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.cpp
index f9376bafd98d0b..427afa36c7cc71 100644
--- a/clang/lib/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.cpp
+++ b/clang/lib/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.cpp
@@ -71,6 +71,17 @@ BoolValue *getHasValue(Environment &Env, RecordStorageLocation *ExpectedLoc) {
return HasValueVal;
}
+/// Sets `HasValueVal` as the symbolic value that represents the "has_value"
+/// property of the expected at `ExpectedLoc`. Does nothing if `ExpectedLoc` is
+/// null or is not the location of a `std::expected` object (e.g. of a derived
+/// class object).
+void setHasValue(RecordStorageLocation *ExpectedLoc, BoolValue &HasValueVal,
+ Environment &Env) {
+ if (ExpectedLoc == nullptr || !isExpectedType(ExpectedLoc->getType()))
+ return;
+ Env.setValue(locForHasValue(*ExpectedLoc), HasValueVal);
+}
+
void transferHasValueCall(const CXXMemberCallExpr *E,
const MatchFinder::MatchResult &,
LatticeTransferState &State) {
@@ -79,13 +90,24 @@ void transferHasValueCall(const CXXMemberCallExpr *E,
State.Env.setValue(*E, *HasValueVal);
}
-// FIXME: Model the constructors, assignments, `emplace` and `swap`.
+void transferEmplaceCall(const CXXMemberCallExpr *E,
+ const MatchFinder::MatchResult &,
+ LatticeTransferState &State) {
+ // Every `emplace` overload leaves the expected holding a value.
+ setHasValue(getImplicitObjectLocation(*E, State.Env),
+ State.Env.getBoolLiteralValue(true), State.Env);
+}
+
+// FIXME: Model the constructors, assignments and `swap`.
auto buildTransferMatchSwitch() {
return CFGMatchSwitchBuilder<LatticeTransferState>()
// expected::has_value, expected::operator bool
.CaseOfCFGStmt<CXXMemberCallExpr>(
expectedMemberCall(hasAnyName("has_value", "operator bool")),
transferHasValueCall)
+ // expected::emplace
+ .CaseOfCFGStmt<CXXMemberCallExpr>(expectedMemberCall(hasName("emplace")),
+ transferEmplaceCall)
.Build();
}
>From 32bf307310510241d392ad09a27779f40034efd3 Mon Sep 17 00:00:00 2001
From: BaLiKfromUA <valentin.yukhymenko at gmail.com>
Date: Thu, 24 Sep 2026 23:42:49 +0100
Subject: [PATCH 08/25] Basic tests for `operator ==`
---
.../std/types/expected.h | 6 ++
.../bugprone/unchecked-expected-access.cpp | 78 +++++++++++++++++++
2 files changed, 84 insertions(+)
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-expected-access/std/types/expected.h b/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-expected-access/std/types/expected.h
index 0c350ab78e6aed..e41fc730852655 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-expected-access/std/types/expected.h
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-expected-access/std/types/expected.h
@@ -77,6 +77,9 @@ template <class T, class E> class expected {
template <class... Args> T &emplace(Args &&...args) noexcept;
void swap(expected &rhs) noexcept;
+
+ template <class T2, class E2>
+ friend bool operator==(const expected &x, const expected<T2, E2> &y);
};
template <class E> class expected<void, E> {
@@ -117,6 +120,9 @@ template <class E> class expected<void, E> {
void emplace() noexcept;
void swap(expected &rhs) noexcept;
+
+ template <class T2, class E2>
+ friend bool operator==(const expected &x, const expected<T2, E2> &y);
};
} // namespace std
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp
index c7dd287264c7d4..de5b38db4892fa 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp
@@ -271,3 +271,81 @@ void void_error_access_after_emplace(std::expected<void, int> e) {
e.error();
// CHECK-MESSAGES: :[[@LINE-1]]:5: warning: unchecked access to 'std::expected' error [bugprone-unchecked-expected-access]
}
+
+// Comparison of two expected objects.
+
+int equal_to_expected_with_value(std::expected<int, int> a,
+ std::expected<int, int> b) {
+ if (a == b && b)
+ return *a;
+ return 0;
+}
+
+int equal_to_expected_with_error(std::expected<int, int> a,
+ std::expected<int, int> b) {
+ if (a == b && !b)
+ return a.error();
+ return 0;
+}
+
+int equal_then_check_other(std::expected<int, int> a,
+ std::expected<int, int> b) {
+ if (a == b) {
+ if (b)
+ return *a;
+ }
+ return 0;
+}
+
+int not_not_equal_to_expected_with_value(std::expected<int, int> a,
+ std::expected<int, int> b) {
+ if (a != b)
+ return 0;
+ if (b)
+ return *a;
+ return 0;
+}
+
+int equal_to_expected_with_other_value_type(std::expected<int, int> a,
+ std::expected<long, int> b) {
+ if (a == b && b)
+ return *a;
+ return 0;
+}
+
+void void_not_equal_to_expected_with_value(std::expected<void, int> a,
+ std::expected<void, int> b) {
+ if (a != b && a)
+ b.error();
+}
+
+int equal_to_unchecked_expected(std::expected<int, int> a,
+ std::expected<int, int> b) {
+ if (a == b)
+ return *a;
+ // CHECK-MESSAGES: :[[@LINE-1]]:12: warning: unchecked access to 'std::expected' value [bugprone-unchecked-expected-access]
+ return 0;
+}
+
+int not_equal_to_expected_with_value(std::expected<int, int> a,
+ std::expected<int, int> b) {
+ if (a != b && b)
+ return *a;
+ // CHECK-MESSAGES: :[[@LINE-1]]:12: warning: unchecked access to 'std::expected' value [bugprone-unchecked-expected-access]
+ return 0;
+}
+
+void not_equal_values_may_differ(std::expected<int, int> a,
+ std::expected<int, int> b) {
+ if (a != b && a)
+ b.error();
+ // CHECK-MESSAGES: :[[@LINE-1]]:7: warning: unchecked access to 'std::expected' error [bugprone-unchecked-expected-access]
+}
+
+int equal_or_other_has_value(std::expected<int, int> a,
+ std::expected<int, int> b) {
+ if (a == b || b)
+ return *a;
+ // CHECK-MESSAGES: :[[@LINE-1]]:12: warning: unchecked access to 'std::expected' value [bugprone-unchecked-expected-access]
+ return 0;
+}
>From a09b035c5b079235414a80ecf90cd8ba01d0ca7d Mon Sep 17 00:00:00 2001
From: BaLiKfromUA <valentin.yukhymenko at gmail.com>
Date: Sat, 26 Sep 2026 21:29:56 +0100
Subject: [PATCH 09/25] Model `operator ==` for comparing expected to other
expected
---
.../bugprone/unchecked-expected-access.md | 6 ++
.../Models/UncheckedExpectedAccessModel.cpp | 64 +++++++++++++++++++
2 files changed, 70 insertions(+)
diff --git a/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md b/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md
index 0c348ec4563b0c..00cde1f4a3288d 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md
+++ b/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md
@@ -75,6 +75,12 @@ int j(std::expected<int, Error> e) {
e.emplace(0);
return *e;
}
+
+int k(std::expected<int, Error> a, std::expected<int, Error> b) {
+ if (a == b && b)
+ return *a; // equal objects either both hold a value or both an error.
+ return 0;
+}
```
## Limitations
diff --git a/clang/lib/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.cpp b/clang/lib/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.cpp
index 427afa36c7cc71..9b8ff84d4f7bab 100644
--- a/clang/lib/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.cpp
+++ b/clang/lib/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.cpp
@@ -13,6 +13,7 @@
#include "clang/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.h"
#include "clang/AST/DeclCXX.h"
+#include "clang/AST/DeclTemplate.h"
#include "clang/AST/ExprCXX.h"
#include "clang/ASTMatchers/ASTMatchersMacros.h"
#include "clang/Analysis/FlowSensitive/Arena.h"
@@ -34,6 +35,17 @@ static bool isExpectedType(QualType Ty) {
return RD != nullptr && isExpectedClass(*RD);
}
+/// Returns true if `Ty` is `std::expected<void, E>`.
+static bool isVoidExpectedType(QualType Ty) {
+ const auto *CTSD = dyn_cast_or_null<ClassTemplateSpecializationDecl>(
+ Ty->getAsCXXRecordDecl());
+ if (CTSD == nullptr || !isExpectedClass(*CTSD))
+ return false;
+ const TemplateArgument &ValueType = CTSD->getTemplateArgs()[0];
+ return ValueType.getKind() == TemplateArgument::Type &&
+ ValueType.getAsType()->isVoidType();
+}
+
namespace {
using namespace ::clang::ast_matchers;
@@ -52,6 +64,28 @@ auto expectedOperatorCall(ArrayRef<StringRef> Names) {
callee(cxxMethodDecl(ofClass(expectedClass()))));
}
+auto hasExpectedType() {
+ return hasType(hasUnqualifiedDesugaredType(
+ recordType(hasDeclaration(cxxRecordDecl(expectedClass())))));
+}
+
+auto expectedEqualityCall() {
+ return cxxOperatorCallExpr(
+ hasOverloadedOperatorName("=="), argumentCountIs(2),
+ hasArgument(0, hasExpectedType()), hasArgument(1, hasExpectedType()));
+}
+
+/// Ensures that `E` is mapped to a `BoolValue` and returns its formula.
+const Formula &forceBoolValue(Environment &Env, const Expr &E) {
+ auto *Value = Env.get<BoolValue>(E);
+ if (Value != nullptr)
+ return Value->formula();
+
+ Value = &Env.makeAtomicBoolValue();
+ Env.setValue(E, *Value);
+ return Value->formula();
+}
+
StorageLocation &locForHasValue(const RecordStorageLocation &ExpectedLoc) {
return ExpectedLoc.getSyntheticField("has_value");
}
@@ -98,6 +132,33 @@ void transferEmplaceCall(const CXXMemberCallExpr *E,
State.Env.getBoolLiteralValue(true), State.Env);
}
+// `x != y` is rewritten to `!(x == y)`, so it needs no separate handling.
+void transferExpectedEqualityCall(const CXXOperatorCallExpr *E,
+ const MatchFinder::MatchResult &,
+ LatticeTransferState &State) {
+ Environment &Env = State.Env;
+ BoolValue *LHasValueVal =
+ getHasValue(Env, Env.get<RecordStorageLocation>(*E->getArg(0)));
+ BoolValue *RHasValueVal =
+ getHasValue(Env, Env.get<RecordStorageLocation>(*E->getArg(1)));
+ if (LHasValueVal == nullptr || RHasValueVal == nullptr)
+ return;
+
+ Arena &A = Env.arena();
+ const Formula &EqVal = forceBoolValue(Env, *E);
+ const Formula &LHasValue = LHasValueVal->formula();
+ const Formula &RHasValue = RHasValueVal->formula();
+
+ // Equal expecteds either both hold a value or both hold an error. Nothing
+ // more follows in general: the values (or errors) themselves may differ.
+ Env.assume(A.makeImplies(EqVal, A.makeEquals(LHasValue, RHasValue)));
+
+ // `expected<void, E>` objects that both hold a value are always equal.
+ if (isVoidExpectedType(E->getArg(0)->getType()) &&
+ isVoidExpectedType(E->getArg(1)->getType()))
+ Env.assume(A.makeImplies(A.makeAnd(LHasValue, RHasValue), EqVal));
+}
+
// FIXME: Model the constructors, assignments and `swap`.
auto buildTransferMatchSwitch() {
return CFGMatchSwitchBuilder<LatticeTransferState>()
@@ -108,6 +169,9 @@ auto buildTransferMatchSwitch() {
// expected::emplace
.CaseOfCFGStmt<CXXMemberCallExpr>(expectedMemberCall(hasName("emplace")),
transferEmplaceCall)
+ // operator== between two expecteds
+ .CaseOfCFGStmt<CXXOperatorCallExpr>(expectedEqualityCall(),
+ transferExpectedEqualityCall)
.Build();
}
>From da5bdcc370da21ca9cffbf9dcb4a24dd5ff949dd Mon Sep 17 00:00:00 2001
From: BaLiKfromUA <valentin.yukhymenko at gmail.com>
Date: Sat, 26 Sep 2026 21:36:55 +0100
Subject: [PATCH 10/25] Basic tests for comparing expected with value
---
.../std/types/expected.h | 10 +++
.../bugprone/unchecked-expected-access.cpp | 61 +++++++++++++++++++
2 files changed, 71 insertions(+)
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-expected-access/std/types/expected.h b/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-expected-access/std/types/expected.h
index e41fc730852655..b2f7088836741d 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-expected-access/std/types/expected.h
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-expected-access/std/types/expected.h
@@ -26,6 +26,12 @@ struct unexpect_t {
inline constexpr unexpect_t unexpect{};
+template <class T, class E> class expected;
+
+template <class> constexpr bool __is_expected = false;
+template <class T, class E>
+constexpr bool __is_expected<expected<T, E>> = true;
+
template <class T, class E> class expected {
public:
using value_type = T;
@@ -80,6 +86,10 @@ template <class T, class E> class expected {
template <class T2, class E2>
friend bool operator==(const expected &x, const expected<T2, E2> &y);
+
+ template <class T2>
+ requires(!__is_expected<T2>)
+ friend bool operator==(const expected &x, const T2 &v);
};
template <class E> class expected<void, E> {
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp
index de5b38db4892fa..d787dc43ca5247 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp
@@ -349,3 +349,64 @@ int equal_or_other_has_value(std::expected<int, int> a,
// CHECK-MESSAGES: :[[@LINE-1]]:12: warning: unchecked access to 'std::expected' value [bugprone-unchecked-expected-access]
return 0;
}
+
+// Comparison of expected with a value.
+
+int equal_to_value(std::expected<int, int> e) {
+ if (e == 5)
+ return *e;
+ return 0;
+}
+
+int value_equal_to_expected(std::expected<int, int> e) {
+ if (5 == e)
+ return e.value();
+ return 0;
+}
+
+int not_equal_to_value_early_return(std::expected<int, int> e) {
+ if (e != 5)
+ return 0;
+ return *e;
+}
+
+long equal_to_value_of_other_type(std::expected<long, int> e, int v) {
+ if (e == v)
+ return *e;
+ return 0;
+}
+
+int not_equal_to_value(std::expected<int, int> e) {
+ if (e != 5)
+ return *e;
+ // CHECK-MESSAGES: :[[@LINE-1]]:12: warning: unchecked access to 'std::expected' value [bugprone-unchecked-expected-access]
+ return 0;
+}
+
+int not_equal_to_value_after_equal(std::expected<int, int> e) {
+ if (e == 5)
+ return 0;
+ return *e;
+ // CHECK-MESSAGES: :[[@LINE-1]]:10: warning: unchecked access to 'std::expected' value [bugprone-unchecked-expected-access]
+}
+
+int error_access_when_equal_to_value(std::expected<int, int> e) {
+ if (e == 5)
+ return e.error();
+ // CHECK-MESSAGES: :[[@LINE-1]]:14: warning: unchecked access to 'std::expected' error [bugprone-unchecked-expected-access]
+ return 0;
+}
+
+int error_access_when_not_equal_to_value(std::expected<int, int> e) {
+ if (e != 5)
+ return e.error();
+ // CHECK-MESSAGES: :[[@LINE-1]]:14: warning: unchecked access to 'std::expected' error [bugprone-unchecked-expected-access]
+ return 0;
+}
+
+int equal_to_value_or_other(std::expected<int, int> e, bool cond) {
+ if (e == 5 || cond)
+ return *e;
+ // CHECK-MESSAGES: :[[@LINE-1]]:12: warning: unchecked access to 'std::expected' value [bugprone-unchecked-expected-access]
+ return 0;
+}
>From e3e892ea29e7da8c0d381d4b650d4f5e95c31c3d Mon Sep 17 00:00:00 2001
From: BaLiKfromUA <valentin.yukhymenko at gmail.com>
Date: Sat, 26 Sep 2026 22:08:57 +0100
Subject: [PATCH 11/25] Tests for `==` with unexpected
---
.../std/types/expected.h | 6 +++
.../bugprone/unchecked-expected-access.cpp | 52 +++++++++++++++++++
2 files changed, 58 insertions(+)
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-expected-access/std/types/expected.h b/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-expected-access/std/types/expected.h
index b2f7088836741d..d8729b3831bd47 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-expected-access/std/types/expected.h
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-expected-access/std/types/expected.h
@@ -90,6 +90,9 @@ template <class T, class E> class expected {
template <class T2>
requires(!__is_expected<T2>)
friend bool operator==(const expected &x, const T2 &v);
+
+ template <class E2>
+ friend bool operator==(const expected &x, const unexpected<E2> &e);
};
template <class E> class expected<void, E> {
@@ -133,6 +136,9 @@ template <class E> class expected<void, E> {
template <class T2, class E2>
friend bool operator==(const expected &x, const expected<T2, E2> &y);
+
+ template <class E2>
+ friend bool operator==(const expected &x, const unexpected<E2> &e);
};
} // namespace std
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp
index d787dc43ca5247..efb52561484ed3 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp
@@ -410,3 +410,55 @@ int equal_to_value_or_other(std::expected<int, int> e, bool cond) {
// CHECK-MESSAGES: :[[@LINE-1]]:12: warning: unchecked access to 'std::expected' value [bugprone-unchecked-expected-access]
return 0;
}
+
+// Comparison of expected with an unexpected.
+
+int equal_to_unexpected(std::expected<int, int> e) {
+ if (e == std::unexpected(1))
+ return e.error();
+ return 0;
+}
+
+int unexpected_equal_to_expected(std::expected<int, int> e) {
+ if (std::unexpected(1) == e)
+ return e.error();
+ return 0;
+}
+
+int not_equal_to_unexpected_early_return(std::expected<int, int> e) {
+ if (e != std::unexpected(1))
+ return 0;
+ return e.error();
+}
+
+void void_equal_to_unexpected(std::expected<void, int> e) {
+ if (e == std::unexpected(1))
+ e.error();
+}
+
+int value_access_when_equal_to_unexpected(std::expected<int, int> e) {
+ if (e == std::unexpected(1))
+ return *e;
+ // CHECK-MESSAGES: :[[@LINE-1]]:12: warning: unchecked access to 'std::expected' value [bugprone-unchecked-expected-access]
+ return 0;
+}
+
+int error_access_when_not_equal_to_unexpected(std::expected<int, int> e) {
+ if (e != std::unexpected(1))
+ return e.error();
+ // CHECK-MESSAGES: :[[@LINE-1]]:14: warning: unchecked access to 'std::expected' error [bugprone-unchecked-expected-access]
+ return 0;
+}
+
+int error_access_after_equal_to_unexpected(std::expected<int, int> e) {
+ if (e == std::unexpected(1))
+ return 0;
+ return e.error();
+ // CHECK-MESSAGES: :[[@LINE-1]]:12: warning: unchecked access to 'std::expected' error [bugprone-unchecked-expected-access]
+}
+
+void void_value_access_when_equal_to_unexpected(std::expected<void, int> e) {
+ if (e == std::unexpected(1))
+ e.value();
+ // CHECK-MESSAGES: :[[@LINE-1]]:7: warning: unchecked access to 'std::expected' value [bugprone-unchecked-expected-access]
+}
>From 90f974ad92238809c5a92cf113d67ff4bc8ee74e Mon Sep 17 00:00:00 2001
From: BaLiKfromUA <valentin.yukhymenko at gmail.com>
Date: Sat, 26 Sep 2026 22:20:36 +0100
Subject: [PATCH 12/25] Basic implemenation to `==` modeling for value or
unexpected comparisons
---
.../bugprone/unchecked-expected-access.md | 12 ++++
.../Models/UncheckedExpectedAccessModel.cpp | 63 +++++++++++++++++++
2 files changed, 75 insertions(+)
diff --git a/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md b/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md
index 00cde1f4a3288d..d4175ea28860bd 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md
+++ b/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md
@@ -81,6 +81,18 @@ int k(std::expected<int, Error> a, std::expected<int, Error> b) {
return *a; // equal objects either both hold a value or both an error.
return 0;
}
+
+int l(std::expected<int, Error> e) {
+ if (e == 42)
+ return *e; // only an expected holding a value compares equal to a value.
+ return 0;
+}
+
+void m(std::expected<int, Error> e) {
+ if (e == std::unexpected(Error::NotFound))
+ report(e.error()); // only an expected holding an error compares equal
+ // to an unexpected.
+}
```
## Limitations
diff --git a/clang/lib/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.cpp b/clang/lib/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.cpp
index 9b8ff84d4f7bab..4b299595befdc0 100644
--- a/clang/lib/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.cpp
+++ b/clang/lib/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.cpp
@@ -54,6 +54,11 @@ using LatticeTransferState = TransferState<NoopLattice>;
AST_MATCHER(CXXRecordDecl, expectedClass) { return isExpectedClass(Node); }
+AST_MATCHER(CXXRecordDecl, unexpectedClass) {
+ return Node.getDeclName().isIdentifier() && Node.getName() == "unexpected" &&
+ Node.getDeclContext()->isStdNamespace();
+}
+
auto expectedMemberCall(ast_matchers::internal::Matcher<NamedDecl> Name) {
return cxxMemberCallExpr(
callee(cxxMethodDecl(Name, ofClass(expectedClass()))));
@@ -69,12 +74,34 @@ auto hasExpectedType() {
recordType(hasDeclaration(cxxRecordDecl(expectedClass())))));
}
+auto hasUnexpectedType() {
+ return hasType(hasUnqualifiedDesugaredType(
+ recordType(hasDeclaration(cxxRecordDecl(unexpectedClass())))));
+}
+
auto expectedEqualityCall() {
return cxxOperatorCallExpr(
hasOverloadedOperatorName("=="), argumentCountIs(2),
hasArgument(0, hasExpectedType()), hasArgument(1, hasExpectedType()));
}
+// Reversed comparisons (`v == e`) are rewritten to `e == v`, so the expected is
+// always the first argument.
+auto expectedValueEqualityCall() {
+ return cxxOperatorCallExpr(
+ hasOverloadedOperatorName("=="), argumentCountIs(2),
+ hasArgument(0, hasExpectedType()),
+ hasArgument(1, unless(anyOf(hasExpectedType(), hasUnexpectedType()))));
+}
+
+// Reversed comparisons (`u == e`) are rewritten to `e == u`, so the expected is
+// always the first argument.
+auto expectedUnexpectedEqualityCall() {
+ return cxxOperatorCallExpr(
+ hasOverloadedOperatorName("=="), argumentCountIs(2),
+ hasArgument(0, hasExpectedType()), hasArgument(1, hasUnexpectedType()));
+}
+
/// Ensures that `E` is mapped to a `BoolValue` and returns its formula.
const Formula &forceBoolValue(Environment &Env, const Expr &E) {
auto *Value = Env.get<BoolValue>(E);
@@ -159,6 +186,35 @@ void transferExpectedEqualityCall(const CXXOperatorCallExpr *E,
Env.assume(A.makeImplies(A.makeAnd(LHasValue, RHasValue), EqVal));
}
+void transferExpectedValueEqualityCall(const CXXOperatorCallExpr *E,
+ const MatchFinder::MatchResult &,
+ LatticeTransferState &State) {
+ Environment &Env = State.Env;
+ BoolValue *HasValueVal =
+ getHasValue(Env, Env.get<RecordStorageLocation>(*E->getArg(0)));
+ if (HasValueVal == nullptr)
+ return;
+
+ // `e == v` --> `e.has_value() && *e == v`
+ Env.assume(
+ Env.arena().makeImplies(forceBoolValue(Env, *E), HasValueVal->formula()));
+}
+
+void transferExpectedUnexpectedEqualityCall(const CXXOperatorCallExpr *E,
+ const MatchFinder::MatchResult &,
+ LatticeTransferState &State) {
+ Environment &Env = State.Env;
+ BoolValue *HasValueVal =
+ getHasValue(Env, Env.get<RecordStorageLocation>(*E->getArg(0)));
+ if (HasValueVal == nullptr)
+ return;
+
+ // `e == u` --> `!e.has_value() && e.error() == u.error()`
+ Arena &A = Env.arena();
+ Env.assume(A.makeImplies(forceBoolValue(Env, *E),
+ A.makeNot(HasValueVal->formula())));
+}
+
// FIXME: Model the constructors, assignments and `swap`.
auto buildTransferMatchSwitch() {
return CFGMatchSwitchBuilder<LatticeTransferState>()
@@ -172,6 +228,13 @@ auto buildTransferMatchSwitch() {
// operator== between two expecteds
.CaseOfCFGStmt<CXXOperatorCallExpr>(expectedEqualityCall(),
transferExpectedEqualityCall)
+ // operator== between an expected and a value
+ .CaseOfCFGStmt<CXXOperatorCallExpr>(expectedValueEqualityCall(),
+ transferExpectedValueEqualityCall)
+ // operator== between an expected and an unexpected
+ .CaseOfCFGStmt<CXXOperatorCallExpr>(
+ expectedUnexpectedEqualityCall(),
+ transferExpectedUnexpectedEqualityCall)
.Build();
}
>From 1ac2952027c395232820644cb995ff585fa123da Mon Sep 17 00:00:00 2001
From: BaLiKfromUA <valentin.yukhymenko at gmail.com>
Date: Sat, 26 Sep 2026 23:50:51 +0100
Subject: [PATCH 13/25] Tests for constructors
---
.../std/types/expected.h | 39 ++++-
.../bugprone/unchecked-expected-access.cpp | 138 ++++++++++++++++++
2 files changed, 176 insertions(+), 1 deletion(-)
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-expected-access/std/types/expected.h b/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-expected-access/std/types/expected.h
index d8729b3831bd47..4a1aa990f39b83 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-expected-access/std/types/expected.h
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-expected-access/std/types/expected.h
@@ -3,6 +3,10 @@
/// Mock of `std::expected`.
// TODO: move to checkers/Inputs/Headers/std/expected?
+
+// For `std::in_place_t` and `std::in_place`.
+#include <type_traits>
+
namespace std {
template <class E> class unexpected {
@@ -32,6 +36,22 @@ template <class> constexpr bool __is_expected = false;
template <class T, class E>
constexpr bool __is_expected<expected<T, E>> = true;
+template <class> constexpr bool __is_unexpected = false;
+template <class E> constexpr bool __is_unexpected<unexpected<E>> = true;
+
+// `__remove_cvref` is a clang builtin, so the helper has a different name.
+// clang-format off
+template <class T> struct __remove_cvref_impl { using type = T; };
+template <class T> struct __remove_cvref_impl<const T> { using type = T; };
+template <class T> struct __remove_cvref_impl<T &> { using type = T; };
+template <class T> struct __remove_cvref_impl<const T &> { using type = T; };
+template <class T> struct __remove_cvref_impl<T &&> { using type = T; };
+template <class T> struct __remove_cvref_impl<const T &&> { using type = T; };
+// clang-format on
+
+template <class T>
+using __remove_cvref_t = typename __remove_cvref_impl<T>::type;
+
template <class T, class E> class expected {
public:
using value_type = T;
@@ -42,17 +62,29 @@ template <class T, class E> class expected {
expected(const expected &);
expected(expected &&) noexcept;
- template <class U = T> expected(U &&v);
+ template <class U, class G> expected(const expected<U, G> &x);
+ template <class U, class G> expected(expected<U, G> &&x);
+
+ template <class U = T>
+ requires(!__is_expected<__remove_cvref_t<U>> &&
+ !__is_same(__remove_cvref_t<U>, in_place_t) &&
+ !__is_same(__remove_cvref_t<U>, unexpect_t) &&
+ !__is_unexpected<__remove_cvref_t<U>>)
+ expected(U &&v);
template <class G> expected(const unexpected<G> &e);
template <class G> expected(unexpected<G> &&e);
+ template <class... Args> explicit expected(in_place_t, Args &&...args);
+
template <class... Args>
explicit expected(unexpect_t, Args &&...args);
expected &operator=(const expected &);
expected &operator=(expected &&) noexcept;
+ template <class G> expected &operator=(unexpected<G> &&e);
+
const T *operator->() const noexcept;
T *operator->() noexcept;
@@ -105,9 +137,14 @@ template <class E> class expected<void, E> {
expected(const expected &);
expected(expected &&) noexcept;
+ template <class U, class G> expected(const expected<U, G> &x);
+ template <class U, class G> expected(expected<U, G> &&x);
+
template <class G> expected(const unexpected<G> &e);
template <class G> expected(unexpected<G> &&e);
+ explicit expected(in_place_t) noexcept;
+
template <class... Args>
explicit expected(unexpect_t, Args &&...args);
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp
index efb52561484ed3..a6bdddfedd667d 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp
@@ -1,6 +1,7 @@
// RUN: %check_clang_tidy -std=c++23-or-later %s bugprone-unchecked-expected-access %t -- -- -I %S/Inputs/unchecked-expected-access
#include "std/types/expected.h"
+#include <utility>
struct Foo {
void foo() const {}
@@ -462,3 +463,140 @@ void void_value_access_when_equal_to_unexpected(std::expected<void, int> e) {
e.value();
// CHECK-MESSAGES: :[[@LINE-1]]:7: warning: unchecked access to 'std::expected' value [bugprone-unchecked-expected-access]
}
+
+// Construction.
+
+int default_construct() {
+ std::expected<int, int> e;
+ return *e;
+}
+
+int value_initialize() {
+ std::expected<int, int> e{};
+ return e.value();
+}
+
+int construct_from_value() {
+ std::expected<int, int> e(5);
+ return *e;
+}
+
+int copy_initialize_from_value() {
+ std::expected<int, int> e = 5;
+ return *e;
+}
+
+void construct_in_place() {
+ std::expected<Foo, int> e(std::in_place);
+ e->foo();
+}
+
+void void_default_construct() {
+ std::expected<void, int> e;
+ e.value();
+}
+
+void void_construct_in_place() {
+ std::expected<void, int> e(std::in_place);
+ e.value();
+}
+
+int construct_temporary_from_value() {
+ return std::expected<int, int>(5).value();
+}
+
+int construct_from_unexpected_rvalue() {
+ std::expected<int, int> e(std::unexpected(1));
+ return e.error();
+}
+
+int construct_from_unexpected_lvalue() {
+ const std::unexpected<int> u(1);
+ std::expected<int, int> e(u);
+ return e.error();
+}
+
+int copy_initialize_from_unexpected() {
+ std::expected<int, int> e = std::unexpected(1);
+ return e.error();
+}
+
+int construct_unexpect() {
+ std::expected<int, int> e(std::unexpect, 1);
+ return e.error();
+}
+
+void void_construct_unexpect() {
+ std::expected<void, int> e(std::unexpect, 1);
+ e.error();
+}
+
+int convert_from_checked(std::expected<long, int> b) {
+ if (!b)
+ return 0;
+ std::expected<int, int> a(b);
+ return *a;
+}
+
+int copy_construct_from_checked(std::expected<int, int> b) {
+ if (!b)
+ return 0;
+ auto a = b;
+ return *a;
+}
+
+int move_construct_from_checked(std::expected<int, int> b) {
+ if (!b)
+ return 0;
+ auto a = std::move(b);
+ return *a;
+}
+
+int error_access_after_default_construct() {
+ std::expected<int, int> e;
+ return e.error();
+ // CHECK-MESSAGES: :[[@LINE-1]]:12: warning: unchecked access to 'std::expected' error [bugprone-unchecked-expected-access]
+}
+
+int error_access_after_construct_from_value() {
+ std::expected<int, int> e(5);
+ return e.error();
+ // CHECK-MESSAGES: :[[@LINE-1]]:12: warning: unchecked access to 'std::expected' error [bugprone-unchecked-expected-access]
+}
+
+int value_access_after_construct_from_unexpected() {
+ std::expected<int, int> e(std::unexpected(1));
+ return *e;
+ // CHECK-MESSAGES: :[[@LINE-1]]:10: warning: unchecked access to 'std::expected' value [bugprone-unchecked-expected-access]
+}
+
+int value_access_after_construct_unexpect() {
+ std::expected<int, int> e(std::unexpect, 1);
+ return *e;
+ // CHECK-MESSAGES: :[[@LINE-1]]:10: warning: unchecked access to 'std::expected' value [bugprone-unchecked-expected-access]
+}
+
+void void_value_access_after_construct_unexpect() {
+ std::expected<void, int> e(std::unexpect, 1);
+ e.value();
+ // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: unchecked access to 'std::expected' value [bugprone-unchecked-expected-access]
+}
+
+int convert_from_unchecked(std::expected<long, int> b) {
+ std::expected<int, int> a(b);
+ return *a;
+ // CHECK-MESSAGES: :[[@LINE-1]]:10: warning: unchecked access to 'std::expected' value [bugprone-unchecked-expected-access]
+}
+
+int copy_construct_from_unchecked(std::expected<int, int> b) {
+ auto a = b;
+ return *a;
+ // CHECK-MESSAGES: :[[@LINE-1]]:10: warning: unchecked access to 'std::expected' value [bugprone-unchecked-expected-access]
+}
+
+// FIXME: should warn once converting assignments are modeled.
+int default_construct_then_assign_unexpected() {
+ std::expected<int, int> e;
+ e = std::unexpected(1);
+ return *e;
+}
>From f1a284b5106de1260f83acdddf50a152d622f055 Mon Sep 17 00:00:00 2001
From: BaLiKfromUA <valentin.yukhymenko at gmail.com>
Date: Sun, 11 Oct 2026 12:58:33 +0100
Subject: [PATCH 14/25] Test for expected inside expected
---
.../std/types/expected.h | 24 ++++++++--
.../bugprone/unchecked-expected-access.cpp | 48 +++++++++++++++++++
2 files changed, 68 insertions(+), 4 deletions(-)
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-expected-access/std/types/expected.h b/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-expected-access/std/types/expected.h
index 4a1aa990f39b83..d96e0a57119716 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-expected-access/std/types/expected.h
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-expected-access/std/types/expected.h
@@ -52,6 +52,15 @@ template <class T> struct __remove_cvref_impl<const T &&> { using type = T; };
template <class T>
using __remove_cvref_t = typename __remove_cvref_impl<T>::type;
+// Whether `T` can be constructed from `expected<U, G>`, in which case the
+// converting constructors from `expected<U, G>` are disabled.
+template <class T, class U, class G>
+constexpr bool __cons_from_expected =
+ __is_constructible(T, expected<U, G> &) ||
+ __is_constructible(T, expected<U, G>) ||
+ __is_constructible(T, const expected<U, G> &) ||
+ __is_constructible(T, const expected<U, G>);
+
template <class T, class E> class expected {
public:
using value_type = T;
@@ -62,14 +71,21 @@ template <class T, class E> class expected {
expected(const expected &);
expected(expected &&) noexcept;
- template <class U, class G> expected(const expected<U, G> &x);
- template <class U, class G> expected(expected<U, G> &&x);
+ template <class U, class G>
+ requires(__is_constructible(T, const U &) &&
+ __is_constructible(E, const G &) &&
+ !__cons_from_expected<T, U, G>)
+ expected(const expected<U, G> &x);
+ template <class U, class G>
+ requires(__is_constructible(T, U) && __is_constructible(E, G) &&
+ !__cons_from_expected<T, U, G>)
+ expected(expected<U, G> &&x);
template <class U = T>
- requires(!__is_expected<__remove_cvref_t<U>> &&
+ requires(!__is_same(__remove_cvref_t<U>, expected) &&
!__is_same(__remove_cvref_t<U>, in_place_t) &&
!__is_same(__remove_cvref_t<U>, unexpect_t) &&
- !__is_unexpected<__remove_cvref_t<U>>)
+ !__is_unexpected<__remove_cvref_t<U>> && __is_constructible(T, U))
expected(U &&v);
template <class G> expected(const unexpected<G> &e);
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp
index a6bdddfedd667d..61217a46bdd815 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp
@@ -552,6 +552,40 @@ int move_construct_from_checked(std::expected<int, int> b) {
return *a;
}
+void construct_nested_from_inner_with_error() {
+ std::expected<int, int> inner(std::unexpect, 1);
+ std::expected<std::expected<int, int>, int> outer(inner);
+ outer.value();
+}
+
+void construct_nested_from_inner_rvalue_with_error() {
+ std::expected<std::expected<int, int>, int> outer(
+ std::expected<int, int>(std::unexpect, 1));
+ outer.value();
+}
+
+void construct_nested_from_unchecked_inner(std::expected<int, int> inner) {
+ std::expected<std::expected<int, int>, int> outer(inner);
+ outer.value();
+}
+
+void convert_nested_from_checked(
+ std::expected<std::expected<long, int>, int> b) {
+ if (!b)
+ return;
+ std::expected<std::expected<int, int>, int> a(b);
+ a.value();
+}
+
+struct FromExpected {
+ FromExpected(std::expected<int, int>);
+};
+
+void construct_from_expected_as_value(std::expected<int, int> b) {
+ std::expected<FromExpected, int> a(b);
+ a.value();
+}
+
int error_access_after_default_construct() {
std::expected<int, int> e;
return e.error();
@@ -594,6 +628,20 @@ int copy_construct_from_unchecked(std::expected<int, int> b) {
// CHECK-MESSAGES: :[[@LINE-1]]:10: warning: unchecked access to 'std::expected' value [bugprone-unchecked-expected-access]
}
+void error_access_after_construct_nested_from_inner_with_error() {
+ std::expected<int, int> inner(std::unexpect, 1);
+ std::expected<std::expected<int, int>, int> outer(inner);
+ outer.error();
+ // CHECK-MESSAGES: :[[@LINE-1]]:9: warning: unchecked access to 'std::expected' error [bugprone-unchecked-expected-access]
+}
+
+void convert_nested_from_unchecked(
+ std::expected<std::expected<long, int>, int> b) {
+ std::expected<std::expected<int, int>, int> a(b);
+ a.value();
+ // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: unchecked access to 'std::expected' value [bugprone-unchecked-expected-access]
+}
+
// FIXME: should warn once converting assignments are modeled.
int default_construct_then_assign_unexpected() {
std::expected<int, int> e;
>From c2f5b76e16383d12172af1c4532d76f0f1825c8d Mon Sep 17 00:00:00 2001
From: BaLiKfromUA <valentin.yukhymenko at gmail.com>
Date: Sun, 11 Oct 2026 13:04:51 +0100
Subject: [PATCH 15/25] model constructors of std::expected
---
.../bugprone/unchecked-expected-access.md | 11 ++-
.../Models/UncheckedExpectedAccessModel.cpp | 82 ++++++++++++++++++-
2 files changed, 89 insertions(+), 4 deletions(-)
diff --git a/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md b/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md
index d4175ea28860bd..4fca4dfdc81532 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md
+++ b/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md
@@ -93,10 +93,15 @@ void m(std::expected<int, Error> e) {
report(e.error()); // only an expected holding an error compares equal
// to an unexpected.
}
+
+int n() {
+ std::expected<int, Error> e; // a default-constructed expected holds a value.
+ return *e;
+}
```
## Limitations
-The check does not yet model the constructors, assignment operators or
-`swap` of `std::expected`, so it does not know, for example, that a
-default-constructed `std::expected` holds a value.
+The check does not yet model the assignment operators (other than copy and
+move assignment) or `swap` of `std::expected`. For example, it does not know
+that `e = std::unexpected(err)` leaves `e` holding an error.
diff --git a/clang/lib/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.cpp b/clang/lib/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.cpp
index 4b299595befdc0..232bb1201b51e2 100644
--- a/clang/lib/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.cpp
+++ b/clang/lib/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.cpp
@@ -102,6 +102,52 @@ auto expectedUnexpectedEqualityCall() {
hasArgument(0, hasExpectedType()), hasArgument(1, hasUnexpectedType()));
}
+// Copy and move constructors are modeled by the framework, which copies the
+// "has_value" property of the source.
+auto expectedConstructor() {
+ return cxxConstructExpr(hasDeclaration(cxxConstructorDecl(
+ ofClass(expectedClass()),
+ unless(anyOf(isCopyConstructor(), isMoveConstructor())))));
+}
+
+// `expected(unexpect_t, ...)`, `expected(const unexpected<G> &)` and
+// `expected(unexpected<G> &&)`.
+auto expectedErrorConstructor() {
+ return cxxConstructExpr(
+ expectedConstructor(),
+ hasArgument(0, anyOf(hasType(namedDecl(hasName("std::unexpect_t"))),
+ hasUnexpectedType())));
+}
+
+// Matches `template <class U, class G> expected(const expected<U, G> &)` and
+// `template <class U, class G> expected(expected<U, G> &&)`. The argument type
+// alone is not enough to recognize them: `expected(U &&)` also accepts an
+// `expected` when `T` is constructible from it, e.g. for nested `expected`s.
+AST_MATCHER(CXXConstructorDecl, expectedConvertingConstructorDecl) {
+ const FunctionTemplateDecl *Template = Node.getPrimaryTemplate();
+ if (Template == nullptr)
+ return false;
+ const FunctionDecl *Pattern = Template->getTemplatedDecl();
+ if (Pattern->getNumParams() != 1)
+ return false;
+ const auto *ParamType = Pattern->getParamDecl(0)
+ ->getType()
+ .getNonReferenceType()
+ ->getAs<TemplateSpecializationType>();
+ if (ParamType == nullptr)
+ return false;
+ const auto *ParamTemplate = dyn_cast_or_null<ClassTemplateDecl>(
+ ParamType->getTemplateName().getAsTemplateDecl());
+ return ParamTemplate != nullptr &&
+ isExpectedClass(*ParamTemplate->getTemplatedDecl());
+}
+
+auto expectedConvertingConstructor() {
+ return cxxConstructExpr(
+ expectedConstructor(),
+ hasDeclaration(cxxConstructorDecl(expectedConvertingConstructorDecl())));
+}
+
/// Ensures that `E` is mapped to a `BoolValue` and returns its formula.
const Formula &forceBoolValue(Environment &Env, const Expr &E) {
auto *Value = Env.get<BoolValue>(E);
@@ -215,7 +261,33 @@ void transferExpectedUnexpectedEqualityCall(const CXXOperatorCallExpr *E,
A.makeNot(HasValueVal->formula())));
}
-// FIXME: Model the constructors, assignments and `swap`.
+void constructExpected(const CXXConstructExpr &E, BoolValue &HasValueVal,
+ Environment &Env) {
+ setHasValue(&Env.getResultObjectLocation(E), HasValueVal, Env);
+}
+
+void transferValueConstructor(const CXXConstructExpr *E,
+ const MatchFinder::MatchResult &,
+ LatticeTransferState &State) {
+ constructExpected(*E, State.Env.getBoolLiteralValue(true), State.Env);
+}
+
+void transferErrorConstructor(const CXXConstructExpr *E,
+ const MatchFinder::MatchResult &,
+ LatticeTransferState &State) {
+ constructExpected(*E, State.Env.getBoolLiteralValue(false), State.Env);
+}
+
+void transferConvertingConstructor(const CXXConstructExpr *E,
+ const MatchFinder::MatchResult &,
+ LatticeTransferState &State) {
+ // The new expected holds a value if and only if the source does.
+ if (BoolValue *HasValueVal = getHasValue(
+ State.Env, State.Env.get<RecordStorageLocation>(*E->getArg(0))))
+ constructExpected(*E, *HasValueVal, State.Env);
+}
+
+// FIXME: Model the assignments and `swap`.
auto buildTransferMatchSwitch() {
return CFGMatchSwitchBuilder<LatticeTransferState>()
// expected::has_value, expected::operator bool
@@ -235,6 +307,14 @@ auto buildTransferMatchSwitch() {
.CaseOfCFGStmt<CXXOperatorCallExpr>(
expectedUnexpectedEqualityCall(),
transferExpectedUnexpectedEqualityCall)
+ // Constructors. The order matters: the last case matches every
+ // constructor that is not matched by the ones before it.
+ .CaseOfCFGStmt<CXXConstructExpr>(expectedErrorConstructor(),
+ transferErrorConstructor)
+ .CaseOfCFGStmt<CXXConstructExpr>(expectedConvertingConstructor(),
+ transferConvertingConstructor)
+ .CaseOfCFGStmt<CXXConstructExpr>(expectedConstructor(),
+ transferValueConstructor)
.Build();
}
>From 1553c1986b22630483826ee5c6e9b0bb0be5e205 Mon Sep 17 00:00:00 2001
From: BaLiKfromUA <valentin.yukhymenko at gmail.com>
Date: Sun, 11 Oct 2026 13:14:46 +0100
Subject: [PATCH 16/25] Add tests for assignments
---
.../std/types/expected.h | 10 ++
.../bugprone/unchecked-expected-access.cpp | 124 +++++++++++++++++-
2 files changed, 133 insertions(+), 1 deletion(-)
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-expected-access/std/types/expected.h b/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-expected-access/std/types/expected.h
index d96e0a57119716..fe9ee804d7cb94 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-expected-access/std/types/expected.h
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-expected-access/std/types/expected.h
@@ -99,6 +99,13 @@ template <class T, class E> class expected {
expected &operator=(const expected &);
expected &operator=(expected &&) noexcept;
+ template <class U = T>
+ requires(!__is_same(__remove_cvref_t<U>, expected) &&
+ !__is_unexpected<__remove_cvref_t<U>> &&
+ __is_constructible(T, U) && __is_assignable(T &, U))
+ expected &operator=(U &&v);
+
+ template <class G> expected &operator=(const unexpected<G> &e);
template <class G> expected &operator=(unexpected<G> &&e);
const T *operator->() const noexcept;
@@ -167,6 +174,9 @@ template <class E> class expected<void, E> {
expected &operator=(const expected &);
expected &operator=(expected &&) noexcept;
+ template <class G> expected &operator=(const unexpected<G> &e);
+ template <class G> expected &operator=(unexpected<G> &&e);
+
explicit operator bool() const noexcept;
bool has_value() const noexcept;
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp
index 61217a46bdd815..f71c362dee42b6 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp
@@ -642,9 +642,131 @@ void convert_nested_from_unchecked(
// CHECK-MESSAGES: :[[@LINE-1]]:5: warning: unchecked access to 'std::expected' value [bugprone-unchecked-expected-access]
}
-// FIXME: should warn once converting assignments are modeled.
int default_construct_then_assign_unexpected() {
std::expected<int, int> e;
e = std::unexpected(1);
return *e;
+ // CHECK-MESSAGES: :[[@LINE-1]]:10: warning: unchecked access to 'std::expected' value [bugprone-unchecked-expected-access]
+}
+
+// Assignment.
+
+int assign_value(std::expected<int, int> e) {
+ e = 5;
+ return *e;
+}
+
+int assign_braces(std::expected<int, int> e) {
+ e = {};
+ return *e;
+}
+
+int assign_value_after_error(std::expected<int, int> e) {
+ if (e)
+ return 0;
+ e = 5;
+ return *e;
+}
+
+int assign_unexpected_lvalue(std::expected<int, int> e) {
+ const std::unexpected<int> u(1);
+ e = u;
+ return e.error();
+}
+
+int assign_unexpected_rvalue(std::expected<int, int> e) {
+ e = std::unexpected(1);
+ return e.error();
+}
+
+int chained_assign_value(std::expected<int, int> e) {
+ return (e = 5).value();
+}
+
+void void_assign_unexpected(std::expected<void, int> e) {
+ e = std::unexpected(1);
+ e.error();
+}
+
+int copy_assign_from_checked(std::expected<int, int> a,
+ std::expected<int, int> b) {
+ if (!b)
+ return 0;
+ a = b;
+ return *a;
+}
+
+int move_assign_from_checked(std::expected<int, int> a,
+ std::expected<int, int> b) {
+ if (!b)
+ return 0;
+ a = std::move(b);
+ return *a;
+}
+
+int assign_from_converted_checked(std::expected<int, int> a,
+ std::expected<long, int> b) {
+ if (!b)
+ return 0;
+ a = b;
+ return *a;
+}
+
+void assign_nested_from_inner_with_error(
+ std::expected<std::expected<int, int>, int> outer) {
+ std::expected<int, int> inner(std::unexpect, 1);
+ outer = inner;
+ outer.value();
+}
+
+int value_access_after_assign_unexpected(std::expected<int, int> e) {
+ e = std::unexpected(1);
+ return *e;
+ // CHECK-MESSAGES: :[[@LINE-1]]:10: warning: unchecked access to 'std::expected' value [bugprone-unchecked-expected-access]
+}
+
+int error_access_after_assign_value(std::expected<int, int> e) {
+ e = 5;
+ return e.error();
+ // CHECK-MESSAGES: :[[@LINE-1]]:12: warning: unchecked access to 'std::expected' error [bugprone-unchecked-expected-access]
+}
+
+int assign_value_on_one_path(std::expected<int, int> e, bool cond) {
+ if (cond)
+ e = 5;
+ return *e;
+ // CHECK-MESSAGES: :[[@LINE-1]]:10: warning: unchecked access to 'std::expected' value [bugprone-unchecked-expected-access]
+}
+
+int chained_assign_unexpected(std::expected<int, int> e) {
+ return (e = std::unexpected(1)).value();
+ // CHECK-MESSAGES: :[[@LINE-1]]:35: warning: unchecked access to 'std::expected' value [bugprone-unchecked-expected-access]
+}
+
+void void_value_access_after_assign_unexpected(std::expected<void, int> e) {
+ e = std::unexpected(1);
+ e.value();
+ // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: unchecked access to 'std::expected' value [bugprone-unchecked-expected-access]
+}
+
+int copy_assign_from_unchecked(std::expected<int, int> a,
+ std::expected<int, int> b) {
+ a = b;
+ return *a;
+ // CHECK-MESSAGES: :[[@LINE-1]]:10: warning: unchecked access to 'std::expected' value [bugprone-unchecked-expected-access]
+}
+
+int assign_from_converted_unchecked(std::expected<int, int> a,
+ std::expected<long, int> b) {
+ a = b;
+ return *a;
+ // CHECK-MESSAGES: :[[@LINE-1]]:10: warning: unchecked access to 'std::expected' value [bugprone-unchecked-expected-access]
+}
+
+void error_access_after_assign_nested_from_inner_with_error(
+ std::expected<std::expected<int, int>, int> outer) {
+ std::expected<int, int> inner(std::unexpect, 1);
+ outer = inner;
+ outer.error();
+ // CHECK-MESSAGES: :[[@LINE-1]]:9: warning: unchecked access to 'std::expected' error [bugprone-unchecked-expected-access]
}
>From 19164975e8121579a3da9bf30fc811d67bac8fe8 Mon Sep 17 00:00:00 2001
From: BaLiKfromUA <valentin.yukhymenko at gmail.com>
Date: Sun, 11 Oct 2026 13:32:29 +0100
Subject: [PATCH 17/25] Model assignments
---
.../bugprone/unchecked-expected-access.md | 10 ++--
.../Models/UncheckedExpectedAccessModel.cpp | 46 ++++++++++++++++++-
2 files changed, 52 insertions(+), 4 deletions(-)
diff --git a/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md b/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md
index 4fca4dfdc81532..cb2f5feab7bcd9 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md
+++ b/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md
@@ -98,10 +98,14 @@ int n() {
std::expected<int, Error> e; // a default-constructed expected holds a value.
return *e;
}
+
+void o(std::expected<int, Error> e) {
+ e = std::unexpected(Error::NotFound);
+ report(e.error());
+}
```
## Limitations
-The check does not yet model the assignment operators (other than copy and
-move assignment) or `swap` of `std::expected`. For example, it does not know
-that `e = std::unexpected(err)` leaves `e` holding an error.
+The check does not yet model `swap` of `std::expected`. For example, it does
+not know that after `a.swap(b)`, `a` holds a value if `b` held one before.
diff --git a/clang/lib/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.cpp b/clang/lib/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.cpp
index 232bb1201b51e2..8258619a11269d 100644
--- a/clang/lib/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.cpp
+++ b/clang/lib/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.cpp
@@ -148,6 +148,22 @@ auto expectedConvertingConstructor() {
hasDeclaration(cxxConstructorDecl(expectedConvertingConstructorDecl())));
}
+// Copy and move assignments are modeled by the framework, which copies the
+// "has_value" property of the source.
+auto expectedAssignment() {
+ return cxxOperatorCallExpr(
+ hasOverloadedOperatorName("="), argumentCountIs(2),
+ callee(cxxMethodDecl(ofClass(expectedClass()),
+ unless(anyOf(isCopyAssignmentOperator(),
+ isMoveAssignmentOperator())))));
+}
+
+// `operator=(const unexpected<G> &)` and `operator=(unexpected<G> &&)`.
+auto expectedErrorAssignment() {
+ return cxxOperatorCallExpr(expectedAssignment(),
+ hasArgument(1, hasUnexpectedType()));
+}
+
/// Ensures that `E` is mapped to a `BoolValue` and returns its formula.
const Formula &forceBoolValue(Environment &Env, const Expr &E) {
auto *Value = Env.get<BoolValue>(E);
@@ -287,7 +303,29 @@ void transferConvertingConstructor(const CXXConstructExpr *E,
constructExpected(*E, *HasValueVal, State.Env);
}
-// FIXME: Model the assignments and `swap`.
+void assignExpected(const CXXOperatorCallExpr &E, BoolValue &HasValueVal,
+ Environment &Env) {
+ auto *Loc = Env.get<RecordStorageLocation>(*E.getArg(0));
+ if (Loc == nullptr)
+ return;
+ setHasValue(Loc, HasValueVal, Env);
+ // The result of the assignment refers to the assigned object.
+ Env.setStorageLocation(E, *Loc);
+}
+
+void transferValueAssignment(const CXXOperatorCallExpr *E,
+ const MatchFinder::MatchResult &,
+ LatticeTransferState &State) {
+ assignExpected(*E, State.Env.getBoolLiteralValue(true), State.Env);
+}
+
+void transferErrorAssignment(const CXXOperatorCallExpr *E,
+ const MatchFinder::MatchResult &,
+ LatticeTransferState &State) {
+ assignExpected(*E, State.Env.getBoolLiteralValue(false), State.Env);
+}
+
+// FIXME: Model `swap`.
auto buildTransferMatchSwitch() {
return CFGMatchSwitchBuilder<LatticeTransferState>()
// expected::has_value, expected::operator bool
@@ -315,6 +353,12 @@ auto buildTransferMatchSwitch() {
transferConvertingConstructor)
.CaseOfCFGStmt<CXXConstructExpr>(expectedConstructor(),
transferValueConstructor)
+ // Assignments. The order matters: the last case matches every
+ // assignment that is not matched by the one before it.
+ .CaseOfCFGStmt<CXXOperatorCallExpr>(expectedErrorAssignment(),
+ transferErrorAssignment)
+ .CaseOfCFGStmt<CXXOperatorCallExpr>(expectedAssignment(),
+ transferValueAssignment)
.Build();
}
>From 0588b31c4afda89817a5236e0ae391517773f7eb Mon Sep 17 00:00:00 2001
From: BaLiKfromUA <valentin.yukhymenko at gmail.com>
Date: Sun, 11 Oct 2026 15:44:15 +0100
Subject: [PATCH 18/25] Add more limitations
---
.../bugprone/unchecked-expected-access.md | 27 +++++++++++++++++--
1 file changed, 25 insertions(+), 2 deletions(-)
diff --git a/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md b/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md
index cb2f5feab7bcd9..2b8a8daa90b211 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md
+++ b/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md
@@ -107,5 +107,28 @@ void o(std::expected<int, Error> e) {
## Limitations
-The check does not yet model `swap` of `std::expected`. For example, it does
-not know that after `a.swap(b)`, `a` holds a value if `b` held one before.
+The check does not yet model some member functions of `std::expected`. These
+are known, temporary limitations:
+
+- `swap`. For example, the check does not know that after `a.swap(b)`, `a`
+ holds a value if `b` held one before.
+- The monadic operations `and_then`, `or_else`, `transform` and
+ `transform_error`. The check does not relate the state of their result to the
+ state of the object they are called on, so accessing the result without a
+ check of its own is diagnosed, even when it is safe:
+
+```cpp
+int f(std::expected<int, Error> e) {
+ if (!e)
+ return 0;
+ auto r = e.transform([](int v) { return v + 1; });
+ return *r; // false positive: `r` holds a value because `e` does.
+}
+
+void g(std::expected<int, Error> e) {
+ if (!e)
+ report(e.transform_error(describe).error()); // false positive: the result
+ // holds an error because `e`
+ // does.
+}
+```
>From 22b6f5fe706cb6d6826a0c142bc258290173134e Mon Sep 17 00:00:00 2001
From: BaLiKfromUA <valentin.yukhymenko at gmail.com>
Date: Sun, 11 Oct 2026 15:56:59 +0100
Subject: [PATCH 19/25] Tests for `transform` and `transform_error`
---
.../std/types/expected.h | 63 ++++++++++
.../bugprone/unchecked-expected-access.cpp | 110 ++++++++++++++++++
2 files changed, 173 insertions(+)
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-expected-access/std/types/expected.h b/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-expected-access/std/types/expected.h
index fe9ee804d7cb94..64cf63ba2a07c0 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-expected-access/std/types/expected.h
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-expected-access/std/types/expected.h
@@ -52,6 +52,12 @@ template <class T> struct __remove_cvref_impl<const T &&> { using type = T; };
template <class T>
using __remove_cvref_t = typename __remove_cvref_impl<T>::type;
+// `__remove_cv` is a clang builtin as well.
+template <class T> struct __remove_cv_impl { using type = T; };
+template <class T> struct __remove_cv_impl<const T> { using type = T; };
+
+template <class T> using __remove_cv_t = typename __remove_cv_impl<T>::type;
+
// Whether `T` can be constructed from `expected<U, G>`, in which case the
// converting constructors from `expected<U, G>` are disabled.
template <class T, class U, class G>
@@ -135,6 +141,34 @@ template <class T, class E> class expected {
template <class G> E error_or(G &&e) const &;
template <class G> E error_or(G &&e) &&;
+ // Defined, so that they can be called with lambdas, whose types have no
+ // linkage.
+ template <class F>
+ expected<__remove_cv_t<decltype(declval<F>()(declval<T &>()))>, E>
+ transform(F &&) & { return {}; }
+ template <class F>
+ expected<__remove_cv_t<decltype(declval<F>()(declval<const T &>()))>, E>
+ transform(F &&) const & { return {}; }
+ template <class F>
+ expected<__remove_cv_t<decltype(declval<F>()(declval<T>()))>, E>
+ transform(F &&) && { return {}; }
+ template <class F>
+ expected<__remove_cv_t<decltype(declval<F>()(declval<const T>()))>, E>
+ transform(F &&) const && { return {}; }
+
+ template <class F>
+ expected<T, __remove_cv_t<decltype(declval<F>()(declval<E &>()))>>
+ transform_error(F &&) & { return {}; }
+ template <class F>
+ expected<T, __remove_cv_t<decltype(declval<F>()(declval<const E &>()))>>
+ transform_error(F &&) const & { return {}; }
+ template <class F>
+ expected<T, __remove_cv_t<decltype(declval<F>()(declval<E>()))>>
+ transform_error(F &&) && { return {}; }
+ template <class F>
+ expected<T, __remove_cv_t<decltype(declval<F>()(declval<const E>()))>>
+ transform_error(F &&) const && { return {}; }
+
template <class... Args> T &emplace(Args &&...args) noexcept;
void swap(expected &rhs) noexcept;
@@ -193,6 +227,35 @@ template <class E> class expected<void, E> {
template <class G> E error_or(G &&e) const &;
template <class G> E error_or(G &&e) &&;
+ // Defined, so that they can be called with lambdas, whose types have no
+ // linkage.
+ template <class F>
+ expected<__remove_cv_t<decltype(declval<F>()())>, E>
+ transform(F &&) & { return {}; }
+ template <class F>
+ expected<__remove_cv_t<decltype(declval<F>()())>, E>
+ transform(F &&) const & { return {}; }
+ template <class F>
+ expected<__remove_cv_t<decltype(declval<F>()())>, E>
+ transform(F &&) && { return {}; }
+ template <class F>
+ expected<__remove_cv_t<decltype(declval<F>()())>, E>
+ transform(F &&) const && { return {}; }
+
+ template <class F>
+ expected<void, __remove_cv_t<decltype(declval<F>()(declval<E &>()))>>
+ transform_error(F &&) & { return {}; }
+ template <class F>
+ expected<void,
+ __remove_cv_t<decltype(declval<F>()(declval<const E &>()))>>
+ transform_error(F &&) const & { return {}; }
+ template <class F>
+ expected<void, __remove_cv_t<decltype(declval<F>()(declval<E>()))>>
+ transform_error(F &&) && { return {}; }
+ template <class F>
+ expected<void, __remove_cv_t<decltype(declval<F>()(declval<const E>()))>>
+ transform_error(F &&) const && { return {}; }
+
void emplace() noexcept;
void swap(expected &rhs) noexcept;
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp
index f71c362dee42b6..a8451aff8b53b8 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access.cpp
@@ -770,3 +770,113 @@ void error_access_after_assign_nested_from_inner_with_error(
outer.error();
// CHECK-MESSAGES: :[[@LINE-1]]:9: warning: unchecked access to 'std::expected' error [bugprone-unchecked-expected-access]
}
+
+// Monadic operations.
+
+constexpr auto add_one = [](int v) { return v + 1; };
+
+int transform_value_access(std::expected<int, int> e) {
+ if (!e)
+ return 0;
+ auto r = e.transform(add_one);
+ return *r;
+}
+
+int transform_temporary_value_access(std::expected<int, int> e) {
+ if (!e)
+ return 0;
+ return e.transform(add_one).value();
+}
+
+int transform_rvalue(std::expected<int, int> e) {
+ if (!e)
+ return 0;
+ auto r = std::move(e).transform(add_one);
+ return *r;
+}
+
+int transform_const(const std::expected<int, int> &e) {
+ if (!e)
+ return 0;
+ return *e.transform(add_one);
+}
+
+int transform_error_access_when_source_has_error(std::expected<int, int> e) {
+ if (e)
+ return 0;
+ return e.transform(add_one).error();
+}
+
+int transform_error_error_access(std::expected<int, int> e) {
+ if (e)
+ return 0;
+ auto r = e.transform_error(add_one);
+ return r.error();
+}
+
+int transform_error_value_access(std::expected<int, int> e) {
+ if (!e)
+ return 0;
+ return *e.transform_error(add_one);
+}
+
+int chained_transform(std::expected<int, int> e) {
+ if (!e)
+ return 0;
+ return *e.transform(add_one).transform(add_one);
+}
+
+void transform_to_void(std::expected<int, int> e) {
+ if (!e)
+ return;
+ e.transform([](int) {}).value();
+}
+
+int transform_result_keeps_state(std::expected<int, int> e) {
+ if (!e)
+ return 0;
+ auto r = e.transform(add_one);
+ e = std::unexpected(1);
+ return *r;
+}
+
+int void_transform(std::expected<void, int> e) {
+ if (!e)
+ return 0;
+ return *e.transform([] { return 1; });
+}
+
+int void_transform_error(std::expected<void, int> e) {
+ if (e)
+ return 0;
+ return e.transform_error(add_one).error();
+}
+
+int transform_unchecked(std::expected<int, int> e) {
+ return *e.transform(add_one);
+ // CHECK-MESSAGES: :[[@LINE-1]]:10: warning: unchecked access to 'std::expected' value [bugprone-unchecked-expected-access]
+}
+
+int value_access_after_transform_of_error(std::expected<int, int> e) {
+ if (e)
+ return 0;
+ return *e.transform(add_one);
+ // CHECK-MESSAGES: :[[@LINE-1]]:10: warning: unchecked access to 'std::expected' value [bugprone-unchecked-expected-access]
+}
+
+int transform_error_unchecked(std::expected<int, int> e) {
+ return e.transform_error(add_one).error();
+ // CHECK-MESSAGES: :[[@LINE-1]]:37: warning: unchecked access to 'std::expected' error [bugprone-unchecked-expected-access]
+}
+
+int error_access_after_transform_error_of_value(std::expected<int, int> e) {
+ if (!e)
+ return 0;
+ return e.transform_error(add_one).error();
+ // CHECK-MESSAGES: :[[@LINE-1]]:37: warning: unchecked access to 'std::expected' error [bugprone-unchecked-expected-access]
+}
+
+int void_transform_unchecked(std::expected<void, int> e) {
+ return *e.transform([] { return 1; });
+ // CHECK-MESSAGES: :[[@LINE-1]]:10: warning: unchecked access to 'std::expected' value [bugprone-unchecked-expected-access]
+}
>From a9688c136443ef9365569e1f79fb3a79b49421ab Mon Sep 17 00:00:00 2001
From: BaLiKfromUA <valentin.yukhymenko at gmail.com>
Date: Sun, 11 Oct 2026 16:04:49 +0100
Subject: [PATCH 20/25] Model `transform` and `transform_error`
---
.../bugprone/unchecked-expected-access.md | 29 +++++++++++--------
.../Models/UncheckedExpectedAccessModel.cpp | 15 ++++++++++
2 files changed, 32 insertions(+), 12 deletions(-)
diff --git a/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md b/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md
index 2b8a8daa90b211..f8187bd68f5d6c 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md
+++ b/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md
@@ -103,6 +103,13 @@ void o(std::expected<int, Error> e) {
e = std::unexpected(Error::NotFound);
report(e.error());
}
+
+int p(std::expected<int, Error> e) {
+ if (!e)
+ return 0;
+ // `transform` and `transform_error` hold a value if and only if `e` does.
+ return *e.transform([](int v) { return v + 1; });
+}
```
## Limitations
@@ -112,23 +119,21 @@ are known, temporary limitations:
- `swap`. For example, the check does not know that after `a.swap(b)`, `a`
holds a value if `b` held one before.
-- The monadic operations `and_then`, `or_else`, `transform` and
- `transform_error`. The check does not relate the state of their result to the
- state of the object they are called on, so accessing the result without a
- check of its own is diagnosed, even when it is safe:
+- The monadic operations `and_then` and `or_else`. The check does not use the
+ fact that `and_then` keeps an error and `or_else` keeps a value, so accessing
+ their result without a check of its own is diagnosed, even when it is safe:
```cpp
-int f(std::expected<int, Error> e) {
+void f(std::expected<int, Error> e) {
if (!e)
- return 0;
- auto r = e.transform([](int v) { return v + 1; });
- return *r; // false positive: `r` holds a value because `e` does.
+ report(e.and_then(validate).error()); // false positive: the result holds
+ // an error because `e` does.
}
-void g(std::expected<int, Error> e) {
+int g(std::expected<int, Error> e) {
if (!e)
- report(e.transform_error(describe).error()); // false positive: the result
- // holds an error because `e`
- // does.
+ return 0;
+ return *e.or_else(recover); // false positive: the result holds a value
+ // because `e` does.
}
```
diff --git a/clang/lib/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.cpp b/clang/lib/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.cpp
index 8258619a11269d..8b63c5f5079146 100644
--- a/clang/lib/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.cpp
+++ b/clang/lib/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.cpp
@@ -221,6 +221,17 @@ void transferEmplaceCall(const CXXMemberCallExpr *E,
State.Env.getBoolLiteralValue(true), State.Env);
}
+void transferTransformCall(const CXXMemberCallExpr *E,
+ const MatchFinder::MatchResult &,
+ LatticeTransferState &State) {
+ // `transform` maps the value and `transform_error` maps the error, so the
+ // result holds a value if and only if the source does.
+ if (BoolValue *HasValueVal =
+ getHasValue(State.Env, getImplicitObjectLocation(*E, State.Env)))
+ setHasValue(&State.Env.getResultObjectLocation(*E), *HasValueVal,
+ State.Env);
+}
+
// `x != y` is rewritten to `!(x == y)`, so it needs no separate handling.
void transferExpectedEqualityCall(const CXXOperatorCallExpr *E,
const MatchFinder::MatchResult &,
@@ -335,6 +346,10 @@ auto buildTransferMatchSwitch() {
// expected::emplace
.CaseOfCFGStmt<CXXMemberCallExpr>(expectedMemberCall(hasName("emplace")),
transferEmplaceCall)
+ // expected::transform, expected::transform_error
+ .CaseOfCFGStmt<CXXMemberCallExpr>(
+ expectedMemberCall(hasAnyName("transform", "transform_error")),
+ transferTransformCall)
// operator== between two expecteds
.CaseOfCFGStmt<CXXOperatorCallExpr>(expectedEqualityCall(),
transferExpectedEqualityCall)
>From 5aa4697786e34f13497ed1223c52f9bfc5a0088e Mon Sep 17 00:00:00 2001
From: BaLiKfromUA <valentin.yukhymenko at gmail.com>
Date: Sat, 17 Oct 2026 11:40:52 +0100
Subject: [PATCH 21/25] put note about Google test
---
.../clang-tidy/checks/bugprone/unchecked-expected-access.md | 3 +++
1 file changed, 3 insertions(+)
diff --git a/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md b/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md
index f8187bd68f5d6c..7154cd10ade789 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md
+++ b/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md
@@ -137,3 +137,6 @@ int g(std::expected<int, Error> e) {
// because `e` does.
}
```
+
+GoogleTest assertions such as `ASSERT_TRUE(e.has_value())` are not recognized
+yet; support for them is planned.
>From 2665a2ff09c17e9b8afd2bc0c506e3bbdcc8561c Mon Sep 17 00:00:00 2001
From: BaLiKfromUA <valentin.yukhymenko at gmail.com>
Date: Sat, 17 Oct 2026 12:17:19 +0100
Subject: [PATCH 22/25] Add todo
---
.../clang-tidy/bugprone/UncheckedExpectedAccessCheck.h | 2 ++
1 file changed, 2 insertions(+)
diff --git a/clang-tools-extra/clang-tidy/bugprone/UncheckedExpectedAccessCheck.h b/clang-tools-extra/clang-tidy/bugprone/UncheckedExpectedAccessCheck.h
index b64362ba7b9d0b..2df3367b7bc4dc 100644
--- a/clang-tools-extra/clang-tidy/bugprone/UncheckedExpectedAccessCheck.h
+++ b/clang-tools-extra/clang-tidy/bugprone/UncheckedExpectedAccessCheck.h
@@ -25,6 +25,8 @@ class UncheckedExpectedAccessCheck : public ClangTidyCheck {
: ClangTidyCheck(Name, Context) {}
void registerMatchers(ast_matchers::MatchFinder *Finder) override;
void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
+ // TODO: Consider limiting to C++23 and later
+ // unless pre-C++23 implementations (e.g. `tl::expected`) get supported.
bool isLanguageVersionSupported(const LangOptions &LangOpts) const override {
return LangOpts.CPlusPlus;
}
>From 42b2e8654bdb1084dc50b56cadeea7d1462c676c Mon Sep 17 00:00:00 2001
From: BaLiKfromUA <valentin.yukhymenko at gmail.com>
Date: Sun, 18 Oct 2026 11:46:49 +0100
Subject: [PATCH 23/25] remove redundant comments
---
.../FlowSensitive/Models/UncheckedExpectedAccessModel.cpp | 7 -------
1 file changed, 7 deletions(-)
diff --git a/clang/lib/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.cpp b/clang/lib/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.cpp
index 8b63c5f5079146..0a7ca793489434 100644
--- a/clang/lib/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.cpp
+++ b/clang/lib/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.cpp
@@ -179,9 +179,6 @@ StorageLocation &locForHasValue(const RecordStorageLocation &ExpectedLoc) {
return ExpectedLoc.getSyntheticField("has_value");
}
-/// Returns the symbolic value that represents the "has_value" property of the
-/// expected at `ExpectedLoc`. Returns null if `ExpectedLoc` is null or is not
-/// the location of a `std::expected` object (e.g. of a derived class object).
BoolValue *getHasValue(Environment &Env, RecordStorageLocation *ExpectedLoc) {
if (ExpectedLoc == nullptr || !isExpectedType(ExpectedLoc->getType()))
return nullptr;
@@ -194,10 +191,6 @@ BoolValue *getHasValue(Environment &Env, RecordStorageLocation *ExpectedLoc) {
return HasValueVal;
}
-/// Sets `HasValueVal` as the symbolic value that represents the "has_value"
-/// property of the expected at `ExpectedLoc`. Does nothing if `ExpectedLoc` is
-/// null or is not the location of a `std::expected` object (e.g. of a derived
-/// class object).
void setHasValue(RecordStorageLocation *ExpectedLoc, BoolValue &HasValueVal,
Environment &Env) {
if (ExpectedLoc == nullptr || !isExpectedType(ExpectedLoc->getType()))
>From 8bbee78e21aee3094c78170c07202731867bbe4b Mon Sep 17 00:00:00 2001
From: BaLiKfromUA <valentin.yukhymenko at gmail.com>
Date: Thu, 22 Oct 2026 23:25:59 +0100
Subject: [PATCH 24/25] Add more limitations
---
.../checks/bugprone/unchecked-expected-access.md | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md b/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md
index 7154cd10ade789..34d72e5ea47022 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md
+++ b/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md
@@ -114,7 +114,7 @@ int p(std::expected<int, Error> e) {
## Limitations
-The check does not yet model some member functions of `std::expected`. These
+1. The check does not yet model some member functions of `std::expected`. These
are known, temporary limitations:
- `swap`. For example, the check does not know that after `a.swap(b)`, `a`
@@ -138,5 +138,9 @@ int g(std::expected<int, Error> e) {
}
```
-GoogleTest assertions such as `ASSERT_TRUE(e.has_value())` are not recognized
+2. **GoogleTest assertions** such as `ASSERT_TRUE(e.has_value())` are not recognized
yet; support for them is planned.
+
+3. **Lambdas.** The check does not currently report unsafe expected accesses in lambdas; support for analyzing is planned.
+
+4. **Derived classes.** Classes derived from `std::expected` are not analyzed by this check.
>From 4947fc6b64dde91d831e98f77d11e74f6913d534 Mon Sep 17 00:00:00 2001
From: BaLiKfromUA <valentin.yukhymenko at gmail.com>
Date: Sat, 24 Oct 2026 21:23:53 +0100
Subject: [PATCH 25/25] Basic support of `tl::expected`
Need it for battle-testing on real codebase
---
.../bugprone/UncheckedExpectedAccessCheck.cpp | 5 +-
clang-tools-extra/docs/ReleaseNotes.md | 5 +-
.../bugprone/unchecked-expected-access.md | 7 +-
.../unchecked-expected-access/tl/expected.hpp | 248 ++++++++++++
.../bugprone/unchecked-expected-access-tl.cpp | 363 ++++++++++++++++++
.../Models/UncheckedExpectedAccessModel.h | 21 +-
.../Models/UncheckedExpectedAccessModel.cpp | 94 +++--
7 files changed, 699 insertions(+), 44 deletions(-)
create mode 100644 clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-expected-access/tl/expected.hpp
create mode 100644 clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access-tl.cpp
diff --git a/clang-tools-extra/clang-tidy/bugprone/UncheckedExpectedAccessCheck.cpp b/clang-tools-extra/clang-tidy/bugprone/UncheckedExpectedAccessCheck.cpp
index f35ce8c74bfca1..77b20191f1d8c8 100644
--- a/clang-tools-extra/clang-tidy/bugprone/UncheckedExpectedAccessCheck.cpp
+++ b/clang-tools-extra/clang-tidy/bugprone/UncheckedExpectedAccessCheck.cpp
@@ -57,9 +57,8 @@ void UncheckedExpectedAccessCheck::check(
UncheckedExpectedAccessDiagnostic>(
*FuncDecl, *Result.Context, Diagnoser))
for (const UncheckedExpectedAccessDiagnostic &Diag : *Diags)
- diag(Diag.Loc,
- "unchecked access to 'std::expected' %select{value|error}0")
- << llvm::to_underlying(Diag.Kind);
+ diag(Diag.Loc, "unchecked access to '%0' %select{value|error}1")
+ << Diag.TypeName << llvm::to_underlying(Diag.Kind);
else
llvm::consumeError(Diags.takeError());
}
diff --git a/clang-tools-extra/docs/ReleaseNotes.md b/clang-tools-extra/docs/ReleaseNotes.md
index e70fa7132695bb..6fa941cbcbb534 100644
--- a/clang-tools-extra/docs/ReleaseNotes.md
+++ b/clang-tools-extra/docs/ReleaseNotes.md
@@ -144,8 +144,9 @@ infrastructure are described first, followed by tool-specific sections.
- New {doc}`bugprone-unchecked-expected-access
<clang-tidy/checks/bugprone/unchecked-expected-access>` check.
- Finds accesses to the value (or error) of a `std::expected` object that are
- not guarded by a check that the object holds a value (or error).
+ Finds accesses to the value (or error) of a `std::expected` or
+ `tl::expected` object that are not guarded by a check that the object holds
+ a value (or error).
- New {doc}`llvm-invalid-regex-pattern
<clang-tidy/checks/llvm/invalid-regex-pattern>` check.
diff --git a/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md b/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md
index 34d72e5ea47022..483c299cf30d78 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md
+++ b/clang-tools-extra/docs/clang-tidy/checks/bugprone/unchecked-expected-access.md
@@ -10,6 +10,11 @@ average clang-tidy check.
This check identifies unsafe accesses to values and errors contained in
`std::expected<T, E>` objects.
+The check also supports `tl::expected<T, E>` from
+[TartanLlama/expected](https://github.com/TartanLlama/expected), and
+everything below applies to it as well. Its `map` and `map_error` member
+functions are treated like `transform` and `transform_error`.
+
An access to the value of a `std::expected<T, E>` occurs when one of its
`value`, `operator*`, or `operator->` member functions is invoked. The check
considers these member functions as equivalent, even though `value` throws
@@ -143,4 +148,4 @@ yet; support for them is planned.
3. **Lambdas.** The check does not currently report unsafe expected accesses in lambdas; support for analyzing is planned.
-4. **Derived classes.** Classes derived from `std::expected` are not analyzed by this check.
+4. **Derived classes.** Classes derived from `std::expected` or `tl::expected` are not analyzed by this check.
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-expected-access/tl/expected.hpp b/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-expected-access/tl/expected.hpp
new file mode 100644
index 00000000000000..da80fb7689113c
--- /dev/null
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-expected-access/tl/expected.hpp
@@ -0,0 +1,248 @@
+#ifndef LLVM_CLANG_TOOLS_EXTRA_TEST_CLANG_TIDY_CHECKERS_INPUTS_TL_EXPECTED_HPP_
+#define LLVM_CLANG_TOOLS_EXTRA_TEST_CLANG_TIDY_CHECKERS_INPUTS_TL_EXPECTED_HPP_
+
+/// Mock of `tl::expected` from https://github.com/TartanLlama/expected.
+///
+/// Unlike `std::expected`, it has no `void` specialization and its comparison
+/// operators are free functions that are declared for both operand orders,
+/// including `operator!=`.
+
+#include <type_traits>
+
+namespace tl {
+
+template <class T, class E> class expected;
+
+struct in_place_t {
+ explicit in_place_t() = default;
+};
+static constexpr in_place_t in_place{};
+
+template <class E> class unexpected {
+public:
+ unexpected() = delete;
+ explicit unexpected(const E &e);
+ explicit unexpected(E &&e);
+
+ const E &value() const &;
+ E &value() &;
+ E &&value() &&;
+ const E &&value() const &&;
+};
+
+template <class E> unexpected(E) -> unexpected<E>;
+
+template <class E>
+unexpected<typename std::decay<E>::type> make_unexpected(E &&e);
+
+struct unexpect_t {
+ unexpect_t() = default;
+};
+static constexpr unexpect_t unexpect{};
+
+namespace detail {
+
+template <class> constexpr bool is_unexpected = false;
+template <class E> constexpr bool is_unexpected<unexpected<E>> = true;
+
+// The result of calling `F` with the value of an `expected<T, E>`.
+template <class F, class T> struct value_result {
+ using type = typename std::remove_cv<decltype(std::declval<F>()(
+ std::declval<T &>()))>::type;
+};
+template <class F> struct value_result<F, void> {
+ using type = typename std::remove_cv<decltype(std::declval<F>()())>::type;
+};
+template <class F> struct value_result<F, const void> {
+ using type = typename std::remove_cv<decltype(std::declval<F>()())>::type;
+};
+
+// The result of calling `F` with the error of an `expected<T, E>`.
+template <class F, class E> struct error_result {
+ using type = typename std::remove_cv<decltype(std::declval<F>()(
+ std::declval<E &>()))>::type;
+};
+
+} // namespace detail
+
+template <class T, class E> class expected {
+public:
+ typedef T value_type;
+ typedef E error_type;
+ typedef unexpected<E> unexpected_type;
+
+ constexpr expected() = default;
+ constexpr expected(const expected &rhs) = default;
+ constexpr expected(expected &&rhs) = default;
+ expected &operator=(const expected &rhs) = default;
+ expected &operator=(expected &&rhs) = default;
+
+ template <class... Args> expected(in_place_t, Args &&...args);
+
+ template <class G> expected(const unexpected<G> &e);
+ template <class G> expected(unexpected<G> &&e);
+
+ template <class... Args> explicit expected(unexpect_t, Args &&...args);
+
+ template <class U, class G,
+ std::enable_if_t<__is_constructible(T, const U &) &&
+ __is_constructible(E, const G &)> * = nullptr>
+ expected(const expected<U, G> &rhs);
+ template <class U, class G,
+ std::enable_if_t<__is_constructible(T, U &&) &&
+ __is_constructible(E, G &&)> * = nullptr>
+ expected(expected<U, G> &&rhs);
+
+ template <
+ class U = T,
+ std::enable_if_t<
+ __is_constructible(T, U &&) &&
+ !std::is_same<typename std::decay<U>::type, in_place_t>::value &&
+ !std::is_same<typename std::decay<U>::type, unexpect_t>::value &&
+ !std::is_same<typename std::decay<U>::type, expected>::value &&
+ !detail::is_unexpected<typename std::decay<U>::type>> * = nullptr>
+ expected(U &&v);
+
+ // `G` delays forming `T &`, which is ill-formed for `void`.
+ template <
+ class U = T, class G = T,
+ std::enable_if_t<
+ __is_constructible(T, U &&) && __is_assignable(G &, U &&) &&
+ !std::is_same<typename std::decay<U>::type, expected>::value &&
+ !detail::is_unexpected<typename std::decay<U>::type>> * = nullptr>
+ expected &operator=(U &&v);
+
+ template <class G> expected &operator=(const unexpected<G> &rhs);
+ template <class G> expected &operator=(unexpected<G> &&rhs);
+
+ template <class... Args> void emplace(Args &&...args);
+
+ void swap(expected &rhs);
+
+ const T *operator->() const;
+ T *operator->();
+
+ template <class U = T,
+ std::enable_if_t<!std::is_void<U>::value> * = nullptr>
+ const U &operator*() const &;
+ template <class U = T,
+ std::enable_if_t<!std::is_void<U>::value> * = nullptr>
+ U &operator*() &;
+ template <class U = T,
+ std::enable_if_t<!std::is_void<U>::value> * = nullptr>
+ U &&operator*() &&;
+
+ bool has_value() const noexcept;
+ explicit operator bool() const noexcept;
+
+ template <class U = T,
+ std::enable_if_t<!std::is_void<U>::value> * = nullptr>
+ const U &value() const &;
+ template <class U = T,
+ std::enable_if_t<!std::is_void<U>::value> * = nullptr>
+ U &value() &;
+ template <class U = T,
+ std::enable_if_t<!std::is_void<U>::value> * = nullptr>
+ U &&value() &&;
+
+ const E &error() const &;
+ E &error() &;
+ E &&error() &&;
+
+ template <class U> T value_or(U &&v) const &;
+ template <class U> T value_or(U &&v) &&;
+
+ // Defined, so that they can be called with lambdas, whose types have no
+ // linkage.
+ template <class F>
+ expected<typename detail::value_result<F, T>::type, E> map(F &&) & {
+ return {};
+ }
+ template <class F>
+ expected<typename detail::value_result<F, const T>::type, E>
+ map(F &&) const & {
+ return {};
+ }
+ template <class F>
+ expected<typename detail::value_result<F, T>::type, E> map(F &&) && {
+ return {};
+ }
+
+ template <class F>
+ expected<typename detail::value_result<F, T>::type, E> transform(F &&) & {
+ return {};
+ }
+ template <class F>
+ expected<typename detail::value_result<F, const T>::type, E>
+ transform(F &&) const & {
+ return {};
+ }
+ template <class F>
+ expected<typename detail::value_result<F, T>::type, E> transform(F &&) && {
+ return {};
+ }
+
+ template <class F>
+ expected<T, typename detail::error_result<F, E>::type> map_error(F &&) & {
+ return {};
+ }
+ template <class F>
+ expected<T, typename detail::error_result<F, const E>::type>
+ map_error(F &&) const & {
+ return {};
+ }
+ template <class F>
+ expected<T, typename detail::error_result<F, E>::type> map_error(F &&) && {
+ return {};
+ }
+
+ template <class F>
+ expected<T, typename detail::error_result<F, E>::type>
+ transform_error(F &&) & {
+ return {};
+ }
+ template <class F>
+ expected<T, typename detail::error_result<F, const E>::type>
+ transform_error(F &&) const & {
+ return {};
+ }
+ template <class F>
+ expected<T, typename detail::error_result<F, E>::type>
+ transform_error(F &&) && {
+ return {};
+ }
+};
+
+template <class T, class E, class U, class F>
+bool operator==(const expected<T, E> &lhs, const expected<U, F> &rhs);
+template <class T, class E, class U, class F>
+bool operator!=(const expected<T, E> &lhs, const expected<U, F> &rhs);
+template <class E, class F>
+bool operator==(const expected<void, E> &lhs, const expected<void, F> &rhs);
+template <class E, class F>
+bool operator!=(const expected<void, E> &lhs, const expected<void, F> &rhs);
+
+template <class T, class E, class U>
+bool operator==(const expected<T, E> &x, const U &v);
+template <class T, class E, class U>
+bool operator==(const U &v, const expected<T, E> &x);
+template <class T, class E, class U>
+bool operator!=(const expected<T, E> &x, const U &v);
+template <class T, class E, class U>
+bool operator!=(const U &v, const expected<T, E> &x);
+
+template <class T, class E>
+bool operator==(const expected<T, E> &x, const unexpected<E> &e);
+template <class T, class E>
+bool operator==(const unexpected<E> &e, const expected<T, E> &x);
+template <class T, class E>
+bool operator!=(const expected<T, E> &x, const unexpected<E> &e);
+template <class T, class E>
+bool operator!=(const unexpected<E> &e, const expected<T, E> &x);
+
+template <class T, class E>
+void swap(expected<T, E> &lhs, expected<T, E> &rhs);
+
+} // namespace tl
+
+#endif // LLVM_CLANG_TOOLS_EXTRA_TEST_CLANG_TIDY_CHECKERS_INPUTS_TL_EXPECTED_HPP_
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access-tl.cpp b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access-tl.cpp
new file mode 100644
index 00000000000000..d23334e84cad6d
--- /dev/null
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-expected-access-tl.cpp
@@ -0,0 +1,363 @@
+// RUN: %check_clang_tidy -std=c++17-or-later %s bugprone-unchecked-expected-access %t -- -- -I %S/Inputs/unchecked-expected-access
+
+#include "tl/expected.hpp"
+#include <utility>
+
+struct Foo {
+ void foo() const {}
+};
+
+constexpr auto add_one = [](int v) { return v + 1; };
+
+// Unchecked accesses.
+
+void unchecked_value_access(tl::expected<int, int> e) {
+ e.value();
+ // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: unchecked access to 'tl::expected' value [bugprone-unchecked-expected-access]
+}
+
+void unchecked_deref_operator_access(tl::expected<int, int> e) {
+ *e;
+ // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: unchecked access to 'tl::expected' value [bugprone-unchecked-expected-access]
+}
+
+void unchecked_arrow_operator_access(tl::expected<Foo, int> e) {
+ e->foo();
+ // CHECK-MESSAGES: :[[@LINE-1]]:4: warning: unchecked access to 'tl::expected' value [bugprone-unchecked-expected-access]
+}
+
+void access_in_negated_check(tl::expected<int, int> e) {
+ if (!e) {
+ *e;
+ // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: unchecked access to 'tl::expected' value [bugprone-unchecked-expected-access]
+ }
+}
+
+void unchecked_error_access(tl::expected<int, int> e) {
+ e.error();
+ // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: unchecked access to 'tl::expected' error [bugprone-unchecked-expected-access]
+}
+
+void error_access_in_has_value_check(tl::expected<int, int> e) {
+ if (e.has_value()) {
+ e.error();
+ // CHECK-MESSAGES: :[[@LINE-1]]:7: warning: unchecked access to 'tl::expected' error [bugprone-unchecked-expected-access]
+ }
+}
+
+void void_unchecked_error_access(tl::expected<void, int> e) {
+ e.error();
+ // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: unchecked access to 'tl::expected' error [bugprone-unchecked-expected-access]
+}
+
+// Checked accesses.
+
+void checked_access_with_has_value(tl::expected<Foo, int> e) {
+ if (e.has_value()) {
+ e.value();
+ *e;
+ e->foo();
+ }
+}
+
+int checked_access_in_ternary(tl::expected<int, int> e) {
+ return e ? *e : 0;
+}
+
+int checked_error_access_after_early_return(tl::expected<int, int> e) {
+ if (e)
+ return 0;
+ return e.error();
+}
+
+void void_checked_error_access(tl::expected<void, int> e) {
+ if (!e)
+ e.error();
+}
+
+void value_or_is_not_an_access(tl::expected<int, int> e) {
+ e.value_or(0);
+}
+
+void unexpected_value_is_not_diagnosed() {
+ tl::unexpected<int> u(1);
+ u.value();
+}
+
+// emplace.
+
+int emplace_if_empty(tl::expected<int, int> e) {
+ if (!e)
+ e.emplace(0);
+ return *e;
+}
+
+void error_access_after_emplace(tl::expected<int, int> e) {
+ e.emplace(1);
+ e.error();
+ // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: unchecked access to 'tl::expected' error [bugprone-unchecked-expected-access]
+}
+
+// Construction.
+
+int default_construct() {
+ tl::expected<int, int> e;
+ return *e;
+}
+
+int construct_from_value() {
+ tl::expected<int, int> e(5);
+ return *e;
+}
+
+int copy_initialize_from_value() {
+ tl::expected<int, int> e = 5;
+ return *e;
+}
+
+void construct_in_place() {
+ tl::expected<Foo, int> e(tl::in_place);
+ e->foo();
+}
+
+int construct_from_unexpected() {
+ tl::expected<int, int> e(tl::unexpected(1));
+ return e.error();
+}
+
+int copy_initialize_from_make_unexpected() {
+ tl::expected<int, int> e = tl::make_unexpected(1);
+ return e.error();
+}
+
+int construct_unexpect() {
+ tl::expected<int, int> e(tl::unexpect, 1);
+ return e.error();
+}
+
+void void_construct_unexpect() {
+ tl::expected<void, int> e(tl::unexpect, 1);
+ e.error();
+}
+
+int convert_from_checked(tl::expected<long, int> b) {
+ if (!b)
+ return 0;
+ tl::expected<int, int> a(b);
+ return *a;
+}
+
+int copy_construct_from_checked(tl::expected<int, int> b) {
+ if (!b)
+ return 0;
+ auto a = b;
+ return *a;
+}
+
+int error_access_after_default_construct() {
+ tl::expected<int, int> e;
+ return e.error();
+ // CHECK-MESSAGES: :[[@LINE-1]]:12: warning: unchecked access to 'tl::expected' error [bugprone-unchecked-expected-access]
+}
+
+void void_error_access_after_default_construct() {
+ tl::expected<void, int> e;
+ e.error();
+ // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: unchecked access to 'tl::expected' error [bugprone-unchecked-expected-access]
+}
+
+int value_access_after_construct_from_make_unexpected() {
+ tl::expected<int, int> e(tl::make_unexpected(1));
+ return *e;
+ // CHECK-MESSAGES: :[[@LINE-1]]:10: warning: unchecked access to 'tl::expected' value [bugprone-unchecked-expected-access]
+}
+
+int convert_from_unchecked(tl::expected<long, int> b) {
+ tl::expected<int, int> a(b);
+ return *a;
+ // CHECK-MESSAGES: :[[@LINE-1]]:10: warning: unchecked access to 'tl::expected' value [bugprone-unchecked-expected-access]
+}
+
+// Assignment.
+
+int assign_value(tl::expected<int, int> e) {
+ e = 5;
+ return *e;
+}
+
+int assign_make_unexpected(tl::expected<int, int> e) {
+ e = tl::make_unexpected(1);
+ return e.error();
+}
+
+int copy_assign_from_checked(tl::expected<int, int> a,
+ tl::expected<int, int> b) {
+ if (!b)
+ return 0;
+ a = b;
+ return *a;
+}
+
+int value_access_after_assign_make_unexpected(tl::expected<int, int> e) {
+ e = tl::make_unexpected(1);
+ return *e;
+ // CHECK-MESSAGES: :[[@LINE-1]]:10: warning: unchecked access to 'tl::expected' value [bugprone-unchecked-expected-access]
+}
+
+int error_access_after_assign_value(tl::expected<int, int> e) {
+ e = 5;
+ return e.error();
+ // CHECK-MESSAGES: :[[@LINE-1]]:12: warning: unchecked access to 'tl::expected' error [bugprone-unchecked-expected-access]
+}
+
+// Monadic operations.
+
+int map_value_access(tl::expected<int, int> e) {
+ if (!e)
+ return 0;
+ return *e.map(add_one);
+}
+
+int transform_value_access(tl::expected<int, int> e) {
+ if (!e)
+ return 0;
+ return *e.transform(add_one);
+}
+
+int map_error_error_access(tl::expected<int, int> e) {
+ if (e)
+ return 0;
+ return e.map_error(add_one).error();
+}
+
+int void_map(tl::expected<void, int> e) {
+ if (!e)
+ return 0;
+ return *e.map([] { return 1; });
+}
+
+int map_unchecked(tl::expected<int, int> e) {
+ return *e.map(add_one);
+ // CHECK-MESSAGES: :[[@LINE-1]]:10: warning: unchecked access to 'tl::expected' value [bugprone-unchecked-expected-access]
+}
+
+int map_error_unchecked(tl::expected<int, int> e) {
+ return e.map_error(add_one).error();
+ // CHECK-MESSAGES: :[[@LINE-1]]:31: warning: unchecked access to 'tl::expected' error [bugprone-unchecked-expected-access]
+}
+
+// Comparisons. Unlike for `std::expected`, these call operators that are
+// declared for both operand orders, and `operator!=` is declared explicitly.
+
+int equal_to_value(tl::expected<int, int> e) {
+ if (e == 5)
+ return *e;
+ return 0;
+}
+
+int value_equal_to_expected(tl::expected<int, int> e) {
+ if (5 == e)
+ return *e;
+ return 0;
+}
+
+int not_equal_to_value_early_return(tl::expected<int, int> e) {
+ if (e != 5)
+ return 0;
+ return *e;
+}
+
+int value_not_equal_to_expected_early_return(tl::expected<int, int> e) {
+ if (5 != e)
+ return 0;
+ return *e;
+}
+
+int equal_to_expected_with_value(tl::expected<int, int> a,
+ tl::expected<int, int> b) {
+ if (a == b && b)
+ return *a;
+ return 0;
+}
+
+int not_equal_to_expected_early_return(tl::expected<int, int> a,
+ tl::expected<int, int> b) {
+ if (a != b)
+ return 0;
+ if (b)
+ return *a;
+ return 0;
+}
+
+void void_not_equal_to_expected_with_value(tl::expected<void, int> a,
+ tl::expected<void, int> b) {
+ if (a != b && a)
+ b.error();
+}
+
+int equal_to_unexpected(tl::expected<int, int> e) {
+ if (e == tl::make_unexpected(1))
+ return e.error();
+ return 0;
+}
+
+int unexpected_equal_to_expected(tl::expected<int, int> e) {
+ if (tl::make_unexpected(1) == e)
+ return e.error();
+ return 0;
+}
+
+int not_equal_to_unexpected_early_return(tl::expected<int, int> e) {
+ if (e != tl::make_unexpected(1))
+ return 0;
+ return e.error();
+}
+
+int unexpected_not_equal_to_expected_early_return(tl::expected<int, int> e) {
+ if (tl::make_unexpected(1) != e)
+ return 0;
+ return e.error();
+}
+
+int not_equal_to_value(tl::expected<int, int> e) {
+ if (e != 5)
+ return *e;
+ // CHECK-MESSAGES: :[[@LINE-1]]:12: warning: unchecked access to 'tl::expected' value [bugprone-unchecked-expected-access]
+ return 0;
+}
+
+int value_not_equal_to_expected(tl::expected<int, int> e) {
+ if (5 != e)
+ return *e;
+ // CHECK-MESSAGES: :[[@LINE-1]]:12: warning: unchecked access to 'tl::expected' value [bugprone-unchecked-expected-access]
+ return 0;
+}
+
+int value_access_after_value_equal_to_expected(tl::expected<int, int> e) {
+ if (5 == e)
+ return 0;
+ return *e;
+ // CHECK-MESSAGES: :[[@LINE-1]]:10: warning: unchecked access to 'tl::expected' value [bugprone-unchecked-expected-access]
+}
+
+int not_equal_to_expected_with_value(tl::expected<int, int> a,
+ tl::expected<int, int> b) {
+ if (a != b && b)
+ return *a;
+ // CHECK-MESSAGES: :[[@LINE-1]]:12: warning: unchecked access to 'tl::expected' value [bugprone-unchecked-expected-access]
+ return 0;
+}
+
+int value_access_when_unexpected_equal_to_expected(tl::expected<int, int> e) {
+ if (tl::make_unexpected(1) == e)
+ return *e;
+ // CHECK-MESSAGES: :[[@LINE-1]]:12: warning: unchecked access to 'tl::expected' value [bugprone-unchecked-expected-access]
+ return 0;
+}
+
+int error_access_when_not_equal_to_unexpected(tl::expected<int, int> e) {
+ if (e != tl::make_unexpected(1))
+ return e.error();
+ // CHECK-MESSAGES: :[[@LINE-1]]:14: warning: unchecked access to 'tl::expected' error [bugprone-unchecked-expected-access]
+ return 0;
+}
diff --git a/clang/include/clang/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.h b/clang/include/clang/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.h
index b6d6b0e5e118a6..2928bc172fc24c 100644
--- a/clang/include/clang/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.h
+++ b/clang/include/clang/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.h
@@ -7,7 +7,7 @@
//===----------------------------------------------------------------------===//
//
// This file defines a dataflow analysis that detects unsafe accesses to the
-// values of `std::expected` objects.
+// values of `std::expected` and `tl::expected` objects.
//
//===----------------------------------------------------------------------===//
@@ -23,17 +23,22 @@
#include "clang/Analysis/FlowSensitive/NoopLattice.h"
#include "clang/Basic/SourceLocation.h"
#include "llvm/ADT/SmallVector.h"
+#include "llvm/ADT/StringRef.h"
namespace clang {
namespace dataflow {
-/// Dataflow analysis that models whether `std::expected` objects hold values.
+/// Dataflow analysis that models whether expected objects hold values.
+///
+/// Supported types are `std::expected` and `tl::expected`
+/// (https://github.com/TartanLlama/expected).
class UncheckedExpectedAccessModel
: public DataflowAnalysis<UncheckedExpectedAccessModel, NoopLattice> {
public:
UncheckedExpectedAccessModel(ASTContext &Ctx, Environment &Env);
- /// Returns a matcher for calls to member functions of `std::expected`.
+ /// Returns a matcher for calls to member functions of a supported expected
+ /// type.
static ast_matchers::StatementMatcher callToExpectedClass();
static NoopLattice initialElement() { return {}; }
@@ -44,7 +49,7 @@ class UncheckedExpectedAccessModel
CFGMatchSwitch<TransferState<NoopLattice>> TransferMatchSwitch;
};
-/// The part of a `std::expected` object that is accessed.
+/// The part of an expected object that is accessed.
///
/// The values are used as `%select` indices in diagnostics, so they must not
/// be reordered.
@@ -55,14 +60,16 @@ enum class UncheckedExpectedAccessKind : int {
Error = 1,
};
-/// Diagnostic information for an unchecked `std::expected` access.
+/// Diagnostic information for an unchecked expected access.
struct UncheckedExpectedAccessDiagnostic {
SourceLocation Loc;
UncheckedExpectedAccessKind Kind;
+ /// The accessed type, either "std::expected" or "tl::expected".
+ llvm::StringRef TypeName;
};
-/// Reports accesses to the value (or error) of `std::expected` objects that are
-/// not provably preceded by a check that the object holds a value (or error).
+/// Reports accesses to the value (or error) of expected objects that are not
+/// provably preceded by a check that the object holds a value (or error).
class UncheckedExpectedAccessDiagnoser {
public:
UncheckedExpectedAccessDiagnoser();
diff --git a/clang/lib/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.cpp b/clang/lib/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.cpp
index 0a7ca793489434..05b7d00874c267 100644
--- a/clang/lib/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.cpp
+++ b/clang/lib/Analysis/FlowSensitive/Models/UncheckedExpectedAccessModel.cpp
@@ -24,9 +24,25 @@
namespace clang {
namespace dataflow {
+/// Returns true if `DC` is `std` or the top-level `tl` namespace, which holds
+/// `tl::expected` (https://github.com/TartanLlama/expected).
+static bool isSupportedNamespace(const DeclContext &DC) {
+ if (DC.isStdNamespace())
+ return true;
+ const auto *NS = dyn_cast<NamespaceDecl>(&DC);
+ return NS != nullptr && NS->getDeclName().isIdentifier() &&
+ NS->getName() == "tl" && NS->getParent()->isTranslationUnit();
+}
+
static bool isExpectedClass(const CXXRecordDecl &RD) {
return RD.getDeclName().isIdentifier() && RD.getName() == "expected" &&
- RD.getDeclContext()->isStdNamespace();
+ isSupportedNamespace(*RD.getDeclContext());
+}
+
+/// Returns the name of the expected class `RD` to show in diagnostics.
+static llvm::StringRef getExpectedTypeName(const CXXRecordDecl &RD) {
+ return RD.getDeclContext()->isStdNamespace() ? "std::expected"
+ : "tl::expected";
}
// Classes derived from `std::expected` are deliberately not supported.
@@ -56,7 +72,7 @@ AST_MATCHER(CXXRecordDecl, expectedClass) { return isExpectedClass(Node); }
AST_MATCHER(CXXRecordDecl, unexpectedClass) {
return Node.getDeclName().isIdentifier() && Node.getName() == "unexpected" &&
- Node.getDeclContext()->isStdNamespace();
+ isSupportedNamespace(*Node.getDeclContext());
}
auto expectedMemberCall(ast_matchers::internal::Matcher<NamedDecl> Name) {
@@ -79,27 +95,25 @@ auto hasUnexpectedType() {
recordType(hasDeclaration(cxxRecordDecl(unexpectedClass())))));
}
-auto expectedEqualityCall() {
+auto expectedComparisonCall(
+ const ast_matchers::internal::Matcher<Expr> &Other) {
return cxxOperatorCallExpr(
- hasOverloadedOperatorName("=="), argumentCountIs(2),
- hasArgument(0, hasExpectedType()), hasArgument(1, hasExpectedType()));
+ hasAnyOverloadedOperatorName("==", "!="), argumentCountIs(2),
+ anyOf(allOf(hasArgument(0, hasExpectedType()), hasArgument(1, Other)),
+ allOf(hasArgument(0, Other), hasArgument(1, hasExpectedType()))));
+}
+
+auto expectedEqualityCall() {
+ return expectedComparisonCall(hasExpectedType());
}
-// Reversed comparisons (`v == e`) are rewritten to `e == v`, so the expected is
-// always the first argument.
auto expectedValueEqualityCall() {
- return cxxOperatorCallExpr(
- hasOverloadedOperatorName("=="), argumentCountIs(2),
- hasArgument(0, hasExpectedType()),
- hasArgument(1, unless(anyOf(hasExpectedType(), hasUnexpectedType()))));
+ return expectedComparisonCall(
+ unless(anyOf(hasExpectedType(), hasUnexpectedType())));
}
-// Reversed comparisons (`u == e`) are rewritten to `e == u`, so the expected is
-// always the first argument.
auto expectedUnexpectedEqualityCall() {
- return cxxOperatorCallExpr(
- hasOverloadedOperatorName("=="), argumentCountIs(2),
- hasArgument(0, hasExpectedType()), hasArgument(1, hasUnexpectedType()));
+ return expectedComparisonCall(hasUnexpectedType());
}
// Copy and move constructors are modeled by the framework, which copies the
@@ -115,7 +129,8 @@ auto expectedConstructor() {
auto expectedErrorConstructor() {
return cxxConstructExpr(
expectedConstructor(),
- hasArgument(0, anyOf(hasType(namedDecl(hasName("std::unexpect_t"))),
+ hasArgument(0, anyOf(hasType(namedDecl(hasAnyName("std::unexpect_t",
+ "tl::unexpect_t"))),
hasUnexpectedType())));
}
@@ -175,6 +190,17 @@ const Formula &forceBoolValue(Environment &Env, const Expr &E) {
return Value->formula();
}
+const Formula &equalityFormula(Environment &Env, const CXXOperatorCallExpr &E) {
+ const Formula &Result = forceBoolValue(Env, E);
+ if (E.getOperator() == OO_ExclaimEqual)
+ return Env.arena().makeNot(Result);
+ return Result;
+}
+
+const Expr &getExpectedOperand(const CXXOperatorCallExpr &E) {
+ return isExpectedType(E.getArg(0)->getType()) ? *E.getArg(0) : *E.getArg(1);
+}
+
StorageLocation &locForHasValue(const RecordStorageLocation &ExpectedLoc) {
return ExpectedLoc.getSyntheticField("has_value");
}
@@ -225,7 +251,6 @@ void transferTransformCall(const CXXMemberCallExpr *E,
State.Env);
}
-// `x != y` is rewritten to `!(x == y)`, so it needs no separate handling.
void transferExpectedEqualityCall(const CXXOperatorCallExpr *E,
const MatchFinder::MatchResult &,
LatticeTransferState &State) {
@@ -238,7 +263,7 @@ void transferExpectedEqualityCall(const CXXOperatorCallExpr *E,
return;
Arena &A = Env.arena();
- const Formula &EqVal = forceBoolValue(Env, *E);
+ const Formula &EqVal = equalityFormula(Env, *E);
const Formula &LHasValue = LHasValueVal->formula();
const Formula &RHasValue = RHasValueVal->formula();
@@ -257,13 +282,13 @@ void transferExpectedValueEqualityCall(const CXXOperatorCallExpr *E,
LatticeTransferState &State) {
Environment &Env = State.Env;
BoolValue *HasValueVal =
- getHasValue(Env, Env.get<RecordStorageLocation>(*E->getArg(0)));
+ getHasValue(Env, Env.get<RecordStorageLocation>(getExpectedOperand(*E)));
if (HasValueVal == nullptr)
return;
// `e == v` --> `e.has_value() && *e == v`
- Env.assume(
- Env.arena().makeImplies(forceBoolValue(Env, *E), HasValueVal->formula()));
+ Env.assume(Env.arena().makeImplies(equalityFormula(Env, *E),
+ HasValueVal->formula()));
}
void transferExpectedUnexpectedEqualityCall(const CXXOperatorCallExpr *E,
@@ -271,13 +296,13 @@ void transferExpectedUnexpectedEqualityCall(const CXXOperatorCallExpr *E,
LatticeTransferState &State) {
Environment &Env = State.Env;
BoolValue *HasValueVal =
- getHasValue(Env, Env.get<RecordStorageLocation>(*E->getArg(0)));
+ getHasValue(Env, Env.get<RecordStorageLocation>(getExpectedOperand(*E)));
if (HasValueVal == nullptr)
return;
// `e == u` --> `!e.has_value() && e.error() == u.error()`
Arena &A = Env.arena();
- Env.assume(A.makeImplies(forceBoolValue(Env, *E),
+ Env.assume(A.makeImplies(equalityFormula(Env, *E),
A.makeNot(HasValueVal->formula())));
}
@@ -339,17 +364,19 @@ auto buildTransferMatchSwitch() {
// expected::emplace
.CaseOfCFGStmt<CXXMemberCallExpr>(expectedMemberCall(hasName("emplace")),
transferEmplaceCall)
- // expected::transform, expected::transform_error
+ // expected::transform, expected::transform_error, and their
+ // `tl::expected` aliases expected::map, expected::map_error
.CaseOfCFGStmt<CXXMemberCallExpr>(
- expectedMemberCall(hasAnyName("transform", "transform_error")),
+ expectedMemberCall(
+ hasAnyName("transform", "transform_error", "map", "map_error")),
transferTransformCall)
- // operator== between two expecteds
+ // operator==, operator!= between two expecteds
.CaseOfCFGStmt<CXXOperatorCallExpr>(expectedEqualityCall(),
transferExpectedEqualityCall)
- // operator== between an expected and a value
+ // operator==, operator!= between an expected and a value
.CaseOfCFGStmt<CXXOperatorCallExpr>(expectedValueEqualityCall(),
transferExpectedValueEqualityCall)
- // operator== between an expected and an unexpected
+ // operator==, operator!= between an expected and an unexpected
.CaseOfCFGStmt<CXXOperatorCallExpr>(
expectedUnexpectedEqualityCall(),
transferExpectedUnexpectedEqualityCall)
@@ -370,12 +397,14 @@ auto buildTransferMatchSwitch() {
.Build();
}
+/// `ExpectedClass` is the class of the called accessor.
llvm::SmallVector<UncheckedExpectedAccessDiagnostic>
diagnoseAccess(SourceLocation AccessLoc, UncheckedExpectedAccessKind Kind,
+ const CXXRecordDecl &ExpectedClass,
const RecordStorageLocation *ExpectedLoc,
const Environment &Env) {
if (ExpectedLoc != nullptr) {
- // Accesses through classes derived from `std::expected` are not modeled.
+ // Accesses through classes derived from an expected are not modeled.
if (!isExpectedType(ExpectedLoc->getType()))
return {};
if (auto *HasValueVal = Env.get<BoolValue>(locForHasValue(*ExpectedLoc))) {
@@ -389,7 +418,7 @@ diagnoseAccess(SourceLocation AccessLoc, UncheckedExpectedAccessKind Kind,
}
}
- return {{AccessLoc, Kind}};
+ return {{AccessLoc, Kind, getExpectedTypeName(ExpectedClass)}};
}
auto buildDiagnoseMatchSwitch() {
@@ -403,6 +432,7 @@ auto buildDiagnoseMatchSwitch() {
const Environment &Env) {
return diagnoseAccess(E->getExprLoc(),
UncheckedExpectedAccessKind::Value,
+ *E->getMethodDecl()->getParent(),
getImplicitObjectLocation(*E, Env), Env);
})
// expected::operator*, expected::operator->
@@ -414,6 +444,7 @@ auto buildDiagnoseMatchSwitch() {
// expression, so point at the operator token instead.
return diagnoseAccess(
E->getOperatorLoc(), UncheckedExpectedAccessKind::Value,
+ *cast<CXXMethodDecl>(E->getCalleeDecl())->getParent(),
Env.get<RecordStorageLocation>(*E->getArg(0)), Env);
})
// expected::error
@@ -423,6 +454,7 @@ auto buildDiagnoseMatchSwitch() {
const Environment &Env) {
return diagnoseAccess(E->getExprLoc(),
UncheckedExpectedAccessKind::Error,
+ *E->getMethodDecl()->getParent(),
getImplicitObjectLocation(*E, Env), Env);
})
.Build();
More information about the cfe-commits
mailing list