Index: include/clang/Tooling/Refactoring/Transformer.h =================================================================== --- include/clang/Tooling/Refactoring/Transformer.h +++ include/clang/Tooling/Refactoring/Transformer.h @@ -0,0 +1,210 @@ +//===--- Transformer.h - Clang source-rewriting library ---------*- 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 +// +//===----------------------------------------------------------------------===// +/// +/// \file +/// Defines a library supporting the concise specification of clang-based +/// source-to-source transformations. +/// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_TOOLING_REFACTOR_TRANSFORMER_H_ +#define LLVM_CLANG_TOOLING_REFACTOR_TRANSFORMER_H_ + +#include "clang/ASTMatchers/ASTMatchFinder.h" +#include "clang/ASTMatchers/ASTMatchers.h" +#include "clang/ASTMatchers/ASTMatchersInternal.h" +#include "clang/Tooling/Refactoring/AtomicChange.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/Support/Error.h" +#include +#include +#include +#include +#include +#include + +namespace clang { +namespace tooling { +/// Determines the part of the AST node to replace. We support this to work +/// around the fact that the AST does not differentiate various syntactic +/// elements into their own nodes, so users can specify them relative to a node, +/// instead. +enum class NodePart { + /// The node itself. + Node, + /// Given a \c MemberExpr, selects the member's token. + Member, + /// Given a \c NamedDecl or \c CxxCtorInitializer, selects that token of the + /// relevant name, not including qualifiers. + Name, +}; + +using TextGenerator = + std::function; + +/// Description of a source-code transformation. +// +// A *rewrite rule* describes a transformation of source code. It has the +// following components: +// +// * Matcher: the pattern term, expressed as clang matchers (with Transformer +// extensions). +// +// * Target: the source code impacted by the rule. This identifies an AST node, +// or part thereof (\c TargetPart), whose source range indicates the extent of +// the replacement applied by the replacement term. By default, the extent is +// the node matched by the pattern term (\c NodePart::Node). Target's are +// typed (\c TargetKind), which guides the determination of the node extent +// and might, in the future, statically constrain the set of eligible +// NodeParts for a given node. +// +// * Replacement: a function that produces a replacement string for the target, +// based on the match result. +// +// * Explanation: explanation of the rewrite. This will be displayed to the +// user, where possible (for example, in clang-tidy fix descriptions). +// +// Rules have an additional, implicit, component: the parameters. These are +// portions of the pattern which are left unspecified, yet named so that we can +// reference them in the replacement term. The structure of parameters can be +// partially or even fully specified, in which case they serve just to identify +// matched nodes for later reference rather than abstract over portions of the +// AST. However, in all cases, we refer to named portions of the pattern as +// parameters. +// +// RewriteRule is constructed in a "fluent" style, by creating a builder and +// chaining setters of individual components. +// \code +// RewriteRule MyRule = buildRule(functionDecl(...)).replaceWith(...); +// \endcode +// +// The \c Transformer class should then be used to apply the rewrite rule and +// obtain the corresponding replacements. +struct RewriteRule { + // `Matcher` describes the context of this rule. It should always be bound to + // at least `RootId`. The builder class below takes care of this + // binding. Here, we bind it to a trivial Matcher to enable the default + // constructor, since DynTypedMatcher has no default constructor. + ast_matchers::internal::DynTypedMatcher Matcher = ast_matchers::stmt(); + // The (bound) id of the node whose source will be replaced. This id should + // never be the empty string. + std::string Target; + ast_type_traits::ASTNodeKind TargetKind; + NodePart TargetPart; + TextGenerator Replacement; + TextGenerator Explanation; + + // Id used as the default target of each match. The node described by the + // matcher is guaranteed to be bound to this id, for all rewrite rules + // constructed with the builder class. + static constexpr llvm::StringLiteral RootId = "___root___"; +}; + +/// A fluent builder class for \c RewriteRule. See comments on \c RewriteRule. +class RewriteRuleBuilder { + RewriteRule Rule; + +public: + RewriteRuleBuilder(ast_matchers::internal::DynTypedMatcher M) { + M.setAllowBind(true); + // `tryBind` is guaranteed to succeed, because `AllowBind` was set to true. + Rule.Matcher = *M.tryBind(RewriteRule::RootId); + Rule.Target = RewriteRule::RootId; + Rule.TargetKind = M.getSupportedKind(); + Rule.TargetPart = NodePart::Node; + } + + /// (Implicit) "build" operator to build a RewriteRule from this builder. + operator RewriteRule() && { return std::move(Rule); } + + // Sets the target kind based on a clang AST node type. + template RewriteRuleBuilder as(); + + template + RewriteRuleBuilder change(llvm::StringRef Target, + NodePart Part = NodePart::Node); + + RewriteRuleBuilder replaceWith(TextGenerator Replacement); + RewriteRuleBuilder replaceWith(std::string Replacement) { + return replaceWith(text(std::move(Replacement))); + } + + RewriteRuleBuilder because(TextGenerator Explanation); + RewriteRuleBuilder because(std::string Explanation) { + return because(text(std::move(Explanation))); + } + +private: + // Wraps a string as a TextGenerator. + static TextGenerator text(std::string M) { + return [M](const ast_matchers::MatchFinder::MatchResult &) { return M; }; + } +}; + +/// Convenience factory functions for starting construction of a \c RewriteRule. +inline RewriteRuleBuilder buildRule(ast_matchers::internal::DynTypedMatcher M) { + return RewriteRuleBuilder(std::move(M)); +} + +template RewriteRuleBuilder RewriteRuleBuilder::as() { + Rule.TargetKind = ast_type_traits::ASTNodeKind::getFromNodeKind(); + return *this; +} + +template +RewriteRuleBuilder RewriteRuleBuilder::change(llvm::StringRef TargetId, + NodePart Part) { + Rule.Target = TargetId; + Rule.TargetKind = ast_type_traits::ASTNodeKind::getFromNodeKind(); + Rule.TargetPart = Part; + return *this; +} + +/// A source "transformation," represented by a character range in the source to +/// be replaced and a corresponding replacement string. +struct Transformation { + CharSourceRange Range; + std::string Replacement; +}; + +/// Attempts to apply a rule to a match. Returns an empty transformation if the +/// match is not eligible for rewriting (certain interactions with macros, for +/// example). Fails if any invariants are violated relating to bound nodes in +/// the match. +Expected +applyRewriteRule(const RewriteRule &Rule, + const ast_matchers::MatchFinder::MatchResult &Match); + +/// Handles the matcher and callback registration for a single rewrite rule, as +/// defined by the arguments of the constructor. +class Transformer : public ast_matchers::MatchFinder::MatchCallback { +public: + using ChangeConsumer = + std::function; + + /// \param Consumer Receives each successful rewrites as an \c AtomicChange. + Transformer(RewriteRule Rule, ChangeConsumer Consumer) + : Rule(std::move(Rule)), Consumer(std::move(Consumer)) {} + + /// N.B. Passes `this` pointer to `MatchFinder`. So, this object should not + /// be moved after this call. + void registerMatchers(ast_matchers::MatchFinder *MatchFinder); + + /// Not called directly by users -- called by the framework, via base class + /// pointer. + void run(const ast_matchers::MatchFinder::MatchResult &Result) override; + +private: + RewriteRule Rule; + /// Receives each successful rewrites as an \c AtomicChange. + ChangeConsumer Consumer; +}; +} // namespace tooling +} // namespace clang + +#endif // LLVM_CLANG_TOOLING_REFACTOR_TRANSFORMER_H_ Index: lib/Tooling/Refactoring/CMakeLists.txt =================================================================== --- lib/Tooling/Refactoring/CMakeLists.txt +++ lib/Tooling/Refactoring/CMakeLists.txt @@ -12,6 +12,7 @@ Rename/USRFinder.cpp Rename/USRFindingAction.cpp Rename/USRLocFinder.cpp + Transformer.cpp LINK_LIBS clangAST Index: lib/Tooling/Refactoring/Transformer.cpp =================================================================== --- lib/Tooling/Refactoring/Transformer.cpp +++ lib/Tooling/Refactoring/Transformer.cpp @@ -0,0 +1,204 @@ +//===--- Transformer.cpp - Transformer library implementation ---*- 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/Tooling/Refactoring/Transformer.h" +#include "clang/AST/Expr.h" +#include "clang/ASTMatchers/ASTMatchFinder.h" +#include "clang/ASTMatchers/ASTMatchers.h" +#include "clang/Basic/Diagnostic.h" +#include "clang/Basic/SourceLocation.h" +#include "clang/Rewrite/Core/Rewriter.h" +#include "clang/Tooling/FixIt.h" +#include "clang/Tooling/Refactoring.h" +#include "clang/Tooling/Refactoring/AtomicChange.h" +#include "llvm/ADT/Optional.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Errc.h" +#include "llvm/Support/Error.h" +#include +#include +#include +#include + +using namespace clang; +using namespace tooling; + +using ast_matchers::MatchFinder; +using ast_type_traits::ASTNodeKind; +using ast_type_traits::DynTypedNode; +using llvm::Error; +using llvm::Expected; +using llvm::Optional; +using llvm::StringError; +using llvm::StringRef; +using llvm::Twine; + +using MatchResult = MatchFinder::MatchResult; + +// Did the text at this location originate in a macro definition (aka. body)? +// For example, +// +// #define NESTED(x) x +// #define MACRO(y) { int y = NESTED(3); } +// if (true) MACRO(foo) +// +// The if statement expands to +// +// if (true) { int foo = 3; } +// ^ ^ +// Loc1 Loc2 +// +// For SourceManager SM, SM.isMacroArgExpansion(Loc1) and +// SM.isMacroArgExpansion(Loc2) are both true, but isOriginMacroBody(sm, Loc1) +// is false, because "foo" originated in the source file (as an argument to a +// macro), whereas isOriginMacroBody(SM, Loc2) is true, because "3" originated +// in the definition of MACRO. +static bool isOriginMacroBody(const clang::SourceManager &SM, + clang::SourceLocation Loc) { + while (Loc.isMacroID()) { + if (SM.isMacroBodyExpansion(Loc)) + return true; + // Otherwise, it must be in an argument, so we continue searching up the + // invocation stack. getImmediateMacroCallerLoc() gives the location of the + // argument text, inside the call text. + Loc = SM.getImmediateMacroCallerLoc(Loc); + } + return false; +} + +static llvm::Error invalidArgumentError(Twine Message) { + return llvm::make_error(llvm::errc::invalid_argument, Message); +} + +static llvm::Error typeError(StringRef Id, const ASTNodeKind &Kind, + Twine Message) { + return invalidArgumentError( + Message + " (node id=" + Id + " kind=" + Kind.asStringRef() + ")"); +} + +static llvm::Error missingPropertyError(StringRef Id, Twine Description, + StringRef Property) { + return invalidArgumentError(Description + " requires property '" + Property + + "' (node id=" + Id + ")"); +} + +static Expected +getTargetRange(StringRef Target, const DynTypedNode &Node, ASTNodeKind Kind, + NodePart TargetPart, ASTContext &Context) { + switch (TargetPart) { + case NodePart::Node: { + // For non-expression statements, associate any trailing semicolon with the + // statement text. However, if the target was intended as an expression (as + // indicated by its kind) then we do not associate any trailing semicolon + // with it. We only associate the exact expression text. + if (Node.get() != nullptr) { + auto ExprKind = ASTNodeKind::getFromNodeKind(); + if (!ExprKind.isBaseOf(Kind)) + return fixit::getExtendedRange(Node, tok::TokenKind::semi, Context); + } + return CharSourceRange::getTokenRange(Node.getSourceRange()); + } + case NodePart::Member: + if (auto *M = Node.get()) + return CharSourceRange::getTokenRange( + M->getMemberNameInfo().getSourceRange()); + return typeError(Target, Node.getNodeKind(), + "NodePart::Member applied to non-MemberExpr"); + case NodePart::Name: + if (const auto *D = Node.get()) { + if (!D->getDeclName().isIdentifier()) + return missingPropertyError(Target, "NodePart::Name", "identifier"); + SourceLocation L = D->getLocation(); + auto R = CharSourceRange::getTokenRange(L, L); + // Verify that the range covers exactly the name. + // FIXME: extend this code to support cases like `operator +` or + // `foo` for which this range will be too short. Doing so will + // require subcasing `NamedDecl`, because it doesn't provide virtual + // access to the \c DeclarationNameInfo. + if (fixit::internal::getText(R, Context) != D->getName()) + return CharSourceRange(); + return R; + } + if (const auto *E = Node.get()) { + if (!E->getNameInfo().getName().isIdentifier()) + return missingPropertyError(Target, "NodePart::Name", "identifier"); + SourceLocation L = E->getLocation(); + return CharSourceRange::getTokenRange(L, L); + } + if (const auto *I = Node.get()) { + if (!I->isMemberInitializer() && I->isWritten()) + return missingPropertyError(Target, "NodePart::Name", + "explicit member initializer"); + SourceLocation L = I->getMemberLocation(); + return CharSourceRange::getTokenRange(L, L); + } + return typeError( + Target, Node.getNodeKind(), + "NodePart::Name applied to neither DeclRefExpr, NamedDecl nor " + "CXXCtorInitializer"); + } + llvm_unreachable("Unexpected case in NodePart type."); +} + +Expected +tooling::applyRewriteRule(const RewriteRule &Rule, + const ast_matchers::MatchFinder::MatchResult &Match) { + if (Match.Context->getDiagnostics().hasErrorOccurred()) + return Transformation(); + + auto &NodesMap = Match.Nodes.getMap(); + auto It = NodesMap.find(Rule.Target); + assert (It != NodesMap.end() && "Rule.Target must be bound in the match."); + + Expected TargetOrErr = + getTargetRange(Rule.Target, It->second, Rule.TargetKind, Rule.TargetPart, + *Match.Context); + if (auto Err = TargetOrErr.takeError()) + return std::move(Err); + auto &Target = *TargetOrErr; + if (Target.isInvalid() || + isOriginMacroBody(*Match.SourceManager, Target.getBegin())) + return Transformation(); + + return Transformation{Target, Rule.Replacement(Match)}; +} + +constexpr llvm::StringLiteral RewriteRule::RootId; + +RewriteRuleBuilder RewriteRuleBuilder::replaceWith(TextGenerator T) { + Rule.Replacement = std::move(T); + return *this; +} + +RewriteRuleBuilder RewriteRuleBuilder::because(TextGenerator T) { + Rule.Explanation = std::move(T); + return *this; +} + +void Transformer::registerMatchers(MatchFinder *MatchFinder) { + MatchFinder->addDynamicMatcher(Rule.Matcher, this); +} + +void Transformer::run(const MatchResult &Result) { + auto ChangeOrErr = applyRewriteRule(Rule, Result); + if (auto Err = ChangeOrErr.takeError()) { + llvm::errs() << "Rewrite failed: " << llvm::toString(std::move(Err)) + << "\n"; + return; + } + auto &Change = *ChangeOrErr; + auto &Range = Change.Range; + if (Range.isInvalid()) { + // No rewrite applied (but no error encountered either). + return; + } + AtomicChange AC(*Result.SourceManager, Range.getBegin()); + if (auto Err = AC.replace(*Result.SourceManager, Range, Change.Replacement)) + AC.setError(llvm::toString(std::move(Err))); + Consumer(AC); +} Index: unittests/Tooling/CMakeLists.txt =================================================================== --- unittests/Tooling/CMakeLists.txt +++ unittests/Tooling/CMakeLists.txt @@ -50,6 +50,7 @@ ReplacementsYamlTest.cpp RewriterTest.cpp ToolingTest.cpp + TransformerTest.cpp ) target_link_libraries(ToolingTests Index: unittests/Tooling/TransformerTest.cpp =================================================================== --- unittests/Tooling/TransformerTest.cpp +++ unittests/Tooling/TransformerTest.cpp @@ -0,0 +1,389 @@ +//===- unittest/Tooling/TransformerTest.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/Refactoring/Transformer.h" + +#include "clang/ASTMatchers/ASTMatchers.h" +#include "clang/Tooling/Tooling.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace tooling { +namespace { +using ast_matchers::anyOf; +using ast_matchers::argumentCountIs; +using ast_matchers::callee; +using ast_matchers::callExpr; +using ast_matchers::cxxMemberCallExpr; +using ast_matchers::cxxMethodDecl; +using ast_matchers::cxxRecordDecl; +using ast_matchers::declRefExpr; +using ast_matchers::expr; +using ast_matchers::functionDecl; +using ast_matchers::hasAnyName; +using ast_matchers::hasArgument; +using ast_matchers::hasDeclaration; +using ast_matchers::hasElse; +using ast_matchers::hasName; +using ast_matchers::hasType; +using ast_matchers::ifStmt; +using ast_matchers::member; +using ast_matchers::memberExpr; +using ast_matchers::namedDecl; +using ast_matchers::on; +using ast_matchers::pointsTo; +using ast_matchers::to; +using ast_matchers::unless; + +using llvm::StringRef; + +constexpr char KHeaderContents[] = R"cc( + struct string { + string(const char*); + char* c_str(); + int size(); + }; + int strlen(const char*); + + namespace proto { + struct PCFProto { + int foo(); + }; + struct ProtoCommandLineFlag : PCFProto { + PCFProto& GetProto(); + }; + } // namespace proto +)cc"; + +static ast_matchers::internal::Matcher +isOrPointsTo(const clang::ast_matchers::DeclarationMatcher &TypeMatcher) { + return anyOf(hasDeclaration(TypeMatcher), pointsTo(TypeMatcher)); +} + +static std::string format(StringRef Code) { + const std::vector Ranges(1, Range(0, Code.size())); + auto Style = format::getLLVMStyle(); + const auto Replacements = format::reformat(Style, Code, Ranges); + auto Formatted = applyAllReplacements(Code, Replacements); + if (!Formatted) { + ADD_FAILURE() << "Could not format code: " + << llvm::toString(Formatted.takeError()); + return std::string(); + } + return *Formatted; +} + +static void compareSnippets(StringRef Expected, + const llvm::Optional &MaybeActual) { + ASSERT_TRUE(MaybeActual) << "Rewrite failed. Expecting: " << Expected; + auto Actual = *MaybeActual; + std::string HL = "#include \"header.h\"\n"; + auto I = Actual.find(HL); + if (I != std::string::npos) + Actual.erase(I, HL.size()); + EXPECT_EQ(format(Expected), format(Actual)); +} + +// FIXME: consider separating this class into its own file(s). +class ClangRefactoringTestBase : public testing::Test { +protected: + void appendToHeader(StringRef S) { FileContents[0].second += S; } + + void addFile(StringRef Filename, StringRef Content) { + FileContents.emplace_back(Filename, Content); + } + + llvm::Optional rewrite(StringRef Input) { + std::string Code = ("#include \"header.h\"\n" + Input).str(); + auto Factory = newFrontendActionFactory(&MatchFinder); + if (!runToolOnCodeWithArgs( + Factory->create(), Code, std::vector(), "input.cc", + "clang-tool", std::make_shared(), + FileContents)) { + return None; + } + auto ChangedCodeOrErr = + applyAtomicChanges("input.cc", Code, Changes, ApplyChangesSpec()); + if (auto Err = ChangedCodeOrErr.takeError()) { + llvm::errs() << "Change failed: " << llvm::toString(std::move(Err)) + << "\n"; + return None; + } + return *ChangedCodeOrErr; + } + + void testRule(RewriteRule Rule, StringRef Input, StringRef Expected) { + Transformer T(std::move(Rule), + [this](const AtomicChange &C) { Changes.push_back(C); }); + T.registerMatchers(&MatchFinder); + compareSnippets(Expected, rewrite(Input)); + } + + clang::ast_matchers::MatchFinder MatchFinder; + AtomicChanges Changes; + +private: + FileContentMappings FileContents = {{"header.h", ""}}; +}; + +class TransformerTest : public ClangRefactoringTestBase { +protected: + TransformerTest() { appendToHeader(KHeaderContents); } +}; + +// Given string s, change strlen($s.c_str()) to $s.size(). +static RewriteRule ruleStrlenSize() { + StringRef StringExpr = "strexpr"; + auto StringType = namedDecl(hasAnyName("::basic_string", "::string")); + return buildRule( + callExpr( + callee(functionDecl(hasName("strlen"))), + hasArgument(0, cxxMemberCallExpr( + on(expr(hasType(isOrPointsTo(StringType))) + .bind(StringExpr)), + callee(cxxMethodDecl(hasName("c_str"))))))) + // Specify the intended type explicitly, because the matcher "type" of + // `callExpr()` is `Stmt`, not `Expr`. + .as() + .replaceWith("REPLACED") + .because("Use size() method directly on string."); +} + +TEST_F(TransformerTest, StrlenSize) { + std::string Input = "int f(string s) { return strlen(s.c_str()); }"; + std::string Expected = "int f(string s) { return REPLACED; }"; + testRule(ruleStrlenSize(), Input, Expected); +} + +// Tests that no change is applied when a match is not expected. +TEST_F(TransformerTest, NoMatch) { + std::string Input = "int f(string s) { return s.size(); }"; + testRule(ruleStrlenSize(), Input, Input); +} + +// Tests that expressions in macro arguments are rewritten (when applicable). +TEST_F(TransformerTest, StrlenSizeMacro) { + std::string Input = R"cc( +#define ID(e) e + int f(string s) { return ID(strlen(s.c_str())); })cc"; + std::string Expected = R"cc( +#define ID(e) e + int f(string s) { return ID(REPLACED); })cc"; + testRule(ruleStrlenSize(), Input, Expected); +} + +// Tests replacing an expression. +TEST_F(TransformerTest, Flag) { + StringRef Flag = "flag"; + RewriteRule Rule = + buildRule( + cxxMemberCallExpr(on(expr(hasType(cxxRecordDecl(hasName( + "proto::ProtoCommandLineFlag")))) + .bind(Flag)), + unless(callee(cxxMethodDecl(hasName("GetProto")))))) + .change(Flag) + .replaceWith("EXPR") + .because("Use GetProto() to access proto fields."); + + std::string Input = R"cc( + proto::ProtoCommandLineFlag flag; + int x = flag.foo(); + int y = flag.GetProto().foo(); + )cc"; + std::string Expected = R"cc( + proto::ProtoCommandLineFlag flag; + int x = EXPR.foo(); + int y = flag.GetProto().foo(); + )cc"; + + testRule(std::move(Rule), Input, Expected); +} + +TEST_F(TransformerTest, NodePartNameNamedDecl) { + StringRef Fun = "fun"; + RewriteRule Rule = buildRule(functionDecl(hasName("bad")).bind(Fun)) + .change(Fun, NodePart::Name) + .replaceWith("good"); + + std::string Input = R"cc( + int bad(int x); + int bad(int x) { return x * x; } + )cc"; + std::string Expected = R"cc( + int good(int x); + int good(int x) { return x * x; } + )cc"; + + testRule(Rule, Input, Expected); +} + +TEST_F(TransformerTest, NodePartNameDeclRef) { + std::string Input = R"cc( + template + T bad(T x) { + return x; + } + int neutral(int x) { return bad(x) * x; } + )cc"; + std::string Expected = R"cc( + template + T bad(T x) { + return x; + } + int neutral(int x) { return good(x) * x; } + )cc"; + + StringRef Ref = "ref"; + testRule(buildRule(declRefExpr(to(functionDecl(hasName("bad")))).bind(Ref)) + .change(Ref, NodePart::Name) + .replaceWith("good"), + Input, Expected); +} + +TEST_F(TransformerTest, NodePartNameDeclRefFailure) { + std::string Input = R"cc( + struct Y { + int operator*(); + }; + int neutral(int x) { + Y y; + int (Y::*ptr)() = &Y::operator*; + return *y + x; + } + )cc"; + + StringRef Ref = "ref"; + testRule(buildRule(declRefExpr(to(functionDecl())).bind(Ref)) + .change(Ref, NodePart::Name) + .replaceWith("good"), + Input, Input); +} + +TEST_F(TransformerTest, NodePartMember) { + StringRef E = "expr"; + RewriteRule Rule = buildRule(memberExpr(member(hasName("bad"))).bind(E)) + .change(E, NodePart::Member) + .replaceWith("good"); + + std::string Input = R"cc( + struct S { + int bad; + }; + int g() { + S s; + return s.bad; + } + )cc"; + std::string Expected = R"cc( + struct S { + int bad; + }; + int g() { + S s; + return s.good; + } + )cc"; + + testRule(Rule, Input, Expected); +} + +TEST_F(TransformerTest, NodePartMemberQualified) { + std::string Input = R"cc( + struct S { + int bad; + int good; + }; + struct T : public S { + int bad; + }; + int g() { + T t; + return t.S::bad; + } + )cc"; + std::string Expected = R"cc( + struct S { + int bad; + int good; + }; + struct T : public S { + int bad; + }; + int g() { + T t; + return t.S::good; + } + )cc"; + + StringRef E = "expr"; + testRule(buildRule(memberExpr().bind(E)) + .change(E, NodePart::Member) + .replaceWith("good"), + Input, Expected); +} + +TEST_F(TransformerTest, NodePartMemberMultiToken) { + std::string Input = R"cc( + struct Y { + int operator*(); + int good(); + template void foo(T t); + }; + int neutral(int x) { + Y y; + y.template foo(3); + return y.operator *(); + } + )cc"; + std::string Expected = R"cc( + struct Y { + int operator*(); + int good(); + template void foo(T t); + }; + int neutral(int x) { + Y y; + y.template good(3); + return y.good(); + } + )cc"; + + StringRef MemExpr = "member"; + testRule(buildRule(memberExpr().bind(MemExpr)) + .change(MemExpr, NodePart::Member) + .replaceWith("good"), + Input, Expected); +} + +// +// Negative tests (where we expect no transformation to occur). +// + +TEST_F(TransformerTest, NoTransformationInMacro) { + std::string Input = R"cc( +#define MACRO(str) strlen((str).c_str()) + int f(string s) { return MACRO(s); })cc"; + testRule(ruleStrlenSize(), Input, Input); +} + +// This test handles the corner case where a macro called within another macro +// expands to matching code, but the matched code is an argument to the nested +// macro. A simple check of isMacroArgExpansion() vs. isMacroBodyExpansion() +// will get this wrong, and transform the code. This test verifies that no such +// transformation occurs. +TEST_F(TransformerTest, NoTransformationInNestedMacro) { + std::string Input = R"cc( +#define NESTED(e) e +#define MACRO(str) NESTED(strlen((str).c_str())) + int f(string s) { return MACRO(s); })cc"; + testRule(ruleStrlenSize(), Input, Input); +} +} // namespace +} // namespace tooling +} // namespace clang