blob: e4637a04a7edd2d0f42e4a1962d3576a919badc7 [file] [log] [blame]
//===-- TweakTesting.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 "TweakTesting.h"
#include "Annotations.h"
#include "SourceCode.h"
#include "TestFS.h"
#include "refactor/Tweak.h"
#include "clang/Tooling/Core/Replacement.h"
#include "llvm/Support/Error.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include <string>
namespace clang {
namespace clangd {
namespace {
using Context = TweakTest::CodeContext;
std::pair<llvm::StringRef, llvm::StringRef> wrapping(Context Ctx) {
switch (Ctx) {
case TweakTest::File:
return {"", ""};
case TweakTest::Function:
return {"void wrapperFunction(){\n", "\n}"};
case TweakTest::Expression:
return {"auto expressionWrapper(){return\n", "\n;}"};
}
llvm_unreachable("Unknown TweakTest::CodeContext enum");
}
std::string wrap(Context Ctx, llvm::StringRef Inner) {
auto Wrapping = wrapping(Ctx);
return (Wrapping.first + Inner + Wrapping.second).str();
}
llvm::StringRef unwrap(Context Ctx, llvm::StringRef Outer) {
auto Wrapping = wrapping(Ctx);
// Unwrap only if the code matches the expected wrapping.
// Don't allow the begin/end wrapping to overlap!
if (Outer.startswith(Wrapping.first) && Outer.endswith(Wrapping.second) &&
Outer.size() >= Wrapping.first.size() + Wrapping.second.size())
return Outer.drop_front(Wrapping.first.size())
.drop_back(Wrapping.second.size());
return Outer;
}
std::pair<unsigned, unsigned> rangeOrPoint(const Annotations &A) {
Range SelectionRng;
if (A.points().size() != 0) {
assert(A.ranges().size() == 0 &&
"both a cursor point and a selection range were specified");
SelectionRng = Range{A.point(), A.point()};
} else {
SelectionRng = A.range();
}
return {cantFail(positionToOffset(A.code(), SelectionRng.start)),
cantFail(positionToOffset(A.code(), SelectionRng.end))};
}
// Prepare and apply the specified tweak based on the selection in Input.
// Returns None if and only if prepare() failed.
llvm::Optional<llvm::Expected<Tweak::Effect>>
applyTweak(ParsedAST &AST, const Annotations &Input, StringRef TweakID,
const SymbolIndex *Index, llvm::vfs::FileSystem *FS) {
auto Range = rangeOrPoint(Input);
llvm::Optional<llvm::Expected<Tweak::Effect>> Result;
SelectionTree::createEach(AST.getASTContext(), AST.getTokens(), Range.first,
Range.second, [&](SelectionTree ST) {
Tweak::Selection S(Index, AST, Range.first,
Range.second, std::move(ST),
FS);
if (auto T = prepareTweak(TweakID, S, nullptr)) {
Result = (*T)->apply(S);
return true;
} else {
llvm::consumeError(T.takeError());
return false;
}
});
return Result;
}
MATCHER_P7(TweakIsAvailable, TweakID, Ctx, Header, ExtraArgs, ExtraFiles, Index,
FileName,
(TweakID + (negation ? " is unavailable" : " is available")).str()) {
std::string WrappedCode = wrap(Ctx, arg);
Annotations Input(WrappedCode);
TestTU TU;
TU.Filename = std::string(FileName);
TU.HeaderCode = Header;
TU.Code = std::string(Input.code());
TU.ExtraArgs = ExtraArgs;
TU.AdditionalFiles = std::move(ExtraFiles);
ParsedAST AST = TU.build();
auto Result = applyTweak(
AST, Input, TweakID, Index,
&AST.getSourceManager().getFileManager().getVirtualFileSystem());
// We only care if prepare() succeeded, but must handle Errors.
if (Result && !*Result)
consumeError(Result->takeError());
return Result.hasValue();
}
} // namespace
std::string TweakTest::apply(llvm::StringRef MarkedCode,
llvm::StringMap<std::string> *EditedFiles) const {
std::string WrappedCode = wrap(Context, MarkedCode);
Annotations Input(WrappedCode);
TestTU TU;
TU.Filename = std::string(FileName);
TU.HeaderCode = Header;
TU.AdditionalFiles = std::move(ExtraFiles);
TU.Code = std::string(Input.code());
TU.ExtraArgs = ExtraArgs;
ParsedAST AST = TU.build();
auto Result = applyTweak(
AST, Input, TweakID, Index.get(),
&AST.getSourceManager().getFileManager().getVirtualFileSystem());
if (!Result)
return "unavailable";
if (!*Result)
return "fail: " + llvm::toString(Result->takeError());
const auto &Effect = **Result;
if ((*Result)->ShowMessage)
return "message:\n" + *Effect.ShowMessage;
if (Effect.ApplyEdits.empty())
return "no effect";
std::string EditedMainFile;
for (auto &It : Effect.ApplyEdits) {
auto NewText = It.second.apply();
if (!NewText)
return "bad edits: " + llvm::toString(NewText.takeError());
llvm::StringRef Unwrapped = unwrap(Context, *NewText);
if (It.first() == testPath(TU.Filename))
EditedMainFile = std::string(Unwrapped);
else {
if (!EditedFiles)
ADD_FAILURE() << "There were changes to additional files, but client "
"provided a nullptr for EditedFiles.";
else
EditedFiles->insert_or_assign(It.first(), Unwrapped.str());
}
}
return EditedMainFile;
}
::testing::Matcher<llvm::StringRef> TweakTest::isAvailable() const {
return TweakIsAvailable(llvm::StringRef(TweakID), Context, Header, ExtraArgs,
ExtraFiles, Index.get(), FileName);
}
std::vector<std::string> TweakTest::expandCases(llvm::StringRef MarkedCode) {
Annotations Test(MarkedCode);
llvm::StringRef Code = Test.code();
std::vector<std::string> Cases;
for (const auto &Point : Test.points()) {
size_t Offset = llvm::cantFail(positionToOffset(Code, Point));
Cases.push_back((Code.substr(0, Offset) + "^" + Code.substr(Offset)).str());
}
for (const auto &Range : Test.ranges()) {
size_t Begin = llvm::cantFail(positionToOffset(Code, Range.start));
size_t End = llvm::cantFail(positionToOffset(Code, Range.end));
Cases.push_back((Code.substr(0, Begin) + "[[" +
Code.substr(Begin, End - Begin) + "]]" + Code.substr(End))
.str());
}
assert(!Cases.empty() && "No markings in MarkedCode?");
return Cases;
}
} // namespace clangd
} // namespace clang