[clang-tools-extra] [clang-tidy] Filter out googletest TUs in bugprone-unchecked-optional-access (PR #115051)

Jan Voung via cfe-commits cfe-commits at lists.llvm.org
Wed Jan 8 13:16:54 PST 2025


https://github.com/jvoung updated https://github.com/llvm/llvm-project/pull/115051

>From 8d83ec25ccdedad5f6a48e4a23176435f71a3e72 Mon Sep 17 00:00:00 2001
From: Jan Voung <jvoung at gmail.com>
Date: Tue, 5 Nov 2024 19:20:36 +0000
Subject: [PATCH 1/6] [clang-tidy] Filter out googletest TUs in
 bugprone-unchecked-optional-access

We currently do not handle ASSERT_TRUE(opt.has_value()) macros from
googletest (and other macros), so would see lots of false positives
in test TUs.
---
 .../bugprone/UncheckedOptionalAccessCheck.cpp | 19 ++++++++++++++-
 .../bugprone/UncheckedOptionalAccessCheck.h   |  8 ++++++-
 .../unchecked-optional-access/gtest/gtest.h   | 24 +++++++++++++++++++
 ...cked-optional-access-ignore-googletest.cpp | 24 +++++++++++++++++++
 4 files changed, 73 insertions(+), 2 deletions(-)
 create mode 100644 clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-optional-access/gtest/gtest.h
 create mode 100644 clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access-ignore-googletest.cpp

diff --git a/clang-tools-extra/clang-tidy/bugprone/UncheckedOptionalAccessCheck.cpp b/clang-tools-extra/clang-tidy/bugprone/UncheckedOptionalAccessCheck.cpp
index 0a0e212f345ed8..06deea0a446809 100644
--- a/clang-tools-extra/clang-tidy/bugprone/UncheckedOptionalAccessCheck.cpp
+++ b/clang-tools-extra/clang-tidy/bugprone/UncheckedOptionalAccessCheck.cpp
@@ -40,10 +40,27 @@ void UncheckedOptionalAccessCheck::registerMatchers(MatchFinder *Finder) {
       this);
 }
 
+void UncheckedOptionalAccessCheck::onStartOfTranslationUnit() {
+  // Reset the flag for each TU.
+  is_test_tu_ = false;
+}
+
 void UncheckedOptionalAccessCheck::check(
     const MatchFinder::MatchResult &Result) {
-  if (Result.SourceManager->getDiagnostics().hasUncompilableErrorOccurred())
+  // The googletest assertion macros are not currently recognized, so we have
+  // many false positives in tests. So, do not check functions in a test TU.
+  if (is_test_tu_ ||
+      Result.SourceManager->getDiagnostics().hasUncompilableErrorOccurred())
+    return;
+
+  // Look for two (public) googletest macros; if found, we'll mark this TU as a
+  // test TU. We look for ASSERT_TRUE because it is a problematic macro that
+  // we don't (yet) support, and GTEST_TEST to disambiguate ASSERT_TRUE.
+  if (Result.Context->Idents.get("ASSERT_TRUE").hasMacroDefinition() &&
+      Result.Context->Idents.get("GTEST_TEST").hasMacroDefinition()) {
+    is_test_tu_ = true;
     return;
+  }
 
   const auto *FuncDecl = Result.Nodes.getNodeAs<FunctionDecl>(FuncID);
   if (FuncDecl->isTemplated())
diff --git a/clang-tools-extra/clang-tidy/bugprone/UncheckedOptionalAccessCheck.h b/clang-tools-extra/clang-tidy/bugprone/UncheckedOptionalAccessCheck.h
index 2d1570f7df8abb..3bd7ff9867b145 100644
--- a/clang-tools-extra/clang-tidy/bugprone/UncheckedOptionalAccessCheck.h
+++ b/clang-tools-extra/clang-tidy/bugprone/UncheckedOptionalAccessCheck.h
@@ -12,7 +12,6 @@
 #include "../ClangTidyCheck.h"
 #include "clang/ASTMatchers/ASTMatchFinder.h"
 #include "clang/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.h"
-#include <optional>
 
 namespace clang::tidy::bugprone {
 
@@ -30,6 +29,7 @@ class UncheckedOptionalAccessCheck : public ClangTidyCheck {
             Options.getLocalOrGlobal("IgnoreSmartPointerDereference", false)} {}
   void registerMatchers(ast_matchers::MatchFinder *Finder) override;
   void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
+  void onStartOfTranslationUnit() override;
   bool isLanguageVersionSupported(const LangOptions &LangOpts) const override {
     return LangOpts.CPlusPlus;
   }
@@ -40,6 +40,12 @@ class UncheckedOptionalAccessCheck : public ClangTidyCheck {
 
 private:
   dataflow::UncheckedOptionalAccessModelOptions ModelOptions;
+
+  // Records whether the current TU includes the googletest headers, in which
+  // case we assume it is a test TU of some sort. The googletest assertion
+  // macros are not currently recognized, so we have many false positives in
+  // tests. So, we disable checking for all test TUs.
+  bool is_test_tu_ = false;
 };
 
 } // namespace clang::tidy::bugprone
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-optional-access/gtest/gtest.h b/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-optional-access/gtest/gtest.h
new file mode 100644
index 00000000000000..8a9eeac94dbb77
--- /dev/null
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-optional-access/gtest/gtest.h
@@ -0,0 +1,24 @@
+#ifndef LLVM_CLANG_TOOLS_EXTRA_TEST_CLANG_TIDY_CHECKERS_INPUTS_GTEST_GTEST_H_
+#define LLVM_CLANG_TOOLS_EXTRA_TEST_CLANG_TIDY_CHECKERS_INPUTS_GTEST_GTEST_H_
+
+// Mock version of googletest macros.
+
+#define GTEST_TEST(test_suite_name, test_name)                                 \
+  class GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) {                   \
+  public:                                                                      \
+    GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)() = default;            \
+  };
+
+#define GTEST_AMBIGUOUS_ELSE_BLOCKER_                                          \
+  switch (0)                                                                   \
+  case 0:                                                                      \
+  default: // NOLINT
+
+#define ASSERT_TRUE(condition)                                                 \
+  GTEST_AMBIGUOUS_ELSE_BLOCKER_                                                \
+  if (condition)                                                               \
+    ;                                                                          \
+  else                                                                         \
+    return;  // should fail...
+
+#endif  // LLVM_CLANG_TOOLS_EXTRA_TEST_CLANG_TIDY_CHECKERS_INPUTS_GTEST_GTEST_H_
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access-ignore-googletest.cpp b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access-ignore-googletest.cpp
new file mode 100644
index 00000000000000..2f213434d0ad61
--- /dev/null
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access-ignore-googletest.cpp
@@ -0,0 +1,24 @@
+// RUN: %check_clang_tidy %s bugprone-unchecked-optional-access %t -- -- -I %S/Inputs/unchecked-optional-access
+
+#include "absl/types/optional.h"
+#include "gtest/gtest.h"
+
+// All of the warnings are suppressed once we detect that we are in a test TU
+// even the unchecked_value_access case.
+
+// CHECK-MESSAGE: 1 warning generated
+// CHECK-MESSAGES: Suppressed 1 warnings
+
+void unchecked_value_access(const absl::optional<int> &opt) {
+  opt.value();
+}
+
+void assert_true_check_operator_access(const absl::optional<int> &opt) {
+  ASSERT_TRUE(opt.has_value());
+  *opt;
+}
+
+void assert_true_check_value_access(const absl::optional<int> &opt) {
+  ASSERT_TRUE(opt.has_value());
+  opt.value();
+}

