199 lines · cpp
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-exception6//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 20namespace clang {21namespace clangd {22namespace {23using Context = TweakTest::CodeContext;24 25std::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 37std::string wrap(Context Ctx, llvm::StringRef Inner) {38 auto Wrapping = wrapping(Ctx);39 return (Wrapping.first + Inner + Wrapping.second).str();40}41 42llvm::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.starts_with(Wrapping.first) && Outer.ends_with(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 53llvm::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.64std::optional<llvm::Expected<Tweak::Effect>>65applyTweak(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} // namespace84 85std::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 else123 EditedFiles->insert_or_assign(It.first(), Unwrapped.str());124 }125 }126 return EditedMainFile;127}128 129bool 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 143TweakTest::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 153std::string TweakTest::decorate(llvm::StringRef Code, unsigned Point) {154 return (Code.substr(0, Point) + "^" + Code.substr(Point)).str();155}156 157std::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// TODO: Reuse more code between TweakTest::apply() and166// TweakWorkspaceTest::apply().167TweakResult168TweakWorkspaceTest::apply(StringRef InvocationFile,169 llvm::Annotations::Range InvocationRange) {170 auto AST = Workspace.openFile(InvocationFile);171 if (!AST) {172 ADD_FAILURE() << "No file '" << InvocationFile << "' in workspace";173 return TweakResult{"failed to setup"};174 }175 176 auto Index = Workspace.index();177 auto Result = applyTweak(178 *AST, InvocationRange, TweakID, Index.get(),179 &AST->getSourceManager().getFileManager().getVirtualFileSystem());180 if (!Result)181 return TweakResult{"unavailable"};182 if (!*Result)183 return TweakResult{"fail: " + llvm::toString(Result->takeError())};184 const auto &Effect = **Result;185 if ((*Result)->ShowMessage)186 return TweakResult{"message:\n" + *Effect.ShowMessage};187 188 TweakResult Retval{"success"};189 for (auto &It : Effect.ApplyEdits) {190 auto NewText = It.second.apply();191 if (!NewText)192 return TweakResult{"bad edits: " + llvm::toString(NewText.takeError())};193 Retval.EditedFiles.insert_or_assign(It.first(), *NewText);194 }195 return Retval;196}197} // namespace clangd198} // namespace clang199