Skip to content

Conversation

jvoung
Copy link
Contributor

@jvoung jvoung commented Nov 5, 2024

Behind an option (default is off / current behavior)

We currently do not handle:

  • ASSERT_TRUE(opt.has_value()) macros followed by *opt (inside or outside macros) from googletest
  • and other macros EXPECT_EQ,, CHECK (though from absl instead of gtest)), some of which have complex forms (ASSERT_THAT(opt, Not(Eq(std::nullopt))))
  • state carried over from test fixture ctor -> test body

So would see lots of false positives in test TUs. Some test TUs have 100s to 1000 warnings, which are too many to look through to find "true" signals, to make it worth warning in tests.

…-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.
@jvoung jvoung marked this pull request as ready for review November 5, 2024 19:59
@llvmbot
Copy link
Member

llvmbot commented Nov 5, 2024

@llvm/pr-subscribers-clang-tidy

Author: Jan Voung (jvoung)

Changes

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.


Full diff: https://github.com/llvm/llvm-project/pull/115051.diff

4 Files Affected:

  • (modified) clang-tools-extra/clang-tidy/bugprone/UncheckedOptionalAccessCheck.cpp (+18-1)
  • (modified) clang-tools-extra/clang-tidy/bugprone/UncheckedOptionalAccessCheck.h (+7-1)
  • (added) clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-optional-access/gtest/gtest.h (+27)
  • (added) clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access-ignore-googletest.cpp (+24)
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..ba865dfc5a966f
--- /dev/null
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-optional-access/gtest/gtest.h
@@ -0,0 +1,27 @@
+#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_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
+
+#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();
+}

@llvmbot
Copy link
Member

llvmbot commented Nov 5, 2024

@llvm/pr-subscribers-clang-tools-extra

Author: Jan Voung (jvoung)

Changes

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.


Full diff: https://github.com/llvm/llvm-project/pull/115051.diff

4 Files Affected:

  • (modified) clang-tools-extra/clang-tidy/bugprone/UncheckedOptionalAccessCheck.cpp (+18-1)
  • (modified) clang-tools-extra/clang-tidy/bugprone/UncheckedOptionalAccessCheck.h (+7-1)
  • (added) clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-optional-access/gtest/gtest.h (+27)
  • (added) clang-tools-extra/test/clang-tidy/checkers/bugprone/unchecked-optional-access-ignore-googletest.cpp (+24)
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..ba865dfc5a966f
--- /dev/null
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unchecked-optional-access/gtest/gtest.h
@@ -0,0 +1,27 @@
+#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_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
+
+#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();
+}

@jvoung jvoung requested a review from ymand November 5, 2024 19:59
Copy link
Contributor

@carlosgalvezp carlosgalvezp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Relying on Google-test internal implementation details that are outside of our control and may change at any point in time does not feel good.

The patch should instead fix the root cause of the problem.

@carlosgalvezp
Copy link
Contributor

A quick middle-point solution is to add an option to allow ignoring code that is executed from within macros, or even
allow the user to specify which macros to ignore.

@jvoung
Copy link
Contributor Author

jvoung commented Nov 5, 2024

Relying on Google-test internal implementation details that are outside of our control and may change at any point in time does not feel good.

The patch should instead fix the root cause of the problem.

I agree that it's not good to rely on implementation details, but these are not internal implementation details - these are key elements of the public API , which trip up the analysis. It's not just any macros that we have a problem with, it's specifically the googletest macros, which we don't properly understand, resulting in false positives.

@jvoung
Copy link
Contributor Author

jvoung commented Nov 5, 2024

A quick middle-point solution is to add an option to allow ignoring code that is executed from within macros, or even allow the user to specify which macros to ignore.

Unfortunately, the problem is exactly that we're not (fully) understanding the content of ASSERT_TRUE and related macros (ASSERT_FALSE, ASSERT_THAT(..., IsTrue(opt.has_value())), etc.), which then affect code outside and following the macros. So, ignoring them would not solve anything. Until we can understand the googletest macros, there is little value in check any tests that use them. That is what this patch accomplishes.