>From 3427fa73c174ccdc68b703c20c0ab40a7418e955 Mon Sep 17 00:00:00 2001
From: Jan Voung <jvoung at gmail.com>
Date: Tue, 5 Nov 2024 19:29:28 +0000
Subject: [PATCH 2/6] Add one more missing macro

---
 .../bugprone/Inputs/unchecked-optional-access/gtest/gtest.h    | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-optional-access/gtest/gtest.h b/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-optional-access/gtest/gtest.h
index 8a9eeac94dbb77..ba865dfc5a966f 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-optional-access/gtest/gtest.h
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-optional-access/gtest/gtest.h
@@ -3,6 +3,9 @@
 
 // Mock version of googletest macros.
 
+#define GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)                     \
+  test_suite_name##_##test_name##_Test
+
 #define GTEST_TEST(test_suite_name, test_name)                                 \
   class GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) {                   \
   public:                                                                      \

>From 2611af1a9a7ecb007d0f0f735d0b7b1581a0f1b1 Mon Sep 17 00:00:00 2001
From: Jan Voung <jvoung at gmail.com>
Date: Wed, 6 Nov 2024 22:10:28 +0000
Subject: [PATCH 3/6] Simplify the mock a bit more

The implementation doesn't matter as much for testing, since the
currently skipping is keying on some top-level macros.
---
 .../unchecked-optional-access/gtest/gtest.h   | 21 ++++++-------------
 1 file changed, 6 insertions(+), 15 deletions(-)

diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-optional-access/gtest/gtest.h b/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-optional-access/gtest/gtest.h
index ba865dfc5a966f..e3b545c01d2fa1 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-optional-access/gtest/gtest.h
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-optional-access/gtest/gtest.h
@@ -3,25 +3,16 @@
 
 // Mock version of googletest macros.
 
-#define GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)                     \
-  test_suite_name##_##test_name##_Test
-
-#define GTEST_TEST(test_suite_name, test_name)                                 \
-  class GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) {                   \
-  public:                                                                      \
-    GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)() = default;            \
-  };
-
-#define GTEST_AMBIGUOUS_ELSE_BLOCKER_                                          \
-  switch (0)                                                                   \
-  case 0:                                                                      \
-  default: // NOLINT
+// Normally this declares a class, but it isn't relevant for testing.
+#define GTEST_TEST(test_suite_name, test_name)
 
+// Normally, this has a relatively complex implementation
+// (wrapping the condition evaluation), more complex failure behavior, etc.,
+// but we keep it simple for testing.
 #define ASSERT_TRUE(condition)                                                 \
-  GTEST_AMBIGUOUS_ELSE_BLOCKER_                                                \
   if (condition)                                                               \
     ;                                                                          \
   else                                                                         \
