1 //===-- TweakTesting.cpp ------------------------------------------------*-===//
2 //
3 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4 // See https://llvm.org/LICENSE.txt for license information.
5 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6 //
7 //===----------------------------------------------------------------------===//
8 
9 #include "TweakTesting.h"
10 
11 #include "Annotations.h"
12 #include "SourceCode.h"
13 #include "TestFS.h"
14 #include "refactor/Tweak.h"
15 #include "clang/Tooling/Core/Replacement.h"
16 #include "llvm/Support/Error.h"
17 #include "gmock/gmock.h"
18 #include "gtest/gtest.h"
19 #include <string>
20 
21 namespace clang {
22 namespace clangd {
23 namespace {
24 using Context = TweakTest::CodeContext;
25 
wrapping(Context Ctx)26 std::pair<llvm::StringRef, llvm::StringRef> wrapping(Context Ctx) {
27   switch (Ctx) {
28   case TweakTest::File:
29     return {"", ""};
30   case TweakTest::Function:
31     return {"void wrapperFunction(){\n", "\n}"};
32   case TweakTest::Expression:
33     return {"auto expressionWrapper(){return\n", "\n;}"};
34   }
35   llvm_unreachable("Unknown TweakTest::CodeContext enum");
36 }
37 
wrap(Context Ctx,llvm::StringRef Inner)38 std::string wrap(Context Ctx, llvm::StringRef Inner) {
39   auto Wrapping = wrapping(Ctx);
40   return (Wrapping.first + Inner + Wrapping.second).str();
41 }
42 
unwrap(Context Ctx,llvm::StringRef Outer)43 llvm::StringRef unwrap(Context Ctx, llvm::StringRef Outer) {
44   auto Wrapping = wrapping(Ctx);
45   // Unwrap only if the code matches the expected wrapping.
46   // Don't allow the begin/end wrapping to overlap!
47   if (Outer.startswith(Wrapping.first) && Outer.endswith(Wrapping.second) &&
48       Outer.size() >= Wrapping.first.size() + Wrapping.second.size())
49     return Outer.drop_front(Wrapping.first.size())
50         .drop_back(Wrapping.second.size());
51   return Outer;
52 }
53 
rangeOrPoint(const Annotations & A)54 std::pair<unsigned, unsigned> rangeOrPoint(const Annotations &A) {
55   Range SelectionRng;
56   if (A.points().size() != 0) {
57     assert(A.ranges().size() == 0 &&
58            "both a cursor point and a selection range were specified");
59     SelectionRng = Range{A.point(), A.point()};
60   } else {
61     SelectionRng = A.range();
62   }
63   return {cantFail(positionToOffset(A.code(), SelectionRng.start)),
64           cantFail(positionToOffset(A.code(), SelectionRng.end))};
65 }
66 
67 // Prepare and apply the specified tweak based on the selection in Input.
68 // Returns None if and only if prepare() failed.
69 llvm::Optional<llvm::Expected<Tweak::Effect>>
applyTweak(ParsedAST & AST,const Annotations & Input,StringRef TweakID,const SymbolIndex * Index)70 applyTweak(ParsedAST &AST, const Annotations &Input, StringRef TweakID,
71            const SymbolIndex *Index) {
72   auto Range = rangeOrPoint(Input);
73   llvm::Optional<llvm::Expected<Tweak::Effect>> Result;
74   SelectionTree::createEach(AST.getASTContext(), AST.getTokens(), Range.first,
75                             Range.second, [&](SelectionTree ST) {
76                               Tweak::Selection S(Index, AST, Range.first,
77                                                  Range.second, std::move(ST));
78                               if (auto T = prepareTweak(TweakID, S)) {
79                                 Result = (*T)->apply(S);
80                                 return true;
81                               } else {
82                                 llvm::consumeError(T.takeError());
83                                 return false;
84                               }
85                             });
86   return Result;
87 }
88 
89 MATCHER_P7(TweakIsAvailable, TweakID, Ctx, Header, ExtraArgs, ExtraFiles, Index,
90            FileName,
91            (TweakID + (negation ? " is unavailable" : " is available")).str()) {
92   std::string WrappedCode = wrap(Ctx, arg);
93   Annotations Input(WrappedCode);
94   TestTU TU;
95   TU.Filename = std::string(FileName);
96   TU.HeaderCode = Header;
97   TU.Code = std::string(Input.code());
98   TU.ExtraArgs = ExtraArgs;
99   TU.AdditionalFiles = std::move(ExtraFiles);
100   ParsedAST AST = TU.build();
101   auto Result = applyTweak(AST, Input, TweakID, Index);
102   // We only care if prepare() succeeded, but must handle Errors.
103   if (Result && !*Result)
104     consumeError(Result->takeError());
105   return Result.hasValue();
106 }
107 
108 } // namespace
109 
apply(llvm::StringRef MarkedCode,llvm::StringMap<std::string> * EditedFiles) const110 std::string TweakTest::apply(llvm::StringRef MarkedCode,
111                              llvm::StringMap<std::string> *EditedFiles) const {
112   std::string WrappedCode = wrap(Context, MarkedCode);
113   Annotations Input(WrappedCode);
114   TestTU TU;
115   TU.Filename = std::string(FileName);
116   TU.HeaderCode = Header;
117   TU.AdditionalFiles = std::move(ExtraFiles);
118   TU.Code = std::string(Input.code());
119   TU.ExtraArgs = ExtraArgs;
120   ParsedAST AST = TU.build();
121 
122   auto Result = applyTweak(AST, Input, TweakID, Index.get());
123   if (!Result)
124     return "unavailable";
125   if (!*Result)
126     return "fail: " + llvm::toString(Result->takeError());
127   const auto &Effect = **Result;
128   if ((*Result)->ShowMessage)
129     return "message:\n" + *Effect.ShowMessage;
130   if (Effect.ApplyEdits.empty())
131     return "no effect";
132 
133   std::string EditedMainFile;
134   for (auto &It : Effect.ApplyEdits) {
135     auto NewText = It.second.apply();
136     if (!NewText)
137       return "bad edits: " + llvm::toString(NewText.takeError());
138     llvm::StringRef Unwrapped = unwrap(Context, *NewText);
139     if (It.first() == testPath(TU.Filename))
140       EditedMainFile = std::string(Unwrapped);
141     else {
142       if (!EditedFiles)
143         ADD_FAILURE() << "There were changes to additional files, but client "
144                          "provided a nullptr for EditedFiles.";
145       else
146         EditedFiles->insert_or_assign(It.first(), Unwrapped.str());
147     }
148   }
149   return EditedMainFile;
150 }
151 
isAvailable() const152 ::testing::Matcher<llvm::StringRef> TweakTest::isAvailable() const {
153   return TweakIsAvailable(llvm::StringRef(TweakID), Context, Header, ExtraArgs,
154                           ExtraFiles, Index.get(), FileName);
155 }
156 
expandCases(llvm::StringRef MarkedCode)157 std::vector<std::string> TweakTest::expandCases(llvm::StringRef MarkedCode) {
158   Annotations Test(MarkedCode);
159   llvm::StringRef Code = Test.code();
160   std::vector<std::string> Cases;
161   for (const auto &Point : Test.points()) {
162     size_t Offset = llvm::cantFail(positionToOffset(Code, Point));
163     Cases.push_back((Code.substr(0, Offset) + "^" + Code.substr(Offset)).str());
164   }
165   for (const auto &Range : Test.ranges()) {
166     size_t Begin = llvm::cantFail(positionToOffset(Code, Range.start));
167     size_t End = llvm::cantFail(positionToOffset(Code, Range.end));
168     Cases.push_back((Code.substr(0, Begin) + "[[" +
169                      Code.substr(Begin, End - Begin) + "]]" + Code.substr(End))
170                         .str());
171   }
172   assert(!Cases.empty() && "No markings in MarkedCode?");
173   return Cases;
174 }
175 
176 } // namespace clangd
177 } // namespace clang
178