From 23444edf30ba00ccefa3a582ac7ddc29774e9da5 Mon Sep 17 00:00:00 2001 From: Yitzhak Mandelbaum Date: Wed, 19 Feb 2020 11:04:51 -0500 Subject: [PATCH] [AST matchers] Add basic matchers for googletest EXPECT/ASSERT calls. Summary: This revision adds matchers that match calls to the gtest EXPECT and ASSERT macros almost like function calls. The matchers are placed in separate files (GtestMatchers...), because they are specific to the gtest library. Reviewers: gribozavr2 Subscribers: mgorny, cfe-commits Tags: #clang Differential Revision: https://reviews.llvm.org/D74840 --- .../include/clang/ASTMatchers/GtestMatchers.h | 45 +++++ clang/lib/ASTMatchers/CMakeLists.txt | 1 + clang/lib/ASTMatchers/GtestMatchers.cpp | 101 +++++++++ clang/unittests/ASTMatchers/CMakeLists.txt | 1 + .../ASTMatchers/GtestMatchersTest.cpp | 191 ++++++++++++++++++ 5 files changed, 339 insertions(+) create mode 100644 clang/include/clang/ASTMatchers/GtestMatchers.h create mode 100644 clang/lib/ASTMatchers/GtestMatchers.cpp create mode 100644 clang/unittests/ASTMatchers/GtestMatchersTest.cpp diff --git a/clang/include/clang/ASTMatchers/GtestMatchers.h b/clang/include/clang/ASTMatchers/GtestMatchers.h new file mode 100644 index 000000000000..4f8addcf744a --- /dev/null +++ b/clang/include/clang/ASTMatchers/GtestMatchers.h @@ -0,0 +1,45 @@ +//===- GtestMatchers.h - AST Matchers for GTest -----------------*- 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 implements matchers specific to structures in the Googletest +// (gtest) framework. +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_ASTMATCHERS_GTESTMATCHERS_H +#define LLVM_CLANG_ASTMATCHERS_GTESTMATCHERS_H + +#include "clang/AST/Stmt.h" +#include "clang/ASTMatchers/ASTMatchers.h" + +namespace clang { +namespace ast_matchers { + +/// Gtest's comparison operations. +enum class GtestCmp { + Eq, + Ne, + Ge, + Gt, + Le, + Lt, +}; + +/// Matcher for gtest's ASSERT_... macros. +internal::BindableMatcher gtestAssert(GtestCmp Cmp, StatementMatcher Left, + StatementMatcher Right); + +/// Matcher for gtest's EXPECT_... macros. +internal::BindableMatcher gtestExpect(GtestCmp Cmp, StatementMatcher Left, + StatementMatcher Right); + +} // namespace ast_matchers +} // namespace clang + +#endif // LLVM_CLANG_ASTMATCHERS_GTESTMATCHERS_H + diff --git a/clang/lib/ASTMatchers/CMakeLists.txt b/clang/lib/ASTMatchers/CMakeLists.txt index cd88d1db9ce4..8f700ca3226b 100644 --- a/clang/lib/ASTMatchers/CMakeLists.txt +++ b/clang/lib/ASTMatchers/CMakeLists.txt @@ -5,6 +5,7 @@ set(LLVM_LINK_COMPONENTS support) add_clang_library(clangASTMatchers ASTMatchFinder.cpp ASTMatchersInternal.cpp + GtestMatchers.cpp LINK_LIBS clangAST diff --git a/clang/lib/ASTMatchers/GtestMatchers.cpp b/clang/lib/ASTMatchers/GtestMatchers.cpp new file mode 100644 index 000000000000..317bddd035f8 --- /dev/null +++ b/clang/lib/ASTMatchers/GtestMatchers.cpp @@ -0,0 +1,101 @@ +//===- GtestMatchers.cpp - AST Matchers for Gtest ---------------*- 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 +// +//===----------------------------------------------------------------------===// + +#include "clang/ASTMatchers/GtestMatchers.h" +#include "clang/ASTMatchers/ASTMatchFinder.h" +#include "clang/AST/ASTConsumer.h" +#include "clang/AST/ASTContext.h" +#include "clang/AST/RecursiveASTVisitor.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/StringMap.h" +#include "llvm/Support/Timer.h" +#include +#include +#include + +namespace clang { +namespace ast_matchers { + +static DeclarationMatcher getComparisonDecl(GtestCmp Cmp) { + switch (Cmp) { + case GtestCmp::Eq: + return cxxMethodDecl(hasName("Compare"), + ofClass(cxxRecordDecl(isSameOrDerivedFrom( + hasName("::testing::internal::EqHelper"))))); + case GtestCmp::Ne: + return functionDecl(hasName("::testing::internal::CmpHelperNE")); + case GtestCmp::Ge: + return functionDecl(hasName("::testing::internal::CmpHelperGE")); + case GtestCmp::Gt: + return functionDecl(hasName("::testing::internal::CmpHelperGT")); + case GtestCmp::Le: + return functionDecl(hasName("::testing::internal::CmpHelperLE")); + case GtestCmp::Lt: + return functionDecl(hasName("::testing::internal::CmpHelperLT")); + } +} + +static llvm::StringRef getAssertMacro(GtestCmp Cmp) { + switch (Cmp) { + case GtestCmp::Eq: + return "ASSERT_EQ"; + case GtestCmp::Ne: + return "ASSERT_NE"; + case GtestCmp::Ge: + return "ASSERT_GE"; + case GtestCmp::Gt: + return "ASSERT_GT"; + case GtestCmp::Le: + return "ASSERT_LE"; + case GtestCmp::Lt: + return "ASSERT_LT"; + } +} + +static llvm::StringRef getExpectMacro(GtestCmp Cmp) { + switch (Cmp) { + case GtestCmp::Eq: + return "EXPECT_EQ"; + case GtestCmp::Ne: + return "EXPECT_NE"; + case GtestCmp::Ge: + return "EXPECT_GE"; + case GtestCmp::Gt: + return "EXPECT_GT"; + case GtestCmp::Le: + return "EXPECT_LE"; + case GtestCmp::Lt: + return "EXPECT_LT"; + } +} + +// In general, AST matchers cannot match calls to macros. However, we can +// simulate such matches if the macro definition has identifiable elements that +// themselves can be matched. In that case, we can match on those elements and +// then check that the match occurs within an expansion of the desired +// macro. The more uncommon the identified elements, the more efficient this +// process will be. +// +// We use this approach to implement the derived matchers gtestAssert and +// gtestExpect. +internal::BindableMatcher gtestAssert(GtestCmp Cmp, StatementMatcher Left, + StatementMatcher Right) { + return callExpr(callee(getComparisonDecl(Cmp)), + isExpandedFromMacro(getAssertMacro(Cmp)), + hasArgument(2, Left), hasArgument(3, Right)); +} + +internal::BindableMatcher gtestExpect(GtestCmp Cmp, StatementMatcher Left, + StatementMatcher Right) { + return callExpr(callee(getComparisonDecl(Cmp)), + isExpandedFromMacro(getExpectMacro(Cmp)), + hasArgument(2, Left), hasArgument(3, Right)); +} + +} // end namespace ast_matchers +} // end namespace clang diff --git a/clang/unittests/ASTMatchers/CMakeLists.txt b/clang/unittests/ASTMatchers/CMakeLists.txt index 09c4290fa1d2..aa5438c947f4 100644 --- a/clang/unittests/ASTMatchers/CMakeLists.txt +++ b/clang/unittests/ASTMatchers/CMakeLists.txt @@ -16,6 +16,7 @@ add_clang_unittest(ASTMatchersTests ASTMatchersNodeTest.cpp ASTMatchersNarrowingTest.cpp ASTMatchersTraversalTest.cpp + GtestMatchersTest.cpp ) clang_target_link_libraries(ASTMatchersTests diff --git a/clang/unittests/ASTMatchers/GtestMatchersTest.cpp b/clang/unittests/ASTMatchers/GtestMatchersTest.cpp new file mode 100644 index 000000000000..e5abb24cb4ab --- /dev/null +++ b/clang/unittests/ASTMatchers/GtestMatchersTest.cpp @@ -0,0 +1,191 @@ +//===- unittests/ASTMatchers/GTestMatchersTest.cpp - GTest matcher unit tests // +// +// 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 "ASTMatchersTest.h" +#include "clang/ASTMatchers/ASTMatchers.h" +#include "clang/ASTMatchers/GtestMatchers.h" + +namespace clang { +namespace ast_matchers { + +constexpr llvm::StringLiteral GtestMockDecls = R"cc( + static int testerr; + +#define GTEST_AMBIGUOUS_ELSE_BLOCKER_ \ + switch (0) \ + case 0: \ + default: // NOLINT + +#define GTEST_NONFATAL_FAILURE_(code) testerr = code + +#define GTEST_FATAL_FAILURE_(code) testerr = code + +#define GTEST_ASSERT_(expression, on_failure) \ + GTEST_AMBIGUOUS_ELSE_BLOCKER_ \ + if (const int gtest_ar = (expression)) \ + ; \ + else \ + on_failure(gtest_ar) + + // Internal macro for implementing {EXPECT|ASSERT}_PRED_FORMAT2. + // Don't use this in your code. +#define GTEST_PRED_FORMAT2_(pred_format, v1, v2, on_failure) \ + GTEST_ASSERT_(pred_format(#v1, #v2, v1, v2), on_failure) + +#define ASSERT_PRED_FORMAT2(pred_format, v1, v2) \ + GTEST_PRED_FORMAT2_(pred_format, v1, v2, GTEST_FATAL_FAILURE_) +#define EXPECT_PRED_FORMAT2(pred_format, v1, v2) \ + GTEST_PRED_FORMAT2_(pred_format, v1, v2, GTEST_NONFATAL_FAILURE_) + +#define EXPECT_EQ(val1, val2) \ + EXPECT_PRED_FORMAT2(::testing::internal::EqHelper::Compare, val1, val2) +#define EXPECT_NE(val1, val2) \ + EXPECT_PRED_FORMAT2(::testing::internal::CmpHelperNE, val1, val2) +#define EXPECT_GE(val1, val2) \ + EXPECT_PRED_FORMAT2(::testing::internal::CmpHelperGE, val1, val2) +#define EXPECT_GT(val1, val2) \ + EXPECT_PRED_FORMAT2(::testing::internal::CmpHelperGT, val1, val2) +#define EXPECT_LE(val1, val2) \ + EXPECT_PRED_FORMAT2(::testing::internal::CmpHelperLE, val1, val2) +#define EXPECT_LT(val1, val2) \ + EXPECT_PRED_FORMAT2(::testing::internal::CmpHelperLT, val1, val2) + +#define ASSERT_EQ(val1, val2) \ + ASSERT_PRED_FORMAT2(::testing::internal::EqHelper::Compare, val1, val2) +#define ASSERT_NE(val1, val2) \ + ASSERT_PRED_FORMAT2(::testing::internal::CmpHelperNE, val1, val2) + + namespace testing { + namespace internal { + class EqHelper { + public: + // This templatized version is for the general case. + template + static int Compare(const char* lhs_expression, const char* rhs_expression, + const T1& lhs, const T2& rhs) { + return 0; + } + }; + template + int CmpHelperNE(const char* expr1, const char* expr2, const T1& val1, + const T2& val2) { + return 0; + } + template + int CmpHelperGE(const char* expr1, const char* expr2, const T1& val1, + const T2& val2) { + return 0; + } + template + int CmpHelperGT(const char* expr1, const char* expr2, const T1& val1, + const T2& val2) { + return 0; + } + template + int CmpHelperLE(const char* expr1, const char* expr2, const T1& val1, + const T2& val2) { + return 0; + } + template + int CmpHelperLT(const char* expr1, const char* expr2, const T1& val1, + const T2& val2) { + return 0; + } + } // namespace internal + } // namespace testing +)cc"; + +static std::string wrapGtest(llvm::StringRef Input) { + return (GtestMockDecls + Input).str(); +} + +TEST(GtestAssertTest, ShouldMatchAssert) { + std::string Input = R"cc( + void Test() { ASSERT_EQ(1010, 4321); } + )cc"; + EXPECT_TRUE(matches(wrapGtest(Input), + gtestAssert(GtestCmp::Eq, integerLiteral(equals(1010)), + integerLiteral(equals(4321))))); +} + +TEST(GtestAssertTest, ShouldNotMatchExpect) { + std::string Input = R"cc( + void Test() { EXPECT_EQ(2, 3); } + )cc"; + EXPECT_TRUE( + notMatches(wrapGtest(Input), gtestAssert(GtestCmp::Eq, expr(), expr()))); +} + +TEST(GtestAssertTest, ShouldMatchNestedAssert) { + std::string Input = R"cc( + #define WRAPPER(a, b) ASSERT_EQ(a, b) + void Test() { WRAPPER(2, 3); } + )cc"; + EXPECT_TRUE( + matches(wrapGtest(Input), gtestAssert(GtestCmp::Eq, expr(), expr()))); +} + +TEST(GtestExpectTest, ShouldMatchExpect) { + std::string Input = R"cc( + void Test() { EXPECT_EQ(1010, 4321); } + )cc"; + EXPECT_TRUE(matches(wrapGtest(Input), + gtestExpect(GtestCmp::Eq, integerLiteral(equals(1010)), + integerLiteral(equals(4321))))); +} + +TEST(GtestExpectTest, ShouldNotMatchAssert) { + std::string Input = R"cc( + void Test() { ASSERT_EQ(2, 3); } + )cc"; + EXPECT_TRUE( + notMatches(wrapGtest(Input), gtestExpect(GtestCmp::Eq, expr(), expr()))); +} + +TEST(GtestExpectTest, NeShouldMatchExpectNe) { + std::string Input = R"cc( + void Test() { EXPECT_NE(2, 3); } + )cc"; + EXPECT_TRUE( + matches(wrapGtest(Input), gtestExpect(GtestCmp::Ne, expr(), expr()))); +} + +TEST(GtestExpectTest, LeShouldMatchExpectLe) { + std::string Input = R"cc( + void Test() { EXPECT_LE(2, 3); } + )cc"; + EXPECT_TRUE( + matches(wrapGtest(Input), gtestExpect(GtestCmp::Le, expr(), expr()))); +} + +TEST(GtestExpectTest, LtShouldMatchExpectLt) { + std::string Input = R"cc( + void Test() { EXPECT_LT(2, 3); } + )cc"; + EXPECT_TRUE( + matches(wrapGtest(Input), gtestExpect(GtestCmp::Lt, expr(), expr()))); +} + +TEST(GtestExpectTest, GeShouldMatchExpectGe) { + std::string Input = R"cc( + void Test() { EXPECT_GE(2, 3); } + )cc"; + EXPECT_TRUE( + matches(wrapGtest(Input), gtestExpect(GtestCmp::Ge, expr(), expr()))); +} + +TEST(GtestExpectTest, GtShouldMatchExpectGt) { + std::string Input = R"cc( + void Test() { EXPECT_GT(2, 3); } + )cc"; + EXPECT_TRUE( + matches(wrapGtest(Input), gtestExpect(GtestCmp::Gt, expr(), expr()))); +} + +} // end namespace ast_matchers +} // end namespace clang