-    return;  // should fail...
+    return;  // normally "fails" rather than just return
 
 #endif  // LLVM_CLANG_TOOLS_EXTRA_TEST_CLANG_TIDY_CHECKERS_INPUTS_GTEST_GTEST_H_

>From ee10510d15e75da7fc75420fa33d00e97d6477a0 Mon Sep 17 00:00:00 2001
From: Jan Voung <jvoung at gmail.com>
Date: Fri, 6 Dec 2024 19:29:51 +0000
Subject: [PATCH 4/6] Make an option, default off

Also fix a CHECK-MESSAGE -> CHECK-MESSAGES
---
 .../bugprone/UncheckedOptionalAccessCheck.cpp |  8 +++--
 .../bugprone/UncheckedOptionalAccessCheck.h   | 32 ++++++++++++++++---
 ...cked-optional-access-ignore-googletest.cpp | 20 +++++++++---
 3 files changed, 48 insertions(+), 12 deletions(-)

diff --git a/clang-tools-extra/clang-tidy/bugprone/UncheckedOptionalAccessCheck.cpp b/clang-tools-extra/clang-tidy/bugprone/UncheckedOptionalAccessCheck.cpp
index 06deea0a446809..71de2fda005bd7 100644
--- a/clang-tools-extra/clang-tidy/bugprone/UncheckedOptionalAccessCheck.cpp
+++ b/clang-tools-extra/clang-tidy/bugprone/UncheckedOptionalAccessCheck.cpp
@@ -48,8 +48,9 @@ void UncheckedOptionalAccessCheck::onStartOfTranslationUnit() {
 void UncheckedOptionalAccessCheck::check(
     const MatchFinder::MatchResult &Result) {
   // The googletest assertion macros are not currently recognized, so we have
-  // many false positives in tests. So, do not check functions in a test TU.
-  if (is_test_tu_ ||
+  // many false positives in tests. So, do not check functions in a test TU
+  // if the option ignore_test_tus_ is set.
+  if ((ignore_test_tus_ && is_test_tu_) ||
       Result.SourceManager->getDiagnostics().hasUncompilableErrorOccurred())
     return;
 
@@ -59,7 +60,8 @@ void UncheckedOptionalAccessCheck::check(
   if (Result.Context->Idents.get("ASSERT_TRUE").hasMacroDefinition() &&
       Result.Context->Idents.get("GTEST_TEST").hasMacroDefinition()) {
     is_test_tu_ = true;
-    return;
+    if (ignore_test_tus_)
+      return;
   }
 
   const auto *FuncDecl = Result.Nodes.getNodeAs<FunctionDecl>(FuncID);
diff --git a/clang-tools-extra/clang-tidy/bugprone/UncheckedOptionalAccessCheck.h b/clang-tools-extra/clang-tidy/bugprone/UncheckedOptionalAccessCheck.h
index 3bd7ff9867b145..3e20fd1a1ed4eb 100644
--- a/clang-tools-extra/clang-tidy/bugprone/UncheckedOptionalAccessCheck.h
+++ b/clang-tools-extra/clang-tidy/bugprone/UncheckedOptionalAccessCheck.h
@@ -10,6 +10,9 @@
 #define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_BUGPRONE_UNCHECKEDOPTIONALACCESSCHECK_H
 
 #include "../ClangTidyCheck.h"
+#include "../ClangTidyOptions.h"
+#include "clang/Basic/LLVM.h"
+#include "clang/Basic/LangOptions.h"
 #include "clang/ASTMatchers/ASTMatchFinder.h"
 #include "clang/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.h"
 
@@ -26,7 +29,8 @@ class UncheckedOptionalAccessCheck : public ClangTidyCheck {
   UncheckedOptionalAccessCheck(StringRef Name, ClangTidyContext *Context)
       : ClangTidyCheck(Name, Context),
         ModelOptions{
-            Options.getLocalOrGlobal("IgnoreSmartPointerDereference", false)} {}
+            Options.getLocalOrGlobal("IgnoreSmartPointerDereference", false)},
+        ignore_test_tus_(Options.getLocalOrGlobal("IgnoreTestTUs", false)) {}
   void registerMatchers(ast_matchers::MatchFinder *Finder) override;
   void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
   void onStartOfTranslationUnit() override;
@@ -36,15 +40,33 @@ class UncheckedOptionalAccessCheck : public ClangTidyCheck {
   void storeOptions(ClangTidyOptions::OptionMap &Opts) override {
     Options.store(Opts, "IgnoreSmartPointerDereference",
                   ModelOptions.IgnoreSmartPointerDereference);
+    Options.store(Opts, "IgnoreTestTUs",
+                  ignore_test_tus_);
   }
 
 private:
   dataflow::UncheckedOptionalAccessModelOptions ModelOptions;
 
-  // Records whether the current TU includes the googletest headers, in which
-  // case we assume it is a test TU of some sort. The googletest assertion
-  // macros are not currently recognized, so we have many false positives in
-  // tests. So, we disable checking for all test TUs.
+  // Tracks the Option of whether we should ignore test TUs (e.g., googletest).
+  // Currently we have many false positives in tests, making it difficult to
+  // find true positives and developers end up ignoring the warnings in tests,
+  // reducing the check's effectiveness.
+  // Reasons for false positives (once fixed we could remove this option):
+  // - has_value() checks wrapped in googletest assertion macros are not handled
+  //   (e.g., EXPECT_TRUE() and fall through, or ASSERT_TRUE() and crash,
+  //    or more complex ones like ASSERT_THAT(x, Not(Eq(std::nullopt))))
+  // - we don't handle state carried over from test fixture constructors/setup
+  //   to test case bodies (constructor may initialize an optional to a value)
+  // - developers may make shortcuts in tests making assumptions and
+  //   use the test runs (expecially with sanitizers) to check assumptions.
+  //   This is different from production code in that test code should have
+  //   near 100% coverage (if not covered by the test itself, it is dead code).
+  bool ignore_test_tus_ = false;
+
+  // Records whether the current TU includes the test-specific headers (e.g.,
+  // googletest), in which case we assume it is a test TU of some sort.
+  // This along with the setting `ignore_test_tus_` allows us to disable
+  // checking for all test TUs.
   bool is_test_tu_ = false;
 };
 
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access-ignore-googletest.cpp b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access-ignore-googletest.cpp
index 2f213434d0ad61..3effafc041daa4 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access-ignore-googletest.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access-ignore-googletest.cpp
@@ -1,16 +1,25 @@
-// RUN: %check_clang_tidy %s bugprone-unchecked-optional-access %t -- -- -I %S/Inputs/unchecked-optional-access
+// RUN: %check_clang_tidy -check-suffix=IGNORE-TESTS %s \
+// RUN:   bugprone-unchecked-optional-access %t -- \
+// RUN:   -config="{CheckOptions: \
+// RUN:            {bugprone-unchecked-optional-access.IgnoreTestTUs: true}}" \
+// RUN:   -- -I %S/Inputs/unchecked-optional-access
+
+// RUN: %check_clang_tidy -check-suffix=DEFAULT %s \
+// RUN:   bugprone-unchecked-optional-access %t -- \
+// RUN:   -- -I %S/Inputs/unchecked-optional-access
 
 #include "absl/types/optional.h"
 #include "gtest/gtest.h"
 
-// All of the warnings are suppressed once we detect that we are in a test TU
+// When IgnoreTestTUs is set, all of the warnings in a test TU are suppressed,
 // even the unchecked_value_access case.
 
-// CHECK-MESSAGE: 1 warning generated
-// CHECK-MESSAGES: Suppressed 1 warnings
+// CHECK-MESSAGES-IGNORE-TESTS: 1 warning generated
+// CHECK-MESSAGES-DEFAULT: 2 warnings generated
 
 void unchecked_value_access(const absl::optional<int> &opt) {
   opt.value();
+  // CHECK-MESSAGES-DEFAULT: :[[@LINE-1]]:3: warning: unchecked access to optional value
 }
 
 void assert_true_check_operator_access(const absl::optional<int> &opt) {
@@ -22,3 +31,6 @@ void assert_true_check_value_access(const absl::optional<int> &opt) {
   ASSERT_TRUE(opt.has_value());
   opt.value();
 }
+
+// CHECK-MESSAGES-IGNORE-TESTS: Suppressed 1 warnings
+// CHECK-MESSAGES-DEFAULT: Suppressed 1 warnings

>From d8b311d053cdbccca43420533eeb0570bf62a8d9 Mon Sep 17 00:00:00 2001
From: Jan Voung <jvoung at gmail.com>
Date: Fri, 6 Dec 2024 19:43:23 +0000
Subject: [PATCH 5/6] fix clang-format

---
 .../clang-tidy/bugprone/UncheckedOptionalAccessCheck.h     | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

diff --git a/clang-tools-extra/clang-tidy/bugprone/UncheckedOptionalAccessCheck.h b/clang-tools-extra/clang-tidy/bugprone/UncheckedOptionalAccessCheck.h
index 3e20fd1a1ed4eb..ec87e23fa7d83d 100644
--- a/clang-tools-extra/clang-tidy/bugprone/UncheckedOptionalAccessCheck.h
+++ b/clang-tools-extra/clang-tidy/bugprone/UncheckedOptionalAccessCheck.h
@@ -11,10 +11,10 @@
 
 #include "../ClangTidyCheck.h"
 #include "../ClangTidyOptions.h"
-#include "clang/Basic/LLVM.h"
-#include "clang/Basic/LangOptions.h"
 #include "clang/ASTMatchers/ASTMatchFinder.h"
 #include "clang/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.h"
+#include "clang/Basic/LLVM.h"
+#include "clang/Basic/LangOptions.h"
 
 namespace clang::tidy::bugprone {
 
@@ -40,8 +40,7 @@ class UncheckedOptionalAccessCheck : public ClangTidyCheck {
   void storeOptions(ClangTidyOptions::OptionMap &Opts) override {
     Options.store(Opts, "IgnoreSmartPointerDereference",
                   ModelOptions.IgnoreSmartPointerDereference);
-    Options.store(Opts, "IgnoreTestTUs",
-                  ignore_test_tus_);
+    Options.store(Opts, "IgnoreTestTUs", ignore_test_tus_);
   }
 
 private:

>From ed8e56a59deacfc235f66a582b528298c1ac256b Mon Sep 17 00:00:00 2001
From: Jan Voung <jvoung at gmail.com>
Date: Wed, 8 Jan 2025 20:49:44 +0000
Subject: [PATCH 6/6] Address review comments

- lightweight detection of catch2
- style etc.
- add release note about option
---
 .../bugprone/UncheckedOptionalAccessCheck.cpp | 27 ++++--
 .../bugprone/UncheckedOptionalAccessCheck.h   | 26 +++---
 clang-tools-extra/docs/ReleaseNotes.rst       |  7 ++
 .../catch2/catch_test_macros.hpp              | 32 +++++++
 .../unchecked-optional-access/gtest/gtest.h   | 27 ++++--
 ...ecked-optional-access-ignore-catchtest.cpp | 85 +++++++++++++++++++
 ...cked-optional-access-ignore-googletest.cpp | 30 +++++--
 7 files changed, 196 insertions(+), 38 deletions(-)
 create mode 100644 clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-optional-access/catch2/catch_test_macros.hpp
 create mode 100644 clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access-ignore-catchtest.cpp

diff --git a/clang-tools-extra/clang-tidy/bugprone/UncheckedOptionalAccessCheck.cpp b/clang-tools-extra/clang-tidy/bugprone/UncheckedOptionalAccessCheck.cpp
index 71de2fda005bd7..eee26604c1d8fd 100644
--- a/clang-tools-extra/clang-tidy/bugprone/UncheckedOptionalAccessCheck.cpp
+++ b/clang-tools-extra/clang-tidy/bugprone/UncheckedOptionalAccessCheck.cpp
@@ -42,7 +42,7 @@ void UncheckedOptionalAccessCheck::registerMatchers(MatchFinder *Finder) {
 
 void UncheckedOptionalAccessCheck::onStartOfTranslationUnit() {
   // Reset the flag for each TU.
-  is_test_tu_ = false;
+  IsTestTu = false;
 }
 
 void UncheckedOptionalAccessCheck::check(
@@ -50,17 +50,26 @@ void UncheckedOptionalAccessCheck::check(
   // The googletest assertion macros are not currently recognized, so we have
   // many false positives in tests. So, do not check functions in a test TU
   // if the option ignore_test_tus_ is set.
-  if ((ignore_test_tus_ && is_test_tu_) ||
+  if ((IgnoreTestTus && IsTestTu) ||
       Result.SourceManager->getDiagnostics().hasUncompilableErrorOccurred())
     return;
 
-  // Look for two (public) googletest macros; if found, we'll mark this TU as a
-  // test TU. We look for ASSERT_TRUE because it is a problematic macro that
-  // we don't (yet) support, and GTEST_TEST to disambiguate ASSERT_TRUE.
-  if (Result.Context->Idents.get("ASSERT_TRUE").hasMacroDefinition() &&
-      Result.Context->Idents.get("GTEST_TEST").hasMacroDefinition()) {
-    is_test_tu_ = true;
-    if (ignore_test_tus_)
+  // Look for some public test library macros; if found, we'll mark this TU as a
+  // test TU. We look for two macros from each library to help disambiguate
+  // (otherwise "ASSERT_TRUE" or "REQUIRE" could be macros for non-test code).
+  IdentifierTable &Idents = Result.Context->Idents;
+  if (
+      // googletest
+      (Idents.get("ASSERT_TRUE").hasMacroDefinition() &&
+       Idents.get("GTEST_TEST").hasMacroDefinition()) ||
+      // catch2 w/out prefix
+      (Idents.get("REQUIRE_FALSE").hasMacroDefinition() &&
+       Idents.get("METHOD_AS_TEST_CASE").hasMacroDefinition()) ||
+      // catch2 w/ prefix
+      (Idents.get("CATCH_REQUIRE_FALSE").hasMacroDefinition() &&
+       Idents.get("CATCH_METHOD_AS_TEST_CASE").hasMacroDefinition())) {
+    IsTestTu = true;
+    if (IgnoreTestTus)
       return;
   }
 
diff --git a/clang-tools-extra/clang-tidy/bugprone/UncheckedOptionalAccessCheck.h b/clang-tools-extra/clang-tidy/bugprone/UncheckedOptionalAccessCheck.h
index 03640acb570b57..b9817dc35b07fe 100644
--- a/clang-tools-extra/clang-tidy/bugprone/UncheckedOptionalAccessCheck.h
+++ b/clang-tools-extra/clang-tidy/bugprone/UncheckedOptionalAccessCheck.h
@@ -29,8 +29,7 @@ class UncheckedOptionalAccessCheck : public ClangTidyCheck {
   UncheckedOptionalAccessCheck(StringRef Name, ClangTidyContext *Context)
       : ClangTidyCheck(Name, Context),
         ModelOptions{Options.get("IgnoreSmartPointerDereference", false)},
-        ignore_test_tus_(Options.get("IgnoreTestTUs", false)) {}
-
+        IgnoreTestTus(Options.get("IgnoreTestTUs", false)) {}
   void registerMatchers(ast_matchers::MatchFinder *Finder) override;
   void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
   void onStartOfTranslationUnit() override;
@@ -40,33 +39,32 @@ class UncheckedOptionalAccessCheck : public ClangTidyCheck {
   void storeOptions(ClangTidyOptions::OptionMap &Opts) override {
     Options.store(Opts, "IgnoreSmartPointerDereference",
                   ModelOptions.IgnoreSmartPointerDereference);
-    Options.store(Opts, "IgnoreTestTUs", ignore_test_tus_);
+    Options.store(Opts, "IgnoreTestTUs", IgnoreTestTus);
   }
 
 private:
   dataflow::UncheckedOptionalAccessModelOptions ModelOptions;
 
-  // Tracks the Option of whether we should ignore test TUs (e.g., googletest).
-  // Currently we have many false positives in tests, making it difficult to
-  // find true positives and developers end up ignoring the warnings in tests,
-  // reducing the check's effectiveness.
+  // Tracks the Option of whether we should ignore test TUs (e.g., googletest,
+  // catch2). Currently we have many false positives in tests, making it
+  // difficult to find true positives and developers end up ignoring the
+  // warnings in tests, reducing the check's effectiveness.
   // Reasons for false positives (once fixed we could remove this option):
   // - has_value() checks wrapped in googletest assertion macros are not handled
   //   (e.g., EXPECT_TRUE() and fall through, or ASSERT_TRUE() and crash,
   //    or more complex ones like ASSERT_THAT(x, Not(Eq(std::nullopt))))
+  //   Catch2's REQUIRE, CHECK, etc. General macro issue:
+  //   https://github.com/llvm/llvm-project/issues/62600
   // - we don't handle state carried over from test fixture constructors/setup
   //   to test case bodies (constructor may initialize an optional to a value)
   // - developers may make shortcuts in tests making assumptions and
   //   use the test runs (expecially with sanitizers) to check assumptions.
-  //   This is different from production code in that test code should have
-  //   near 100% coverage (if not covered by the test itself, it is dead code).
-  bool ignore_test_tus_ = false;
+  bool IgnoreTestTus = false;
 
   // Records whether the current TU includes the test-specific headers (e.g.,
-  // googletest), in which case we assume it is a test TU of some sort.
-  // This along with the setting `ignore_test_tus_` allows us to disable
-  // checking for all test TUs.
-  bool is_test_tu_ = false;
+  // googletest, catch2), in which case we assume it is a test TU.
+  // This along with `IgnoreTestTus` allows us to disable checking in test TUs.
+  bool IsTestTu = false;
 };
 
 } // namespace clang::tidy::bugprone
diff --git a/clang-tools-extra/docs/ReleaseNotes.rst b/clang-tools-extra/docs/ReleaseNotes.rst
index 94e15639c4a92e..755d8ef1711503 100644
--- a/clang-tools-extra/docs/ReleaseNotes.rst
+++ b/clang-tools-extra/docs/ReleaseNotes.rst
@@ -232,6 +232,13 @@ Changes in existing checks
   <clang-tidy/checks/bugprone/unchecked-optional-access>` to support
   `bsl::optional` and `bdlb::NullableValue` from
   <https://github.com/bloomberg/bde>_.
+  Added option `IgnoreTestTUs` to suppress all warnings in Googletest and Catch2
+  translation units. This is useful (a) if projects don't have separate folders
+  for test code (otherwise, one can just add a .clang-tidy config in test
+  folders to exclude the check) (b) until false positives due to the analysis
+  seeing values of checks propagate through a variety of complex macros and
+  wrapper functions is fixed (general problem
+  https://github.com/llvm/llvm-project/issues/62600).
 
 - Improved :doc:`bugprone-unhandled-self-assignment
   <clang-tidy/checks/bugprone/unhandled-self-assignment>` check by fixing smart
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-optional-access/catch2/catch_test_macros.hpp b/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-optional-access/catch2/catch_test_macros.hpp
new file mode 100644
index 00000000000000..1c15bd82668510
--- /dev/null
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-optional-access/catch2/catch_test_macros.hpp
@@ -0,0 +1,32 @@
+#ifndef LLVM_CLANG_TOOLS_EXTRA_TEST_CLANG_TIDY_CHECKERS_INPUTS_CATCH2_CATCH_TEST_MACROS_HPP_
+#define LLVM_CLANG_TOOLS_EXTRA_TEST_CLANG_TIDY_CHECKERS_INPUTS_CATCH2_CATCH_TEST_MACROS_HPP_
+
+// Mock version of catch2 test framework macros.
+
+// The macros are normally much more complex. For
+// - REQUIRE and REQUIRE_FALSE: define them to go through some wrapper
+//   (that is not at all what catch2 actually does)
+// - TEST_CASE and METHOD_AS_TEST_CASE: define a function
+void Wrapper(bool cond) {
+  if (!cond)
+    __builtin_trap();
+}
+
+#define CONCAT_HELP(X,Y) X##Y  // helper macro
+#define CONCAT(X,Y) CONCAT_HELP(X,Y)
+
+// Catch2 can be configured to have a prefix "CATCH" for macro names, or not,
+// so we model this.
+#ifdef CATCH_CONFIG_PREFIX_ALL
+#define CATCH_REQUIRE( ... ) Wrapper(static_cast<bool>(__VA_ARGS__))
+#define CATCH_REQUIRE_FALSE( ... ) Wrapper(static_cast<bool>(__VA_ARGS__))
+#define CATCH_TEST_CASE( ... ) void CONCAT(TEST, __LINE__)()
+#define CATCH_METHOD_AS_TEST_CASE( ... ) void CONCAT(TEST, __LINE__)()
+#else
+#define REQUIRE( ... ) Wrapper(static_cast<bool>(__VA_ARGS__))
+#define REQUIRE_FALSE( ... ) Wrapper(static_cast<bool>(__VA_ARGS__))
+#define TEST_CASE( ... ) void CONCAT(TEST, __LINE__)()
+#define METHOD_AS_TEST_CASE( ... ) void CONCAT(TEST, __LINE__)()
+#endif
+
+#endif // LLVM_CLANG_TOOLS_EXTRA_TEST_CLANG_TIDY_CHECKERS_INPUTS_CATCH2_CATCH_TEST_MACROS_HPP_
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-optional-access/gtest/gtest.h b/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-optional-access/gtest/gtest.h
index e3b545c01d2fa1..3e17737bedfe34 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-optional-access/gtest/gtest.h
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-optional-access/gtest/gtest.h
@@ -6,13 +6,28 @@
 // Normally this declares a class, but it isn't relevant for testing.
 #define GTEST_TEST(test_suite_name, test_name)
 
-// Normally, this has a relatively complex implementation
-// (wrapping the condition evaluation), more complex failure behavior, etc.,
-// but we keep it simple for testing.
+// Normally googletest creates a class wrapping the condition, which the
+// dataflow analysis framework has difficulty "seeing through"
+// (needs some inter-procedural analysis, or special-case handling).
+// Here is a simplified version.
+class BoolWrapper {
+public:
+  template <typename T>
+  BoolWrapper(T &&val) : success_(static_cast<bool>(val)) {}
+  operator bool() const { return success_; }
+  bool success_;
+};
+
 #define ASSERT_TRUE(condition)                                                 \
-  if (condition)                                                               \
+  if (BoolWrapper(condition))                                                  \
+    ;                                                                          \
+  else                                                                         \
+    return;
+
+#define ASSERT_FALSE(condition)                                                \
+  if (BoolWrapper(!condition))                                                 \
     ;                                                                          \
   else                                                                         \
-    return;  // normally "fails" rather than just return
+    return;
 
-#endif  // LLVM_CLANG_TOOLS_EXTRA_TEST_CLANG_TIDY_CHECKERS_INPUTS_GTEST_GTEST_H_
+#endif // LLVM_CLANG_TOOLS_EXTRA_TEST_CLANG_TIDY_CHECKERS_INPUTS_GTEST_GTEST_H_
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access-ignore-catchtest.cpp b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access-ignore-catchtest.cpp
new file mode 100644
index 00000000000000..fbf3b20d66f73b
--- /dev/null
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access-ignore-catchtest.cpp
@@ -0,0 +1,85 @@
+// RUN: %check_clang_tidy -check-suffix=IGNORE-TESTS %s \
+// RUN:   bugprone-unchecked-optional-access %t -- \
+// RUN:   -config="{CheckOptions: \
+// RUN:            {bugprone-unchecked-optional-access.IgnoreTestTUs: true}}" \
+// RUN:   -- -I %S/Inputs/unchecked-optional-access
+
+// RUN: %check_clang_tidy -check-suffix=IGNORE-TESTS-CATCH-PREFIX %s \
+// RUN:   --extra-arg=-DCATCH_CONFIG_PREFIX_ALL \
+// RUN:   bugprone-unchecked-optional-access %t -- \
+// RUN:   -config="{CheckOptions: \
+// RUN:            {bugprone-unchecked-optional-access.IgnoreTestTUs: true}}" \
+// RUN:   -- -I %S/Inputs/unchecked-optional-access
+
+// RUN: %check_clang_tidy -check-suffix=DEFAULT %s \
+// RUN:   bugprone-unchecked-optional-access %t -- \
+// RUN:   -- -I %S/Inputs/unchecked-optional-access
+
+// RUN: %check_clang_tidy -check-suffix=DEFAULT-CATCH-PREFIX %s \
+// RUN:   --extra-arg=-DCATCH_CONFIG_PREFIX_ALL \
+// RUN:   bugprone-unchecked-optional-access %t -- \
+// RUN:   -- -I %S/Inputs/unchecked-optional-access
+
+#include "absl/types/optional.h"
+#include "catch2/catch_test_macros.hpp"
+
+// When IgnoreTestTUs is set, all of the warnings in a test TU are suppressed,
+// even the "Unguarded access" and "Guarded incorrectly" cases.
+
+// CHECK-MESSAGES-IGNORE-TESTS: 2 warnings generated
+// CHECK-MESSAGES-IGNORE-TESTS-CATCH-PREFIX: 1 warning generated
+// CHECK-MESSAGES-DEFAULT: 6 warnings generated
+// CHECK-MESSAGES-DEFAULT-CATCH-PREFIX: 3 warnings generated
+
+absl::optional<int> FunctionUnderTest();
+
+#ifndef CATCH_CONFIG_PREFIX_ALL
+
+TEST_CASE("FN -- Unguarded access", "[optional]") {
+  auto opt = FunctionUnderTest();
+  opt.value();
+  // CHECK-MESSAGES-DEFAULT: :[[@LINE-1]]:3: warning: unchecked access to optional value
+}
+
+TEST_CASE("FN -- Guarded incorrectly assert false has_value()", "[optional]") {
+  auto opt = FunctionUnderTest();
+  REQUIRE_FALSE(!opt.has_value());
+  opt.value();
+  // CHECK-MESSAGES-DEFAULT: :[[@LINE-1]]:3: warning: unchecked access to optional value
+}
+
+TEST_CASE("FP -- Guarded assert true has_value()", "[optional]") {
+  auto opt = FunctionUnderTest();
+  REQUIRE(opt.has_value());
+  *opt;
+  // CHECK-MESSAGES-DEFAULT: :[[@LINE-1]]:4: warning: unchecked access to optional value
+}
+
+TEST_CASE("FP -- Guarded assert false not operator bool()", "[optional]") {
+  auto opt = FunctionUnderTest();
+  REQUIRE_FALSE(!opt);
+  opt.value();
+  // CHECK-MESSAGES-DEFAULT: :[[@LINE-1]]:3: warning: unchecked access to optional value
+}
+
+#else
+
+CATCH_TEST_CASE("FN -- Unguarded access", "[optional]") {
+  auto opt = FunctionUnderTest();
+  opt.value();
+  // CHECK-MESSAGES-DEFAULT-CATCH-PREFIX: :[[@LINE-1]]:3: warning: unchecked access to optional value
+}
+
+CATCH_TEST_CASE("FP -- Guarded assert true has_value()", "[optional]") {
+  auto opt = FunctionUnderTest();
+  CATCH_REQUIRE(opt.has_value());
+  *opt;
+  // CHECK-MESSAGES-DEFAULT-CATCH-PREFIX: :[[@LINE-1]]:4: warning: unchecked access to optional value
+}
+
+#endif
+
+// CHECK-MESSAGES-IGNORE-TESTS: Suppressed 2 warnings
+// CHECK-MESSAGES-IGNORE-TESTS-CATCH-PREFIX: Suppressed 1 warning
+// CHECK-MESSAGES-DEFAULT: Suppressed 2 warnings
+// CHECK-MESSAGES-DEFAULT-CATCH-PREFIX: Suppressed 1 warning
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access-ignore-googletest.cpp b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access-ignore-googletest.cpp
index 3effafc041daa4..0e74bf9aed1779 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access-ignore-googletest.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access-ignore-googletest.cpp
@@ -12,25 +12,37 @@
 #include "gtest/gtest.h"
 
 // When IgnoreTestTUs is set, all of the warnings in a test TU are suppressed,
-// even the unchecked_value_access case.
+// even the `unchecked_value_access` and `assert_true_incorrect` cases.
 
-// CHECK-MESSAGES-IGNORE-TESTS: 1 warning generated
-// CHECK-MESSAGES-DEFAULT: 2 warnings generated
+// CHECK-MESSAGES-IGNORE-TESTS: 2 warnings generated
+// CHECK-MESSAGES-DEFAULT: 6 warnings generated
 
-void unchecked_value_access(const absl::optional<int> &opt) {
+// False negative from suppressing in test TU.
+void unchecked_value_access(const absl::optional<int> opt) {
   opt.value();
   // CHECK-MESSAGES-DEFAULT: :[[@LINE-1]]:3: warning: unchecked access to optional value
 }
 
-void assert_true_check_operator_access(const absl::optional<int> &opt) {
+// False negative from suppressing in test TU.
+void assert_true_incorrect(const absl::optional<int> opt) {
+  ASSERT_TRUE(!opt.has_value());
+  opt.value();
+  // CHECK-MESSAGES-DEFAULT: :[[@LINE-1]]:3: warning: unchecked access to optional value
+}
+
+// False positive, unless we suppress in test TU.
+void assert_true_check_has_value(const absl::optional<int> opt) {
   ASSERT_TRUE(opt.has_value());
   *opt;
+  // CHECK-MESSAGES-DEFAULT: :[[@LINE-1]]:4: warning: unchecked access to optional value
 }
 
-void assert_true_check_value_access(const absl::optional<int> &opt) {
-  ASSERT_TRUE(opt.has_value());
+// False positive, unless we suppress (one of many other ways to check)
+void assert_true_check_operator_bool_not_false(const absl::optional<int> opt) {
+  ASSERT_FALSE(!opt);
   opt.value();
+  // CHECK-MESSAGES-DEFAULT: :[[@LINE-1]]:3: warning: unchecked access to optional value
 }
 
-// CHECK-MESSAGES-IGNORE-TESTS: Suppressed 1 warnings
-// CHECK-MESSAGES-DEFAULT: Suppressed 1 warnings
+// CHECK-MESSAGES-IGNORE-TESTS: Suppressed 2 warnings
+// CHECK-MESSAGES-DEFAULT: Suppressed 2 warnings



More information about the cfe-commits mailing list