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()).drop_back(Wrapping.second.size());
50 return Outer;
51 }
52
rangeOrPoint(const Annotations & A)53 std::pair<unsigned, unsigned> rangeOrPoint(const Annotations &A) {
54 Range SelectionRng;
55 if (A.points().size() != 0) {
56 assert(A.ranges().size() == 0 &&
57 "both a cursor point and a selection range were specified");
58 SelectionRng = Range{A.point(), A.point()};
59 } else {
60 SelectionRng = A.range();
61 }
62 return {cantFail(positionToOffset(A.code(), SelectionRng.start)),
63 cantFail(positionToOffset(A.code(), SelectionRng.end))};
64 }
65
66 // Prepare and apply the specified tweak based on the selection in Input.
67 // Returns None if and only if prepare() failed.
68 llvm::Optional<llvm::Expected<Tweak::Effect>>
applyTweak(ParsedAST & AST,const Annotations & Input,StringRef TweakID,const SymbolIndex * Index)69 applyTweak(ParsedAST &AST, const Annotations &Input, StringRef TweakID,
70 const SymbolIndex *Index) {
71 auto Range = rangeOrPoint(Input);
72 llvm::Optional<llvm::Expected<Tweak::Effect>> Result;
73 SelectionTree::createEach(AST.getASTContext(), AST.getTokens(), Range.first,
74 Range.second, [&](SelectionTree ST) {
75 Tweak::Selection S(Index, AST, Range.first,
76 Range.second, std::move(ST));
77 if (auto T = prepareTweak(TweakID, S)) {
78 Result = (*T)->apply(S);
79 return true;
80 } else {
81 llvm::consumeError(T.takeError());
82 return false;
83 }
84 });
85 return Result;
86 }
87
88 MATCHER_P7(TweakIsAvailable, TweakID, Ctx, Header, ExtraArgs, ExtraFiles, Index,
89 FileName,
90 (TweakID + (negation ? " is unavailable" : " is available")).str()) {
91 std::string WrappedCode = wrap(Ctx, arg);
92 Annotations Input(WrappedCode);
93 TestTU TU;
94 TU.Filename = std::string(FileName);
95 TU.HeaderCode = Header;
96 TU.Code = std::string(Input.code());
97 TU.ExtraArgs = ExtraArgs;
98 TU.AdditionalFiles = std::move(ExtraFiles);
99 ParsedAST AST = TU.build();
100 auto Result = applyTweak(AST, Input, TweakID, Index);
101 // We only care if prepare() succeeded, but must handle Errors.
102 if (Result && !*Result)
103 consumeError(Result->takeError());
104 return Result.hasValue();
105 }
106
107 } // namespace
108
apply(llvm::StringRef MarkedCode,llvm::StringMap<std::string> * EditedFiles) const109 std::string TweakTest::apply(llvm::StringRef MarkedCode,
110 llvm::StringMap<std::string> *EditedFiles) const {
111 std::string WrappedCode = wrap(Context, MarkedCode);
112 Annotations Input(WrappedCode);
113 TestTU TU;
114 TU.Filename = std::string(FileName);
115 TU.HeaderCode = Header;
116 TU.AdditionalFiles = std::move(ExtraFiles);
117 TU.Code = std::string(Input.code());
118 TU.ExtraArgs = ExtraArgs;
119 ParsedAST AST = TU.build();
120
121 auto Result = applyTweak(AST, Input, TweakID, Index.get());
122 if (!Result)
123 return "unavailable";
124 if (!*Result)
125 return "fail: " + llvm::toString(Result->takeError());
126 const auto &Effect = **Result;
127 if ((*Result)->ShowMessage)
128 return "message:\n" + *Effect.ShowMessage;
129 if (Effect.ApplyEdits.empty())
130 return "no effect";
131
132 std::string EditedMainFile;
133 for (auto &It : Effect.ApplyEdits) {
134 auto NewText = It.second.apply();
135 if (!NewText)
136 return "bad edits: " + llvm::toString(NewText.takeError());
137 llvm::StringRef Unwrapped = unwrap(Context, *NewText);
138 if (It.first() == testPath(TU.Filename))
139 EditedMainFile = std::string(Unwrapped);
140 else {
141 if (!EditedFiles)
142 ADD_FAILURE() << "There were changes to additional files, but client "
143 "provided a nullptr for EditedFiles.";
144 else
145 EditedFiles->insert_or_assign(It.first(), Unwrapped.str());
146 }
147 }
148 return EditedMainFile;
149 }
150
isAvailable() const151 ::testing::Matcher<llvm::StringRef> TweakTest::isAvailable() const {
152 return TweakIsAvailable(llvm::StringRef(TweakID), Context, Header, ExtraArgs,
153 ExtraFiles, Index.get(), FileName);
154 }
155
expandCases(llvm::StringRef MarkedCode)156 std::vector<std::string> TweakTest::expandCases(llvm::StringRef MarkedCode) {
157 Annotations Test(MarkedCode);
158 llvm::StringRef Code = Test.code();
159 std::vector<std::string> Cases;
160 for (const auto& Point : Test.points()) {
161 size_t Offset = llvm::cantFail(positionToOffset(Code, Point));
162 Cases.push_back((Code.substr(0, Offset) + "^" + Code.substr(Offset)).str());
163 }
164 for (const auto& Range : Test.ranges()) {
165 size_t Begin = llvm::cantFail(positionToOffset(Code, Range.start));
166 size_t End = llvm::cantFail(positionToOffset(Code, Range.end));
167 Cases.push_back((Code.substr(0, Begin) + "[[" +
168 Code.substr(Begin, End - Begin) + "]]" + Code.substr(End))
169 .str());
170 }
171 assert(!Cases.empty() && "No markings in MarkedCode?");
172 return Cases;
173 }
174
175 } // namespace clangd
176 } // namespace clang
177