diff --git a/clang/include/clang/Tooling/Transformer/Stencil.h b/clang/include/clang/Tooling/Transformer/Stencil.h index 1b7495eb02622b..249f95b7391dfe 100644 --- a/clang/include/clang/Tooling/Transformer/Stencil.h +++ b/clang/include/clang/Tooling/Transformer/Stencil.h @@ -117,6 +117,38 @@ inline Stencil ifBound(llvm::StringRef Id, llvm::StringRef TrueText, detail::makeStencil(FalseText)); } +/// Chooses between multiple stencils, based on the presence of bound nodes. \p +/// CaseStencils takes a vector of (ID, \c Stencil) pairs and checks each ID in +/// order to see if it's bound to a node. If so, the associated \c Stencil is +/// run and all other cases are ignored. An optional \p DefaultStencil can be +/// provided to be run if all cases are exhausted beacause none of the provided +/// IDs are bound. If no default case is provided and all cases are exhausted, +/// the stencil will fail with error `llvm::errc::result_out_of_range`. +/// +/// For example, say one matches a statement's type with: +/// anyOf( +/// qualType(isInteger()).bind("int"), +/// qualType(realFloatingPointType()).bind("float"), +/// qualType(isAnyCharacter()).bind("char"), +/// booleanType().bind("bool")) +/// +/// Then, one can decide in a stencil how to construct a literal. +/// cat("a = ", +/// selectBound( +/// {{"int", cat("0")}, +/// {"float", cat("0.0")}, +/// {"char", cat("'\\0'")}, +/// {"bool", cat("false")}})) +/// +/// In addition, one could supply a default case for all other types: +/// selectBound( +/// {{"int", cat("0")}, +/// ... +/// {"bool", cat("false")}}, +/// cat("{}")) +Stencil selectBound(std::vector> CaseStencils, + Stencil DefaultStencil = nullptr); + /// Wraps a \c MatchConsumer in a \c Stencil, so that it can be used in a \c /// Stencil. This supports user-defined extensions to the \c Stencil language. Stencil run(MatchConsumer C); diff --git a/clang/lib/Tooling/Transformer/Stencil.cpp b/clang/lib/Tooling/Transformer/Stencil.cpp index 4dc3544bb06db4..8b20ef34c3ff2d 100644 --- a/clang/lib/Tooling/Transformer/Stencil.cpp +++ b/clang/lib/Tooling/Transformer/Stencil.cpp @@ -27,14 +27,15 @@ using namespace clang; using namespace transformer; +using ast_matchers::BoundNodes; using ast_matchers::MatchFinder; using llvm::errc; using llvm::Error; using llvm::Expected; using llvm::StringError; -static llvm::Expected -getNode(const ast_matchers::BoundNodes &Nodes, StringRef Id) { +static llvm::Expected getNode(const BoundNodes &Nodes, + StringRef Id) { auto &NodesMap = Nodes.getMap(); auto It = NodesMap.find(Id); if (It == NodesMap.end()) @@ -366,6 +367,73 @@ class IfBoundStencil : public StencilInterface { } }; +class SelectBoundStencil : public clang::transformer::StencilInterface { + static bool containsNoNullStencils( + const std::vector> &Cases) { + for (const auto &S : Cases) + if (S.second == nullptr) + return false; + return true; + } + +public: + SelectBoundStencil(std::vector> Cases, + Stencil Default) + : CaseStencils(std::move(Cases)), DefaultStencil(std::move(Default)) { + assert(containsNoNullStencils(CaseStencils) && + "cases of selectBound may not be null"); + } + ~SelectBoundStencil() override{}; + + llvm::Error eval(const MatchFinder::MatchResult &match, + std::string *result) const override { + const BoundNodes::IDToNodeMap &NodeMap = match.Nodes.getMap(); + for (const auto &S : CaseStencils) { + if (NodeMap.count(S.first) > 0) { + return S.second->eval(match, result); + } + } + + if (DefaultStencil != nullptr) { + return DefaultStencil->eval(match, result); + } + + llvm::SmallVector CaseIDs; + CaseIDs.reserve(CaseStencils.size()); + for (const auto &S : CaseStencils) + CaseIDs.emplace_back(S.first); + + return llvm::createStringError( + errc::result_out_of_range, + llvm::Twine("selectBound failed: no cases bound and no default: {") + + llvm::join(CaseIDs, ", ") + "}"); + } + + std::string toString() const override { + std::string Buffer; + llvm::raw_string_ostream Stream(Buffer); + Stream << "selectBound({"; + bool First = true; + for (const auto &S : CaseStencils) { + if (First) + First = false; + else + Stream << "}, "; + Stream << "{\"" << S.first << "\", " << S.second->toString(); + } + Stream << "}}"; + if (DefaultStencil != nullptr) { + Stream << ", " << DefaultStencil->toString(); + } + Stream << ")"; + return Stream.str(); + } + +private: + std::vector> CaseStencils; + Stencil DefaultStencil; +}; + class SequenceStencil : public StencilInterface { std::vector Stencils; @@ -462,6 +530,13 @@ Stencil transformer::ifBound(StringRef Id, Stencil TrueStencil, std::move(FalseStencil)); } +Stencil transformer::selectBound( + std::vector> CaseStencils, + Stencil DefaultStencil) { + return std::make_shared(std::move(CaseStencils), + std::move(DefaultStencil)); +} + Stencil transformer::run(MatchConsumer Fn) { return std::make_shared(std::move(Fn)); } diff --git a/clang/unittests/Tooling/StencilTest.cpp b/clang/unittests/Tooling/StencilTest.cpp index 0b5b9691dadc59..6036f8f4fa381d 100644 --- a/clang/unittests/Tooling/StencilTest.cpp +++ b/clang/unittests/Tooling/StencilTest.cpp @@ -201,6 +201,58 @@ TEST_F(StencilTest, IfBoundOpUnbound) { testExpr(Id, "3;", ifBound("other", cat("5"), cat("7")), "7"); } +static auto selectMatcher() { + // The `anything` matcher is not bound, to test for none of the cases + // matching. + return expr(anyOf(integerLiteral().bind("int"), cxxBoolLiteral().bind("bool"), + floatLiteral().bind("float"), anything())); +} + +static auto selectStencil() { + return selectBound({ + {"int", cat("I")}, + {"bool", cat("B")}, + {"bool", cat("redundant")}, + {"float", cat("F")}, + }); +} + +TEST_F(StencilTest, SelectBoundChooseDetectedMatch) { + std::string Input = "3;"; + auto StmtMatch = matchStmt(Input, selectMatcher()); + ASSERT_TRUE(StmtMatch); + EXPECT_THAT_EXPECTED(selectStencil()->eval(StmtMatch->Result), + HasValue(std::string("I"))); +} + +TEST_F(StencilTest, SelectBoundChooseFirst) { + std::string Input = "true;"; + auto StmtMatch = matchStmt(Input, selectMatcher()); + ASSERT_TRUE(StmtMatch); + EXPECT_THAT_EXPECTED(selectStencil()->eval(StmtMatch->Result), + HasValue(std::string("B"))); +} + +TEST_F(StencilTest, SelectBoundDiesOnExhaustedCases) { + std::string Input = "\"string\";"; + auto StmtMatch = matchStmt(Input, selectMatcher()); + ASSERT_TRUE(StmtMatch); + EXPECT_THAT_EXPECTED( + selectStencil()->eval(StmtMatch->Result), + Failed(testing::Property( + &StringError::getMessage, + AllOf(HasSubstr("selectBound failed"), HasSubstr("no default"))))); +} + +TEST_F(StencilTest, SelectBoundSucceedsWithDefault) { + std::string Input = "\"string\";"; + auto StmtMatch = matchStmt(Input, selectMatcher()); + ASSERT_TRUE(StmtMatch); + auto Stencil = selectBound({{"int", cat("I")}}, cat("D")); + EXPECT_THAT_EXPECTED(Stencil->eval(StmtMatch->Result), + HasValue(std::string("D"))); +} + TEST_F(StencilTest, ExpressionOpNoParens) { StringRef Id = "id"; testExpr(Id, "3;", expression(Id), "3"); @@ -674,6 +726,28 @@ TEST(StencilToStringTest, IfBoundOp) { EXPECT_EQ(S->toString(), Expected); } +TEST(StencilToStringTest, SelectBoundOp) { + auto S = selectBound({ + {"int", cat("I")}, + {"float", cat("F")}, + }); + StringRef Expected = R"repr(selectBound({{"int", "I"}, {"float", "F"}}))repr"; + EXPECT_EQ(S->toString(), Expected); +} + +TEST(StencilToStringTest, SelectBoundOpWithOneCase) { + auto S = selectBound({{"int", cat("I")}}); + StringRef Expected = R"repr(selectBound({{"int", "I"}}))repr"; + EXPECT_EQ(S->toString(), Expected); +} + +TEST(StencilToStringTest, SelectBoundOpWithDefault) { + auto S = selectBound({{"int", cat("I")}, {"float", cat("F")}}, cat("D")); + StringRef Expected = + R"cc(selectBound({{"int", "I"}, {"float", "F"}}, "D"))cc"; + EXPECT_EQ(S->toString(), Expected); +} + TEST(StencilToStringTest, RunOp) { auto F1 = [](const MatchResult &R) { return "foo"; }; auto S1 = run(F1);