@carlosgalvezp
Copy link
Contributor

carlosgalvezp commented Nov 6, 2024

but these are not internal implementation details - these are key elements of the public API

In the unit test, you have copied internal code from here:
https://github.com/google/googletest/blob/d144031940543e15423a25ae5a8a74141044862f/googletest/include/gtest/internal/gtest-internal.h#L1477

This is not OK to do, since it may change at any time and make the test no longer useful. The test may also work for some versions of googletest but not others.

The code that has been copied is also Copyrighted and has certain License obligations, which we may not be allowed to bring into the LLVM repository. Pinging @tstellar for clarification.

@carlosgalvezp
Copy link
Contributor

carlosgalvezp commented Nov 6, 2024

we're not (fully) understanding the content

My thinking was that we don't even need to understand the content, we simply exclude code that is contained within any of the problematic public macros. This sounds like it should be possible to do? Unfortunately I don't know the details on how this could be implemented, hopefully other reviewers know better?

Otherwise ChatGPT seems to give useful ideas on how to skip a matched result contained within an ASSERT macro (obviously untested):

  if (Lexer::getImmediateMacroName(Loc, SM, Result.Context->getLangOpts()) == "ASSERT") {
    // The call is within ASSERT, no diagnostic needed.
    return;
  }

The implementation doesn't matter as much for testing, since the
currently skipping is keying on some top-level macros.
@jvoung
Copy link
Contributor Author

jvoung commented Nov 6, 2024

we're not (fully) understanding the content

My thinking was that we don't even need to understand the content, we simply exclude code that is contained within any of the problematic public macros. This sounds like it should be possible to do? Unfortunately I don't know the details on how this could be implemented, hopefully other reviewers know better?

Otherwise ChatGPT seems to give useful ideas on how to skip a matched result contained within an ASSERT macro (obviously untested):

  if (Lexer::getImmediateMacroName(Loc, SM, Result.Context->getLangOpts()) == "ASSERT") {
    // The call is within ASSERT, no diagnostic needed.
    return;
  }

That doesn't handle some cases like:

auto opt = DoSomeSetup(...)
ASSERT_TRUE(opt.has_value())
T x = DoMoreSetup(*opt)  // warn right here, since we didn't interpret the above ASSERT_TRUE (or other ways to check)

EXPECT_EQ(FunctionToTest(x), ...);

Sometimes the *opt may be within a macro, but not always.

@jvoung
Copy link
Contributor Author

jvoung commented Nov 6, 2024

but these are not internal implementation details - these are key elements of the public API

In the unit test, you have copied internal code from here: https://github.com/google/googletest/blob/d144031940543e15423a25ae5a8a74141044862f/googletest/include/gtest/internal/gtest-internal.h#L1477

This is not OK to do, since it may change at any time and make the test no longer useful. The test may also work for some versions of googletest but not others.

The code that has been copied is also Copyrighted and has certain License obligations, which we may not be allowed to bring into the LLVM repository. Pinging @tstellar for clarification.

Simplified the mock, since we're only matching on the top-level macro names.

@jvoung
Copy link
Contributor Author

jvoung commented Nov 7, 2024

we're not (fully) understanding the content

My thinking was that we don't even need to understand the content, we simply exclude code that is contained within any of the problematic public macros. This sounds like it should be possible to do? Unfortunately I don't know the details on how this could be implemented, hopefully other reviewers know better?
Otherwise ChatGPT seems to give useful ideas on how to skip a matched result contained within an ASSERT macro (obviously untested):

  if (Lexer::getImmediateMacroName(Loc, SM, Result.Context->getLangOpts()) == "ASSERT") {
    // The call is within ASSERT, no diagnostic needed.
    return;
  }

