//===- 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; // 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 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 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 MessageMatcher) { auto StmtMatch = matchStmt(Snippet, expr().bind(Id)); ASSERT_TRUE(StmtMatch); EXPECT_THAT_EXPECTED(Stencil.eval(StmtMatch->Result), Failed(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(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