| //===- unittest/Tooling/StencilTest.cpp -----------------------------------===// |
| // |
| // 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/Tooling/Transformer/Stencil.h" |
| #include "clang/ASTMatchers/ASTMatchers.h" |
| #include "clang/Tooling/FixIt.h" |
| #include "clang/Tooling/Tooling.h" |
| #include "llvm/Support/Error.h" |
| #include "llvm/Testing/Support/Error.h" |
| #include "gmock/gmock.h" |
| #include "gtest/gtest.h" |
| |
| using namespace clang; |
| using namespace transformer; |
| using namespace ast_matchers; |
| |
| namespace { |
| using ::llvm::Failed; |
| using ::llvm::HasValue; |
| using ::llvm::StringError; |
| using ::testing::AllOf; |
| using ::testing::Eq; |
| using ::testing::HasSubstr; |
| using MatchResult = MatchFinder::MatchResult; |
| |
| // Create a valid translation-unit from a statement. |
| static std::string wrapSnippet(StringRef StatementCode) { |
| return ("struct S { int field; }; auto stencil_test_snippet = []{" + |
| StatementCode + "};") |
| .str(); |
| } |
| |
| static DeclarationMatcher wrapMatcher(const StatementMatcher &Matcher) { |
| return varDecl(hasName("stencil_test_snippet"), |
| hasDescendant(compoundStmt(hasAnySubstatement(Matcher)))); |
| } |
| |
| struct TestMatch { |
| // The AST unit from which `result` is built. We bundle it because it backs |
| // the result. Users are not expected to access it. |
| std::unique_ptr<ASTUnit> AstUnit; |
| // The result to use in the test. References `ast_unit`. |
| MatchResult Result; |
| }; |
| |
| // Matches `Matcher` against the statement `StatementCode` and returns the |
| // result. Handles putting the statement inside a function and modifying the |
| // matcher correspondingly. `Matcher` should match one of the statements in |
| // `StatementCode` exactly -- that is, produce exactly one match. However, |
| // `StatementCode` may contain other statements not described by `Matcher`. |
| static llvm::Optional<TestMatch> matchStmt(StringRef StatementCode, |
| StatementMatcher Matcher) { |
| auto AstUnit = tooling::buildASTFromCode(wrapSnippet(StatementCode)); |
| if (AstUnit == nullptr) { |
| ADD_FAILURE() << "AST construction failed"; |
| return llvm::None; |
| } |
| ASTContext &Context = AstUnit->getASTContext(); |
| auto Matches = ast_matchers::match(wrapMatcher(Matcher), Context); |
| // We expect a single, exact match for the statement. |
| if (Matches.size() != 1) { |
| ADD_FAILURE() << "Wrong number of matches: " << Matches.size(); |
| return llvm::None; |
| } |
| return TestMatch{std::move(AstUnit), MatchResult(Matches[0], &Context)}; |
| } |
| |
| class StencilTest : public ::testing::Test { |
| protected: |
| // Verifies that the given stencil fails when evaluated on a valid match |
| // result. Binds a statement to "stmt", a (non-member) ctor-initializer to |
| // "init", an expression to "expr" and a (nameless) declaration to "decl". |
| void testError(const Stencil &Stencil, |
| ::testing::Matcher<std::string> Matcher) { |
| const std::string Snippet = R"cc( |
| struct A {}; |
| class F : public A { |
| public: |
| F(int) {} |
| }; |
| F(1); |
| )cc"; |
| auto StmtMatch = matchStmt( |
| Snippet, |
| stmt(hasDescendant( |
| cxxConstructExpr( |
| hasDeclaration(decl(hasDescendant(cxxCtorInitializer( |
| isBaseInitializer()) |
| .bind("init"))) |
| .bind("decl"))) |
| .bind("expr"))) |
| .bind("stmt")); |
| ASSERT_TRUE(StmtMatch); |
| if (auto ResultOrErr = Stencil.eval(StmtMatch->Result)) { |
| ADD_FAILURE() << "Expected failure but succeeded: " << *ResultOrErr; |
| } else { |
| auto Err = llvm::handleErrors(ResultOrErr.takeError(), |
| [&Matcher](const StringError &Err) { |
| EXPECT_THAT(Err.getMessage(), Matcher); |
| }); |
| if (Err) { |
| ADD_FAILURE() << "Unhandled error: " << llvm::toString(std::move(Err)); |
| } |
| } |
| } |
| |
| // Tests failures caused by references to unbound nodes. `unbound_id` is the |
| // id that will cause the failure. |
| void testUnboundNodeError(const Stencil &Stencil, StringRef UnboundId) { |
| testError(Stencil, AllOf(HasSubstr(UnboundId), HasSubstr("not bound"))); |
| } |
| }; |
| |
| TEST_F(StencilTest, SingleStatement) { |
| StringRef Condition("C"), Then("T"), Else("E"); |
| const std::string Snippet = R"cc( |
| if (true) |
| return 1; |
| else |
| return 0; |
| )cc"; |
| auto StmtMatch = matchStmt( |
| Snippet, ifStmt(hasCondition(expr().bind(Condition)), |
| hasThen(stmt().bind(Then)), hasElse(stmt().bind(Else)))); |
| ASSERT_TRUE(StmtMatch); |
| // Invert the if-then-else. |
| auto Stencil = cat("if (!", node(Condition), ") ", statement(Else), " else ", |
| statement(Then)); |
| EXPECT_THAT_EXPECTED(Stencil.eval(StmtMatch->Result), |
| HasValue("if (!true) return 0; else return 1;")); |
| } |
| |
| TEST_F(StencilTest, SingleStatementCallOperator) { |
| StringRef Condition("C"), Then("T"), Else("E"); |
| const std::string Snippet = R"cc( |
| if (true) |
| return 1; |
| else |
| return 0; |
| )cc"; |
| auto StmtMatch = matchStmt( |
| Snippet, ifStmt(hasCondition(expr().bind(Condition)), |
| hasThen(stmt().bind(Then)), hasElse(stmt().bind(Else)))); |
| ASSERT_TRUE(StmtMatch); |
| // Invert the if-then-else. |
| Stencil S = cat("if (!", node(Condition), ") ", statement(Else), " else ", |
| statement(Then)); |
| EXPECT_THAT_EXPECTED(S(StmtMatch->Result), |
| HasValue("if (!true) return 0; else return 1;")); |
| } |
| |
| TEST_F(StencilTest, UnboundNode) { |
| const std::string Snippet = R"cc( |
| if (true) |
| return 1; |
| else |
| return 0; |
| )cc"; |
| auto StmtMatch = matchStmt(Snippet, ifStmt(hasCondition(stmt().bind("a1")), |
| hasThen(stmt().bind("a2")))); |
| ASSERT_TRUE(StmtMatch); |
| auto Stencil = cat("if(!", node("a1"), ") ", node("UNBOUND"), ";"); |
| auto ResultOrErr = Stencil.eval(StmtMatch->Result); |
| EXPECT_TRUE(llvm::errorToBool(ResultOrErr.takeError())) |
| << "Expected unbound node, got " << *ResultOrErr; |
| } |
| |
| // Tests that a stencil with a single parameter (`Id`) evaluates to the expected |
| // string, when `Id` is bound to the expression-statement in `Snippet`. |
| void testExpr(StringRef Id, StringRef Snippet, const Stencil &Stencil, |
| StringRef Expected) { |
| auto StmtMatch = matchStmt(Snippet, expr().bind(Id)); |
| ASSERT_TRUE(StmtMatch); |
| EXPECT_THAT_EXPECTED(Stencil.eval(StmtMatch->Result), HasValue(Expected)); |
| } |
| |
| void testFailure(StringRef Id, StringRef Snippet, const Stencil &Stencil, |
| testing::Matcher<std::string> MessageMatcher) { |
| auto StmtMatch = matchStmt(Snippet, expr().bind(Id)); |
| ASSERT_TRUE(StmtMatch); |
| EXPECT_THAT_EXPECTED(Stencil.eval(StmtMatch->Result), |
| Failed<StringError>(testing::Property( |
| &StringError::getMessage, MessageMatcher))); |
| } |
| |
| TEST_F(StencilTest, SelectionOp) { |
| StringRef Id = "id"; |
| testExpr(Id, "3;", cat(node(Id)), "3"); |
| } |
| |
| TEST_F(StencilTest, IfBoundOpBound) { |
| StringRef Id = "id"; |
| testExpr(Id, "3;", cat(ifBound(Id, text("5"), text("7"))), "5"); |
| } |
| |
| TEST_F(StencilTest, IfBoundOpUnbound) { |
| StringRef Id = "id"; |
| testExpr(Id, "3;", cat(ifBound("other", text("5"), text("7"))), "7"); |
| } |
| |
| TEST_F(StencilTest, ExpressionOpNoParens) { |
| StringRef Id = "id"; |
| testExpr(Id, "3;", cat(expression(Id)), "3"); |
| } |
| |
| // Don't parenthesize a parens expression. |
| TEST_F(StencilTest, ExpressionOpNoParensParens) { |
| StringRef Id = "id"; |
| testExpr(Id, "(3);", cat(expression(Id)), "(3)"); |
| } |
| |
| TEST_F(StencilTest, ExpressionOpBinaryOpParens) { |
| StringRef Id = "id"; |
| testExpr(Id, "3+4;", cat(expression(Id)), "(3+4)"); |
| } |
| |
| // `expression` shares code with other ops, so we get sufficient coverage of the |
| // error handling code with this test. If that changes in the future, more error |
| // tests should be added. |
| TEST_F(StencilTest, ExpressionOpUnbound) { |
| StringRef Id = "id"; |
| testFailure(Id, "3;", cat(expression("ACACA")), |
| AllOf(HasSubstr("ACACA"), HasSubstr("not bound"))); |
| } |
| |
| TEST_F(StencilTest, DerefPointer) { |
| StringRef Id = "id"; |
| testExpr(Id, "int *x; x;", cat(deref(Id)), "*x"); |
| } |
| |
| TEST_F(StencilTest, DerefBinOp) { |
| StringRef Id = "id"; |
| testExpr(Id, "int *x; x + 1;", cat(deref(Id)), "*(x + 1)"); |
| } |
| |
| TEST_F(StencilTest, DerefAddressExpr) { |
| StringRef Id = "id"; |
| testExpr(Id, "int x; &x;", cat(deref(Id)), "x"); |
| } |
| |
| TEST_F(StencilTest, AddressOfValue) { |
| StringRef Id = "id"; |
| testExpr(Id, "int x; x;", cat(addressOf(Id)), "&x"); |
| } |
| |
| TEST_F(StencilTest, AddressOfDerefExpr) { |
| StringRef Id = "id"; |
| testExpr(Id, "int *x; *x;", cat(addressOf(Id)), "x"); |
| } |
| |
| TEST_F(StencilTest, AccessOpValue) { |
| StringRef Snippet = R"cc( |
| S x; |
| x; |
| )cc"; |
| StringRef Id = "id"; |
| testExpr(Id, Snippet, cat(access(Id, "field")), "x.field"); |
| } |
| |
| TEST_F(StencilTest, AccessOpValueExplicitText) { |
| StringRef Snippet = R"cc( |
| S x; |
| x; |
| )cc"; |
| StringRef Id = "id"; |
| testExpr(Id, Snippet, cat(access(Id, text("field"))), "x.field"); |
| } |
| |
| TEST_F(StencilTest, AccessOpValueAddress) { |
| StringRef Snippet = R"cc( |
| S x; |
| &x; |
| )cc"; |
| StringRef Id = "id"; |
| testExpr(Id, Snippet, cat(access(Id, "field")), "x.field"); |
| } |
| |
| TEST_F(StencilTest, AccessOpPointer) { |
| StringRef Snippet = R"cc( |
| S *x; |
| x; |
| )cc"; |
| StringRef Id = "id"; |
| testExpr(Id, Snippet, cat(access(Id, "field")), "x->field"); |
| } |
| |
| TEST_F(StencilTest, AccessOpPointerDereference) { |
| StringRef Snippet = R"cc( |
| S *x; |
| *x; |
| )cc"; |
| StringRef Id = "id"; |
| testExpr(Id, Snippet, cat(access(Id, "field")), "x->field"); |
| } |
| |
| TEST_F(StencilTest, AccessOpExplicitThis) { |
| using clang::ast_matchers::hasObjectExpression; |
| using clang::ast_matchers::memberExpr; |
| |
| // Set up the code so we can bind to a use of this. |
| StringRef Snippet = R"cc( |
| class C { |
| public: |
| int x; |
| int foo() { return this->x; } |
| }; |
| )cc"; |
| auto StmtMatch = |
| matchStmt(Snippet, returnStmt(hasReturnValue(ignoringImplicit(memberExpr( |
| hasObjectExpression(expr().bind("obj"))))))); |
| ASSERT_TRUE(StmtMatch); |
| const Stencil Stencil = cat(access("obj", "field")); |
| EXPECT_THAT_EXPECTED(Stencil.eval(StmtMatch->Result), |
| HasValue("this->field")); |
| } |
| |
| TEST_F(StencilTest, AccessOpImplicitThis) { |
| using clang::ast_matchers::hasObjectExpression; |
| using clang::ast_matchers::memberExpr; |
| |
| // Set up the code so we can bind to a use of (implicit) this. |
| StringRef Snippet = R"cc( |
| class C { |
| public: |
| int x; |
| int foo() { return x; } |
| }; |
| )cc"; |
| auto StmtMatch = |
| matchStmt(Snippet, returnStmt(hasReturnValue(ignoringImplicit(memberExpr( |
| hasObjectExpression(expr().bind("obj"))))))); |
| ASSERT_TRUE(StmtMatch); |
| const Stencil Stencil = cat(access("obj", "field")); |
| EXPECT_THAT_EXPECTED(Stencil.eval(StmtMatch->Result), HasValue("field")); |
| } |
| |
| TEST_F(StencilTest, RunOp) { |
| StringRef Id = "id"; |
| auto SimpleFn = [Id](const MatchResult &R) { |
| return std::string(R.Nodes.getNodeAs<Stmt>(Id) != nullptr ? "Bound" |
| : "Unbound"); |
| }; |
| testExpr(Id, "3;", cat(run(SimpleFn)), "Bound"); |
| } |
| |
| TEST(StencilToStringTest, RawTextOp) { |
| auto S = cat("foo bar baz"); |
| StringRef Expected = R"("foo bar baz")"; |
| EXPECT_EQ(S.toString(), Expected); |
| } |
| |
| TEST(StencilToStringTest, RawTextOpEscaping) { |
| auto S = cat("foo \"bar\" baz\\n"); |
| StringRef Expected = R"("foo \"bar\" baz\\n")"; |
| EXPECT_EQ(S.toString(), Expected); |
| } |
| |
| TEST(StencilToStringTest, DebugPrintNodeOp) { |
| auto S = cat(dPrint("Id")); |
| StringRef Expected = R"repr(dPrint("Id"))repr"; |
| EXPECT_EQ(S.toString(), Expected); |
| } |
| |
| TEST(StencilToStringTest, ExpressionOp) { |
| auto S = cat(expression("Id")); |
| StringRef Expected = R"repr(expression("Id"))repr"; |
| EXPECT_EQ(S.toString(), Expected); |
| } |
| |
| TEST(StencilToStringTest, DerefOp) { |
| auto S = cat(deref("Id")); |
| StringRef Expected = R"repr(deref("Id"))repr"; |
| EXPECT_EQ(S.toString(), Expected); |
| } |
| |
| TEST(StencilToStringTest, AddressOfOp) { |
| auto S = cat(addressOf("Id")); |
| StringRef Expected = R"repr(addressOf("Id"))repr"; |
| EXPECT_EQ(S.toString(), Expected); |
| } |
| |
| TEST(StencilToStringTest, SelectionOp) { |
| auto S1 = cat(node("node1")); |
| EXPECT_EQ(S1.toString(), "selection(...)"); |
| } |
| |
| TEST(StencilToStringTest, AccessOp) { |
| auto S = cat(access("Id", text("memberData"))); |
| StringRef Expected = R"repr(access("Id", "memberData"))repr"; |
| EXPECT_EQ(S.toString(), Expected); |
| } |
| |
| TEST(StencilToStringTest, AccessOpStencilPart) { |
| auto S = cat(access("Id", access("subId", "memberData"))); |
| StringRef Expected = R"repr(access("Id", access("subId", "memberData")))repr"; |
| EXPECT_EQ(S.toString(), Expected); |
| } |
| |
| TEST(StencilToStringTest, IfBoundOp) { |
| auto S = cat(ifBound("Id", text("trueText"), access("exprId", "memberData"))); |
| StringRef Expected = |
| R"repr(ifBound("Id", "trueText", access("exprId", "memberData")))repr"; |
| EXPECT_EQ(S.toString(), Expected); |
| } |
| |
| TEST(StencilToStringTest, RunOp) { |
| auto F1 = [](const MatchResult &R) { return "foo"; }; |
| auto S1 = cat(run(F1)); |
| EXPECT_EQ(S1.toString(), "run(...)"); |
| } |
| |
| TEST(StencilToStringTest, MultipleOp) { |
| auto S = cat("foo", access("x", "m()"), "bar", |
| ifBound("x", text("t"), access("e", "f"))); |
| StringRef Expected = R"repr("foo", access("x", "m()"), "bar", )repr" |
| R"repr(ifBound("x", "t", access("e", "f")))repr"; |
| EXPECT_EQ(S.toString(), Expected); |
| } |
| } // namespace |