That doesn't handle some cases like:

auto opt = DoSomeSetup(...)
ASSERT_TRUE(opt.has_value())
T x = DoMoreSetup(*opt)  // warn right here, since we didn't interpret the above ASSERT_TRUE (or other ways to check)

EXPECT_EQ(FunctionToTest(x), ...);

Sometimes the *opt may be within a macro, but not always.

  • In non-test code, it is even more likely that the *opt is not wrapped in a macro, while the ASSERT(opt.has_value()) is.
  • If in non-test scenarios, the ASSERT macro implementation is actually simple, and the analysis already understood ASSERT(opt.has_value()) makes a following *opt safe, then ignoring the ASSERT is actually worse and causes false positives.
  • We wouldn't want to accidentally ignore the wrong macros (especially in non-test contexts)
  • Coming up with a list of exactly the right macros to ignore for gtest, would be a bigger list of public macro names than the current change.
  • And it still doesn't solve the "sometimes the *opt may be within a macro, but not always."

@jvoung
Copy link
Contributor Author

jvoung commented Nov 13, 2024

Any other concerns? I think I've elaborated on why the ignore/exclude alternative is not better.

Otherwise, eventually, it would be great to be able to understand the various macros and how they could serve as checks for later accesses, but I think this is an improvement on the status quo (we've seen lots of such googletest false positives).

Thanks!

@carlosgalvezp
Copy link
Contributor

Apologies for the late response, I don't have much time to perform in-depth reviews. Tagging additional reviewers @5chmidti @HerrCai0907

Copy link
Contributor

@5chmidti 5chmidti left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we can ignore a TU simply because it has the Google Test header included, which this will do.
This would ignore real problems, such as

#include <gtest/gtest.h>
#include <optional>
void issue(const absl::optional<int> &opt) {
  if (!opt.has_value())
  *opt;
}

The issue with the Google Test macros seems to be, that the dataflow check does not understand using a helper for the cast to bool like this:

#include <optional>

struct Converter {
    template <typename T>
    Converter(T&& val) : success{static_cast<bool>(std::forward<T>(val))} {}
    operator bool() const { return success; }
    bool success;
};

void bar(std::optional<int> v) {
    switch (0)
    case 0:
    default:
        if (const auto wrapped_check = Converter{v})
            ;
        else
            return;
    auto val = *v;
}

https://godbolt.org/z/7EdsGYhaW

(found the FP by expanding the ASSERT_TRUE macro and reducing manually)

Copy link
Contributor

@HerrCai0907 HerrCai0907 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it is a good way to suppress all warning when we find gtest macro.

  1. it will confuse user who really want to check even in test file.
  2. the user can disable this rules in test folder or even use nolint the test area.

the potential solution is treat assert_true as assert in dataflow analysis

@jvoung
Copy link
Contributor Author

jvoung commented Nov 14, 2024

I don't think we can ignore a TU simply because it has the Google Test header included, which this will do. This would ignore real problems, such as

That is true (and we've considered that -- FWIW, ymand and our team have been maintaining this checker).
But as is, we are currently seeing many false positives in test files, and it becomes harder for developers to spot such true positives, given the FP noise to filter through.

Would it help if this was instead a checker option?

the user can disable this rules in test folder or even use nolint the test area.

I don't think nolint is a viable option, given the volume of false positives. It would just add lots of noise to the source code.

The issue with the Google Test macros seems to be, that the dataflow check does not understand using a helper for the cast to bool like this:

...
        if (const auto wrapped_check = Converter{v})
            ;
        else
            return;
    auto val = *v;
}

Thanks, that's definitely part of it. If we handle EXPECT_TRUE as well (and other EXPECT_... forms), EXPECT does not have a return so would need special handling (otherwise looks like both branches go to the *v).

the potential solution is treat assert_true as assert in dataflow analysis

Right, that is what we'd like in the longer term, as I mentioned "eventually, it would be great to be able to understand the various macros and how they could serve as checks for later accesses".

@jvoung
Copy link
Contributor Author

jvoung commented Dec 2, 2024

Hi @5chmidti and @HerrCai0907 any thoughts on the "Would it help if this was instead a checker option?"? Given the volume of false positives right now, I think skip by default, but can turn on if needed.

I updated the description to include the point of "we are currently seeing many false positives in test files, and it becomes harder for developers to spot such true positives, given the FP noise to filter through".

@HerrCai0907
Copy link
Contributor

Could clang/lib/ASTMatchers/GtestMatchers.cpp help you to have a better solution?

Also fix a CHECK-MESSAGE -> CHECK-MESSAGES
Copy link

github-actions bot commented Dec 6, 2024

✅ With the latest revision this PR passed the C/C++ code formatter.

@jvoung
Copy link
Contributor Author

jvoung commented Dec 6, 2024

Thanks! Ok that does that look useful (and I think the our other reviewer ymand@ wrote it =) https://reviews.llvm.org/D74840).

  • I'll work on using those in parallel
  • It will need to be extended a little to handle a few more matches (right now it's the binary comparison eq/ne, etc. not unary EXPECT_TRUE that is more common for optional)
  • Handle enough of the cases EXPECT_TRUE, EXPECT_EQ, EXPECT_NE, EXPECT_THAT...
  • Do some analysis on our false positive data to see what else remains. We have some data already of other issues besides macros, like state carried over from constructors. There may be cases still where we just can't reduce the FP (developers making shortcuts not doing the check, assuming that running the test will catch the issue)

In the meantime, I updated the patch to make an option that is off by default. For all other users the behavior is unchanged, but we can opt in on our side.

Can we add the option to use in the meantime, while I work on the macro handling in parallel?

Thanks!

Copy link
Contributor

@5chmidti 5chmidti left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

option that is off by default. For all other users the behavior is unchanged, but we can opt in on our side.
Can we add the option to use in the meantime, while I work on the macro handling in parallel?

That sounds okay to me, even though the option is expected to be removed again because it won't be needed then. Others may have stronger opinions on this, so lets see.

A less fine-grained solution to skip these FPs would be to create a .clang-tidy inside test directories with InheritParentConfig enabled, and exclude the check there. Though that would mean that, e.g., test utils would be skipped that do not contain the macro.


While you're at it, maybe add the catch2 and doctest REQUIRE macro as well? This could of course mean that the option may not be removedfor those test frameworks if only the GTest macros are handled, which could be complex enough without implementing the REQUIRE macro as well.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also add an ASSERT_FALSE (or just ASSERT_TRUE(!opt.has_value())) with a dereference after, to show that there are false-negatives

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done (there was a false-negative example in unchecked_value_access but also added the ASSERT_FALSE)

Comment on lines 63 to 69
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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: var casing should be CamelCase

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, done!

Comment on lines 49 to 62
// 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).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a release note, and add the option to the check documentation with an explanation similar to this, but targeted towards the user.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point -- added a note about the option in release notes (e.g., when preferred, use the .clang-tidy instead, if test files are actually separated into folders)

Copy link
Contributor

@HerrCai0907 HerrCai0907 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am still confused why your team did not just disable this rule for test folder?
It is normal cases that source code and test code have different quality metrics.

Copy link
Contributor

@HerrCai0907 HerrCai0907 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better to use GtestCmp if you really want clang-tidy support gtest in this check.
Here is an unfinished example which can match

TEST(a, b) {
  std::optional<int> a;
  if (v) {
    a = 1;
  }
  ASSERT_NE(a, std::nullopt);
  a.value();
}
  using namespace ast_matchers;
  auto HasOptionalCallDescendant = hasDescendant(callExpr(callee(cxxMethodDecl(
      ofClass(UncheckedOptionalAccessModel::optionalClassDecl())))));
  Finder->addMatcher(
      decl(anyOf(functionDecl(
                     unless(isExpansionInSystemHeader()),
                     // FIXME: Remove the filter below when lambdas are
                     // well supported by the check.
                     unless(hasDeclContext(cxxRecordDecl(isLambda()))),
                     hasBody(allOf(
                         HasOptionalCallDescendant,
                         unless(hasDescendant(gtestAssert(
                             GtestCmp::Ne,
                             expr(hasType(qualType(hasUnqualifiedDesugaredType(
                                 recordType(hasDeclaration(
                                     UncheckedOptionalAccessModel::
                                         optionalClassDecl())))))),
                             anything())))))),
                 cxxConstructorDecl(hasAnyConstructorInitializer(
                     withInitializer(HasOptionalCallDescendant)))))
          .bind(FuncID),
      this);

@carlosgalvezp
Copy link
Contributor

carlosgalvezp commented Dec 7, 2024

It is normal cases that source code and test code have different quality metrics.

+1. That's responsibility of the build system. "What is a test" is something that can vary from project to project. There's many other clang-tidy checks that don't make sense in tests (for example, *-avoid-magic-numbers), and we wouldn't want to start adding such a flag to all these checks.

is expected to be removed again

From experience, removing flags it's quite hard, due to backwards compatibility reasons (we break other people that rely on them). It usually takes about 2 releases (1 year) to pull it off.

So I would be strongly against adding such a flag now if we already see that it will be removed - we should rather take the time to implement this right from the start.

@ymand
Copy link
Collaborator

ymand commented Dec 9, 2024

I am still confused why your team did not just disable this rule for test folder? It is normal cases that source code and test code have different quality metrics.

Our codebase doesn't use test folders. We have no foolproof way of distinguishing code from test based on the file system.

@ymand
Copy link
Collaborator

ymand commented Dec 9, 2024

This review has been going on for longer than a month so, as one of the reviewers and the original author of this check, I wanted to jump in and see if we can bring it to an agreeable conclusion. I’d like to start with some background:

  • This check was introduced in 2020 with the current limitations of poor understanding of tests and was raised as an issue in May 2023. Our codebase does not distinguish test folders for the most part, so we relied on regexp matching of file names, which never really worked (the patterns match both too much and too little).
  • This check is built on the Clang Dataflow Framework, which takes primary responsibility for modeling code. The check simply wraps the optional model from that framework. In that project (of which I am a lead and active contributor), we have not prioritized modeling test code, because modeling the large variety of test macros correctly is quite difficult and we see it as a lower value than improving the framework and models for normal code. We would love to have better coverage, but given our limited resources, this is our prioritization.
  • Despite this limitation being present in this checker since 2020, no one in the broader community has ever expressed an interest in picking up this work. We take that as a (weak) signal of lack of user prioritization for this feature as well. For codebases with dedicated test directories, that makes a lot of sense.
  • We (jvoung and I) cannot commit to the level of work required to fully support the variety of googletest matchers — not least ASSERT_THAT, for which we've seen significant use with optional values. We certainly can’t commit to support beyond googletest, which is a real issue, as 5chmidti's comment about catch2 and REQUIRE highlights.

With all this, we view it as necessary, for codebases without dedicated test directories, to offer this feature in the checker. Not as a short-term fix, but as a long term feature. As we progress (hopefully) in coverage of test macros, the users of this feature will be able to gradually switch off of it — starting with those users that only rely on the relatively simple ASSERT_TRUE/FALSE and continuing from there.

Given how long this issue has remained open, I hope we can resolve and reach a conclusion soon.

Thanks!
Yitzhak

@ymand
Copy link
Collaborator

ymand commented Dec 18, 2024

Gentle ping, since it's been a week since my last post.

@ymand ymand requested a review from Xazax-hun December 18, 2024 14:19
@Xazax-hun
Copy link
Collaborator

Xazax-hun commented Dec 18, 2024

I think the ideal solution would be something like using the analyzer_noreturn attribute in GTest to mark branches that should not be taken by static analyzers because some invariants failed and every further diagnostics are false positive on that path. This is the solution that the Clang Static Analyzer recommends to users (adding annotations to custom asserts). That being said, there is a precedent of the analyzer recognizing custom assert functions and treating them as noreturn even if they are not annotated: https://github.com/llvm/llvm-project/blob/main/clang/lib/StaticAnalyzer/Checkers/NoReturnFunctionChecker.cpp

If GTest had a similar function for the assert failed paths, a similar approach could be taken for the dataflow framework as well. If there is no such function that we could match on in the expanded code, I don't think we could do anything, we might need to do changes to GTest.

All that being said, I think we want to provide a good experience for people using this popular testing library out of the box, even if they use an older version that does not have any annotations. I am OK with suppressing noisy static analysis results for test files for the time being. It looks like the current options are either leave it broken on tests or turn it off for tests as modeling non-test code is prioritized. As a user I would definitely prefer the latter.

Copy link
Contributor

@5chmidti 5chmidti left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the delay.

I think, that we can probably all agree that the best solution would be to have the model support the macros, e.g., by recognizing the patterns that are behind these macros. However, that is currently not something that people can find time to do (as was communicated by @ymand w.r.t. current priorities), and not something that seems to have caused significant enough irritation for someone to step in themselves.

As discussed, disabling the check inside the test folder only works if the project structure supports it (which is IMO the majority, but that is just a feeling), but that is also something users have to know they can do. We should open an issue with the help wanted label, and mention explicitly that this is not simple in the description. That way, someone may be inclined to start working on this.

I think the GTest matchers don't work here, because you'd want to mark the optional as having a value or not having a value, and you would need access to the environment at that time, which is not available at this stage. But the GTest macros could be used for a crude implementation inside the model, right?

I'm not really that opposed to this solution, but the other solutions sound better to me, though with their own caveats (time, work, GTest macros are only for GTest). I'm fine with this, if all other options are not feasible to implement in the near to mid term, but a second non-blocking review from another clang-tidy dev would be best.


Added two more, but there are some other review comments still open, probably because of the discussion.

jvoung and others added 2 commits January 8, 2025 15:14
- lightweight detection of catch2
- style etc.
- add release note about option
@jvoung
Copy link
Contributor Author

jvoung commented Jan 8, 2025

Sorry for the delay.

I think, that we can probably all agree that the best solution would be to have the model support the macros, e.g., by recognizing the patterns that are behind these macros. However, that is currently not something that people can find time to do (as was communicated by @ymand w.r.t. current priorities), and not something that seems to have caused significant enough irritation for someone to step in themselves.

As discussed, disabling the check inside the test folder only works if the project structure supports it (which is IMO the majority, but that is just a feeling), but that is also something users have to know they can do. We should open an issue with the help wanted label, and mention explicitly that this is not simple in the description. That way, someone may be inclined to start working on this.

Thanks, that is a good summary =)

(and sorry for not replying earlier about the test folder alternative, but as @ymand mentioned, our codebase unfortunately doesn't use test folders)

I think the GTest matchers don't work here, because you'd want to mark the optional as having a value or not having a value, and you would need access to the environment at that time, which is not available at this stage. But the GTest macros could be used for a crude implementation inside the model, right?

Yes, the analysis would want to mark the optional as having a value or not in the environment. So modeling would be a combination of the GTest matchers to dispatch to some transfer function, and then the transfer functions will modify the environment appropriately. Similarly for catch2 matchers + transfer functions, etc.

There was also the suggestion from Gabor of adding analyzer_noreturn attributes. But it would also take some time to modify the appropriate libraries and wait for roll out, etc. Or perhaps similarly, if the libraries could be modified to provide super-simple versions of the macro behind ifdef clang_analyzer (e.g., for googletest, skip the Converter ctor + operator bool function crossings), but again take time to see if the libraries are willing, modify, wait for rollout, etc.

I'm not really that opposed to this solution, but the other solutions sound better to me, though with their own caveats (time, work, GTest macros are only for GTest). I'm fine with this, if all other options are not feasible to implement in the near to mid term, but a second non-blocking review from another clang-tidy dev would be best.

Added two more, but there are some other review comments still open, probably because of the discussion.

Ah sorry, yes, I hadn't addressed the earlier review comments since it seemed like bigger picture discussion was still open. Update the patch.

@jvoung
Copy link
Contributor Author

jvoung commented Jan 24, 2025

I'm not really that opposed to this solution, but the other solutions sound better to me, though with their own caveats (time, work, GTest macros are only for GTest). I'm fine with this, if all other options are not feasible to implement in the near to mid term, but a second non-blocking review from another clang-tidy dev would be best.

Just coming back to this, would you mind clarifying "but a second non-blocking review from another clang-tidy dev would be best"? Does ymand and Xazax-hun count?

@jvoung
Copy link
Contributor Author

jvoung commented Jan 24, 2025

It would be better to use GtestCmp if you really want clang-tidy support gtest in this check. Here is an unfinished example which can match

TEST(a, b) {
  std::optional<int> a;
  if (v) {
    a = 1;
  }
  ASSERT_NE(a, std::nullopt);
  a.value();
}
  using namespace ast_matchers;
  auto HasOptionalCallDescendant = hasDescendant(callExpr(callee(cxxMethodDecl(
      ofClass(UncheckedOptionalAccessModel::optionalClassDecl())))));
  Finder->addMatcher(
      decl(anyOf(functionDecl(
                     unless(isExpansionInSystemHeader()),
                     // FIXME: Remove the filter below when lambdas are
                     // well supported by the check.
                     unless(hasDeclContext(cxxRecordDecl(isLambda()))),
                     hasBody(allOf(
                         HasOptionalCallDescendant,
                         unless(hasDescendant(gtestAssert(
                             GtestCmp::Ne,
                             expr(hasType(qualType(hasUnqualifiedDesugaredType(
                                 recordType(hasDeclaration(
                                     UncheckedOptionalAccessModel::
                                         optionalClassDecl())))))),
                             anything())))))),
                 cxxConstructorDecl(hasAnyConstructorInitializer(
                     withInitializer(HasOptionalCallDescendant)))))
          .bind(FuncID),
      this);

Sorry I missed this comment earlier!

I think doing "unless( ... gtestAssert... GtestCmp...)" for filtering this way would be more complex for not much gain, over the current filtering. Is the concern accuracy?

regarding complexity: there are many forms to check if the optional has a value, so doing filters this way (with "unless") we would need to enumerate all of them (gtestExpect, gtestExpectThat, gtestAssertThat, a to-be-added gtestExpectTrue, gtestAssertTrue, might need GtestCmp::Eq as well as Ne, like ASSERT_EQ(a.has_value(), true)). Similarly if we want to filter out catch2.

At that point we may as well handle the matcher + transfer functions to "see through" the macros in the checker itself rather than filtering here in a simple way (but then see prioritization concern).

regarding accuracy: I've run a large scale experiment across our monorepo, and the current method seems accurate w.r.t. filtering test files, test utilities, test base classes, fakes, mock classes, etc. Only 0.4% of the diffs didn't have "test/Test/TEST/..." etc in the path for file name (as a rough check). Among that 0.4% some really were test-only libraries that were less obviously test-only from the file name (golden_generator, sampler, fake, mock, benchmark, etc.), and some were timeout diffs.

@tpudlik
Copy link

tpudlik commented Jul 8, 2025

I would love to see this PR merged; false positives from googletest prevent my project from enabling this powerful check (https://issues.pigweed.dev/issues/405164174). What's the next step? @5chmidti if another clang-tidy dev should review this, could you recommend one?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants