[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
Sun Oct 18 03:51:05 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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()))



More information about the cfe-commits mailing list