| //===- 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/AST/ASTTypeTraits.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::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( |
| traverse(ast_type_traits::TK_AsIs, 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(std::string(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(std::string(Condition)), ") ", |
| statement(std::string(Else)), " else ", statement(std::string(Then))); |
| EXPECT_THAT_EXPECTED(Stencil->eval(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(std::string(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(std::string(Id))), "3"); |
| } |
| |
| TEST_F(StencilTest, IfBoundOpBound) { |
| StringRef Id = "id"; |
| testExpr(Id, "3;", ifBound(Id, cat("5"), cat("7")), "5"); |
| } |
| |
| TEST_F(StencilTest, IfBoundOpUnbound) { |
| StringRef Id = "id"; |
| testExpr(Id, "3;", ifBound("other", cat("5"), cat("7")), "7"); |
| } |
| |
| TEST_F(StencilTest, ExpressionOpNoParens) { |
| StringRef Id = "id"; |
| testExpr(Id, "3;", expression(Id), "3"); |
| } |
| |
| // Don't parenthesize a parens expression. |
| TEST_F(StencilTest, ExpressionOpNoParensParens) { |
| StringRef Id = "id"; |
| testExpr(Id, "(3);", expression(Id), "(3)"); |
| } |
| |
| TEST_F(StencilTest, ExpressionOpBinaryOpParens) { |
| StringRef Id = "id"; |
| testExpr(Id, "3+4;", 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;", expression("ACACA"), |
| AllOf(HasSubstr("ACACA"), HasSubstr("not bound"))); |
| } |
| |
| TEST_F(StencilTest, DerefPointer) { |
| StringRef Id = "id"; |
| testExpr(Id, "int *x; x;", deref(Id), "*x"); |
| } |
| |
| TEST_F(StencilTest, DerefBinOp) { |
| StringRef Id = "id"; |
| testExpr(Id, "int *x; x + 1;", deref(Id), "*(x + 1)"); |
| } |
| |
| TEST_F(StencilTest, DerefAddressExpr) { |
| StringRef Id = "id"; |
| testExpr(Id, "int x; &x;", deref(Id), "x"); |
| } |
| |
| TEST_F(StencilTest, AddressOfValue) { |
| StringRef Id = "id"; |
| testExpr(Id, "int x; x;", addressOf(Id), "&x"); |
| } |
| |
| TEST_F(StencilTest, AddressOfDerefExpr) { |
| StringRef Id = "id"; |
| testExpr(Id, "int *x; *x;", addressOf(Id), "x"); |
| } |
| |
| TEST_F(StencilTest, MaybeDerefValue) { |
| StringRef Id = "id"; |
| testExpr(Id, "int x; x;", maybeDeref(Id), "x"); |
| } |
| |
| TEST_F(StencilTest, MaybeDerefPointer) { |
| StringRef Id = "id"; |
| testExpr(Id, "int *x; x;", maybeDeref(Id), "*x"); |
| } |
| |
| TEST_F(StencilTest, MaybeDerefBinOp) { |
| StringRef Id = "id"; |
| testExpr(Id, "int *x; x + 1;", maybeDeref(Id), "*(x + 1)"); |
| } |
| |
| TEST_F(StencilTest, MaybeDerefAddressExpr) { |
| StringRef Id = "id"; |
| testExpr(Id, "int x; &x;", maybeDeref(Id), "x"); |
| } |
| |
| TEST_F(StencilTest, MaybeAddressOfPointer) { |
| StringRef Id = "id"; |
| testExpr(Id, "int *x; x;", maybeAddressOf(Id), "x"); |
| } |
| |
| TEST_F(StencilTest, MaybeAddressOfValue) { |
| StringRef Id = "id"; |
| testExpr(Id, "int x; x;", addressOf(Id), "&x"); |
| } |
| |
| TEST_F(StencilTest, MaybeAddressOfBinOp) { |
| StringRef Id = "id"; |
| testExpr(Id, "int x; x + 1;", maybeAddressOf(Id), "&(x + 1)"); |
| } |
| |
| TEST_F(StencilTest, MaybeAddressOfDerefExpr) { |
| StringRef Id = "id"; |
| testExpr(Id, "int *x; *x;", addressOf(Id), "x"); |
| } |
| |
| TEST_F(StencilTest, AccessOpValue) { |
| StringRef Snippet = R"cc( |
| S x; |
| x; |
| )cc"; |
| StringRef Id = "id"; |
| testExpr(Id, Snippet, access(Id, "field"), "x.field"); |
| } |
| |
| TEST_F(StencilTest, AccessOpValueExplicitText) { |
| StringRef Snippet = R"cc( |
| S x; |
| x; |
| )cc"; |
| StringRef Id = "id"; |
| testExpr(Id, Snippet, access(Id, cat("field")), "x.field"); |
| } |
| |
| TEST_F(StencilTest, AccessOpValueAddress) { |
| StringRef Snippet = R"cc( |
| S x; |
| &x; |
| )cc"; |
| StringRef Id = "id"; |
| testExpr(Id, Snippet, access(Id, "field"), "x.field"); |
| } |
| |
| TEST_F(StencilTest, AccessOpPointer) { |
| StringRef Snippet = R"cc( |
| S *x; |
| x; |
| )cc"; |
| StringRef Id = "id"; |
| testExpr(Id, Snippet, access(Id, "field"), "x->field"); |
| } |
| |
| TEST_F(StencilTest, AccessOpPointerDereference) { |
| StringRef Snippet = R"cc( |
| S *x; |
| *x; |
| )cc"; |
| StringRef Id = "id"; |
| testExpr(Id, Snippet, 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, traverse(ast_type_traits::TK_AsIs, |
| returnStmt(hasReturnValue(ignoringImplicit(memberExpr( |
| hasObjectExpression(expr().bind("obj")))))))); |
| ASSERT_TRUE(StmtMatch); |
| const Stencil Stencil = 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 = 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;", run(SimpleFn), "Bound"); |
| } |
| |
| TEST_F(StencilTest, CatOfMacroRangeSucceeds) { |
| StringRef Snippet = R"cpp( |
| #define MACRO 3.77 |
| double foo(double d); |
| foo(MACRO);)cpp"; |
| |
| auto StmtMatch = |
| matchStmt(Snippet, callExpr(callee(functionDecl(hasName("foo"))), |
| argumentCountIs(1), |
| hasArgument(0, expr().bind("arg")))); |
| ASSERT_TRUE(StmtMatch); |
| Stencil S = cat(node("arg")); |
| EXPECT_THAT_EXPECTED(S->eval(StmtMatch->Result), HasValue("MACRO")); |
| } |
| |
| TEST_F(StencilTest, CatOfMacroArgRangeSucceeds) { |
| StringRef Snippet = R"cpp( |
| #define MACRO(a, b) a + b |
| MACRO(2, 3);)cpp"; |
| |
| auto StmtMatch = |
| matchStmt(Snippet, binaryOperator(hasRHS(expr().bind("rhs")))); |
| ASSERT_TRUE(StmtMatch); |
| Stencil S = cat(node("rhs")); |
| EXPECT_THAT_EXPECTED(S->eval(StmtMatch->Result), HasValue("3")); |
| } |
| |
| TEST_F(StencilTest, CatOfMacroArgSubRangeSucceeds) { |
| StringRef Snippet = R"cpp( |
| #define MACRO(a, b) a + b |
| int foo(int); |
| MACRO(2, foo(3));)cpp"; |
| |
| auto StmtMatch = matchStmt( |
| Snippet, binaryOperator(hasRHS(callExpr( |
| callee(functionDecl(hasName("foo"))), argumentCountIs(1), |
| hasArgument(0, expr().bind("arg")))))); |
| ASSERT_TRUE(StmtMatch); |
| Stencil S = cat(node("arg")); |
| EXPECT_THAT_EXPECTED(S->eval(StmtMatch->Result), HasValue("3")); |
| } |
| |
| TEST_F(StencilTest, CatOfInvalidRangeFails) { |
| StringRef Snippet = R"cpp( |
| #define MACRO (3.77) |
| double foo(double d); |
| foo(MACRO);)cpp"; |
| |
| auto StmtMatch = |
| matchStmt(Snippet, callExpr(callee(functionDecl(hasName("foo"))), |
| argumentCountIs(1), |
| hasArgument(0, expr().bind("arg")))); |
| ASSERT_TRUE(StmtMatch); |
| Stencil S = cat(node("arg")); |
| Expected<std::string> Result = S->eval(StmtMatch->Result); |
| ASSERT_THAT_EXPECTED(Result, Failed<StringError>()); |
| llvm::handleAllErrors(Result.takeError(), [](const llvm::StringError &E) { |
| EXPECT_THAT(E.getMessage(), AllOf(HasSubstr("selected range"), |
| HasSubstr("macro expansion"))); |
| }); |
| } |
| |
| 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 = dPrint("Id"); |
| StringRef Expected = R"repr(dPrint("Id"))repr"; |
| EXPECT_EQ(S->toString(), Expected); |
| } |
| |
| TEST(StencilToStringTest, ExpressionOp) { |
| auto S = expression("Id"); |
| StringRef Expected = R"repr(expression("Id"))repr"; |
| EXPECT_EQ(S->toString(), Expected); |
| } |
| |
| TEST(StencilToStringTest, DerefOp) { |
| auto S = deref("Id"); |
| StringRef Expected = R"repr(deref("Id"))repr"; |
| EXPECT_EQ(S->toString(), Expected); |
| } |
| |
| TEST(StencilToStringTest, AddressOfOp) { |
| auto S = 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, AccessOpText) { |
| auto S = access("Id", "memberData"); |
| StringRef Expected = R"repr(access("Id", "memberData"))repr"; |
| EXPECT_EQ(S->toString(), Expected); |
| } |
| |
| TEST(StencilToStringTest, AccessOpSelector) { |
| auto S = access("Id", cat(name("otherId"))); |
| StringRef Expected = R"repr(access("Id", selection(...)))repr"; |
| EXPECT_EQ(S->toString(), Expected); |
| } |
| |
| TEST(StencilToStringTest, AccessOpStencil) { |
| auto S = access("Id", cat("foo_", "bar")); |
| StringRef Expected = R"repr(access("Id", seq("foo_", "bar")))repr"; |
| EXPECT_EQ(S->toString(), Expected); |
| } |
| |
| TEST(StencilToStringTest, IfBoundOp) { |
| auto S = ifBound("Id", cat("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 = run(F1); |
| EXPECT_EQ(S1->toString(), "run(...)"); |
| } |
| |
| TEST(StencilToStringTest, Sequence) { |
| auto S = cat("foo", access("x", "m()"), "bar", |
| ifBound("x", cat("t"), access("e", "f"))); |
| StringRef Expected = R"repr(seq("foo", access("x", "m()"), "bar", )repr" |
| R"repr(ifBound("x", "t", access("e", "f"))))repr"; |
| EXPECT_EQ(S->toString(), Expected); |
| } |
| |
| TEST(StencilToStringTest, SequenceEmpty) { |
| auto S = cat(); |
| StringRef Expected = "seq()"; |
| EXPECT_EQ(S->toString(), Expected); |
| } |
| |
| TEST(StencilToStringTest, SequenceSingle) { |
| auto S = cat("foo"); |
| StringRef Expected = "\"foo\""; |
| EXPECT_EQ(S->toString(), Expected); |
| } |
| |
| TEST(StencilToStringTest, SequenceFromVector) { |
| auto S = catVector({cat("foo"), access("x", "m()"), cat("bar"), |
| ifBound("x", cat("t"), access("e", "f"))}); |
| StringRef Expected = R"repr(seq("foo", access("x", "m()"), "bar", )repr" |
| R"repr(ifBound("x", "t", access("e", "f"))))repr"; |
| EXPECT_EQ(S->toString(), Expected); |
| } |
| } // namespace |