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 "SourceCode.h" 12 #include "TestTU.h" 13 #include "refactor/Tweak.h" 14 #include "llvm/Support/Error.h" 15 #include "gmock/gmock.h" 16 #include "gtest/gtest.h" 17 #include <optional> 18 #include <string> 19 20 namespace clang { 21 namespace clangd { 22 namespace { 23 using Context = TweakTest::CodeContext; 24 25 std::pair<llvm::StringRef, llvm::StringRef> wrapping(Context Ctx) { 26 switch (Ctx) { 27 case TweakTest::File: 28 return {"", ""}; 29 case TweakTest::Function: 30 return {"void wrapperFunction(){\n", "\n}"}; 31 case TweakTest::Expression: 32 return {"auto expressionWrapper(){return\n", "\n;}"}; 33 } 34 llvm_unreachable("Unknown TweakTest::CodeContext enum"); 35 } 36 37 std::string wrap(Context Ctx, llvm::StringRef Inner) { 38 auto Wrapping = wrapping(Ctx); 39 return (Wrapping.first + Inner + Wrapping.second).str(); 40 } 41 42 llvm::StringRef unwrap(Context Ctx, llvm::StringRef Outer) { 43 auto Wrapping = wrapping(Ctx); 44 // Unwrap only if the code matches the expected wrapping. 45 // Don't allow the begin/end wrapping to overlap! 46 if (Outer.startswith(Wrapping.first) && Outer.endswith(Wrapping.second) && 47 Outer.size() >= Wrapping.first.size() + Wrapping.second.size()) 48 return Outer.drop_front(Wrapping.first.size()) 49 .drop_back(Wrapping.second.size()); 50 return Outer; 51 } 52 53 llvm::Annotations::Range rangeOrPoint(const llvm::Annotations &A) { 54 if (A.points().size() != 0) { 55 assert(A.ranges().size() == 0 && 56 "both a cursor point and a selection range were specified"); 57 return {A.point(), A.point()}; 58 } 59 return A.range(); 60 } 61 62 // Prepare and apply the specified tweak based on the selection in Input. 63 // Returns std::nullopt if and only if prepare() failed. 64 std::optional<llvm::Expected<Tweak::Effect>> 65 applyTweak(ParsedAST &AST, llvm::Annotations::Range Range, StringRef TweakID, 66 const SymbolIndex *Index, llvm::vfs::FileSystem *FS) { 67 std::optional<llvm::Expected<Tweak::Effect>> Result; 68 SelectionTree::createEach(AST.getASTContext(), AST.getTokens(), Range.Begin, 69 Range.End, [&](SelectionTree ST) { 70 Tweak::Selection S(Index, AST, Range.Begin, 71 Range.End, std::move(ST), FS); 72 if (auto T = prepareTweak(TweakID, S, nullptr)) { 73 Result = (*T)->apply(S); 74 return true; 75 } else { 76 llvm::consumeError(T.takeError()); 77 return false; 78 } 79 }); 80 return Result; 81 } 82 83 } // namespace 84 85 std::string TweakTest::apply(llvm::StringRef MarkedCode, 86 llvm::StringMap<std::string> *EditedFiles) const { 87 std::string WrappedCode = wrap(Context, MarkedCode); 88 llvm::Annotations Input(WrappedCode); 89 TestTU TU; 90 TU.Filename = std::string(FileName); 91 TU.HeaderCode = Header; 92 TU.AdditionalFiles = std::move(ExtraFiles); 93 TU.Code = std::string(Input.code()); 94 TU.ExtraArgs = ExtraArgs; 95 ParsedAST AST = TU.build(); 96 97 auto Result = applyTweak( 98 AST, rangeOrPoint(Input), TweakID, Index.get(), 99 &AST.getSourceManager().getFileManager().getVirtualFileSystem()); 100 if (!Result) 101 return "unavailable"; 102 if (!*Result) 103 return "fail: " + llvm::toString(Result->takeError()); 104 const auto &Effect = **Result; 105 if ((*Result)->ShowMessage) 106 return "message:\n" + *Effect.ShowMessage; 107 if (Effect.ApplyEdits.empty()) 108 return "no effect"; 109 110 std::string EditedMainFile; 111 for (auto &It : Effect.ApplyEdits) { 112 auto NewText = It.second.apply(); 113 if (!NewText) 114 return "bad edits: " + llvm::toString(NewText.takeError()); 115 llvm::StringRef Unwrapped = unwrap(Context, *NewText); 116 if (It.first() == testPath(TU.Filename)) 117 EditedMainFile = std::string(Unwrapped); 118 else { 119 if (!EditedFiles) 120 ADD_FAILURE() << "There were changes to additional files, but client " 121 "provided a nullptr for EditedFiles."; 122 else 123 EditedFiles->insert_or_assign(It.first(), Unwrapped.str()); 124 } 125 } 126 return EditedMainFile; 127 } 128 129 bool TweakTest::isAvailable(WrappedAST &AST, 130 llvm::Annotations::Range Range) const { 131 // Adjust range for wrapping offset. 132 Range.Begin += AST.second; 133 Range.End += AST.second; 134 auto Result = applyTweak( 135 AST.first, Range, TweakID, Index.get(), 136 &AST.first.getSourceManager().getFileManager().getVirtualFileSystem()); 137 // We only care if prepare() succeeded, but must handle Errors. 138 if (Result && !*Result) 139 consumeError(Result->takeError()); 140 return Result.has_value(); 141 } 142 143 TweakTest::WrappedAST TweakTest::build(llvm::StringRef Code) const { 144 TestTU TU; 145 TU.Filename = std::string(FileName); 146 TU.HeaderCode = Header; 147 TU.Code = wrap(Context, Code); 148 TU.ExtraArgs = ExtraArgs; 149 TU.AdditionalFiles = std::move(ExtraFiles); 150 return {TU.build(), wrapping(Context).first.size()}; 151 } 152 153 std::string TweakTest::decorate(llvm::StringRef Code, unsigned Point) { 154 return (Code.substr(0, Point) + "^" + Code.substr(Point)).str(); 155 } 156 157 std::string TweakTest::decorate(llvm::StringRef Code, 158 llvm::Annotations::Range Range) { 159 return (Code.substr(0, Range.Begin) + "[[" + 160 Code.substr(Range.Begin, Range.End - Range.Begin) + "]]" + 161 Code.substr(Range.End)) 162 .str(); 163 } 164 165 } // namespace clangd 166 } // namespace clang 167