Index: clang-tools-extra/clangd/PathMapping.h =================================================================== --- clang-tools-extra/clangd/PathMapping.h +++ clang-tools-extra/clangd/PathMapping.h @@ -5,12 +5,13 @@ // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // //===----------------------------------------------------------------------===// +#include "llvm/ADT/Optional.h" #include "llvm/ADT/StringRef.h" #include "llvm/Support/Error.h" #include "llvm/Support/JSON.h" +#include "llvm/Support/raw_ostream.h" #include #include -#include #include namespace clang { @@ -18,29 +19,42 @@ class Transport; -/// PathMappings are a collection of paired host and remote paths. +/// PathMappings are a collection of paired client and server paths. /// These pairs are used to alter file:// URIs appearing in inbound and outbound -/// LSP messages, as the host environment may have source files or dependencies -/// at different locations than the remote. +/// LSP messages, as the client's environment may have source files or +/// dependencies at different locations than the server. /// /// For example, if the mappings were {{"/home/user", "/workarea"}}, then /// an inbound LSP message would have file:///home/user/foo.cpp remapped to /// file:///workarea/foo.cpp, and the same would happen for replies (in the /// opposite order). -using PathMappings = std::vector>; +struct PathMapping { + PathMapping() {} + PathMapping(llvm::StringRef ClientPath, llvm::StringRef ServerPath) + : ClientPath(ClientPath), ServerPath(ServerPath) {} + std::string ClientPath; + std::string ServerPath; +}; +using PathMappings = std::vector; -/// Parse the command line \pRawPathMappings (e.g. "/host|/remote") into +llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const PathMapping &M); + +/// Parse the command line \pRawPathMappings (e.g. "/client=/server") into /// pairs. Returns an error if the mappings are malformed, i.e. not absolute or /// not a proper pair. -llvm::Expected -parsePathMappings(const std::vector &RawPathMappings); +llvm::Expected parsePathMappings(llvm::StringRef RawPathMappings); + +/// Returns a modified \pS with the first matching path in \pPathMappings +/// substituted, if applicable +llvm::Optional doPathMapping(llvm::StringRef S, bool IsIncoming, + const PathMappings &Mappings); -/// Returns an altered \pParams, where all the file:// URIs have the \pMappings -/// applied. \pIsIncoming affects which direction the mappings are applied. +/// Applies the \pMappings to all the file:// URIs in \pParams. +/// \pIsIncoming affects which direction the mappings are applied. /// NOTE: The first matching mapping will be applied, otherwise \pParams will be /// untouched. -llvm::json::Value doPathMapping(const llvm::json::Value &Params, - bool IsIncoming, const PathMappings &Mappings); +void applyPathMappings(llvm::json::Value &Params, bool IsIncoming, + const PathMappings &Mappings); /// Creates a wrapping transport over \pTransp that applies the \pMappings to /// all inbound and outbound LSP messages. All calls are then delegated to the Index: clang-tools-extra/clangd/PathMapping.cpp =================================================================== --- clang-tools-extra/clangd/PathMapping.cpp +++ clang-tools-extra/clangd/PathMapping.cpp @@ -9,11 +9,16 @@ #include "Logger.h" #include "Transport.h" #include "URI.h" +#include "llvm/ADT/None.h" +#include "llvm/ADT/Optional.h" #include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallString.h" #include "llvm/ADT/StringRef.h" #include "llvm/Support/Errno.h" +#include "llvm/Support/JSON.h" #include "llvm/Support/Path.h" #include "llvm/Support/raw_ostream.h" +#include #include #include #include @@ -21,22 +26,40 @@ namespace clang { namespace clangd { namespace { +using MapperFunc = llvm::function_ref( + llvm::StringRef, bool, const PathMappings &)>; -// Recurively apply the \pMF function on every string value in \pV -template -void recursivelyMap(llvm::json::Value &V, const MapperFunc &MF) { +// Recursively apply the \pMF to all string keys/values in \pV +void recursivelyMap(llvm::json::Value &V, bool IsIncoming, + const PathMappings &Mappings, const MapperFunc &MF) { using Kind = llvm::json::Value::Kind; - const auto &K = V.kind(); + const Kind &K = V.kind(); if (K == Kind::Object) { - for (auto &KV : *V.getAsObject()) { - recursivelyMap(KV.second, MF); + llvm::json::Object *Obj = V.getAsObject(); + llvm::json::Object MappedObj; + // 1. Map all the Keys + for (auto &KV : *Obj) { + if (llvm::Optional MappedKey = + MF(KV.first.str(), IsIncoming, Mappings)) { + MappedObj.try_emplace(std::move(*MappedKey), std::move(KV.second)); + } else { + MappedObj.try_emplace(std::move(KV.first), std::move(KV.second)); + } + } + *Obj = std::move(MappedObj); + // 2. Map all the values + for (auto &KV : *Obj) { + recursivelyMap(KV.second, IsIncoming, Mappings, MF); } } else if (K == Kind::Array) { - for (auto &Val : *V.getAsArray()) { - recursivelyMap(Val, MF); + for (llvm::json::Value &Val : *V.getAsArray()) { + recursivelyMap(Val, IsIncoming, Mappings, MF); } } else if (K == Kind::String) { - V = MF(*V.getAsString()); + if (llvm::Optional Mapped = + MF(*V.getAsString(), IsIncoming, Mappings)) { + V = std::move(*Mapped); + } } } @@ -47,23 +70,20 @@ : WrappedHandler(Handler), Mappings(Mappings) {} bool onNotify(llvm::StringRef Method, llvm::json::Value Params) override { - llvm::json::Value MappedParams = - doPathMapping(Params, /*IsIncoming=*/true, Mappings); - return WrappedHandler.onNotify(Method, std::move(MappedParams)); + applyPathMappings(Params, /*IsIncoming=*/true, Mappings); + return WrappedHandler.onNotify(Method, std::move(Params)); } bool onCall(llvm::StringRef Method, llvm::json::Value Params, llvm::json::Value ID) override { - llvm::json::Value MappedParams = - doPathMapping(Params, /*IsIncoming=*/true, Mappings); - return WrappedHandler.onCall(Method, std::move(MappedParams), - std::move(ID)); + applyPathMappings(Params, /*IsIncoming=*/true, Mappings); + return WrappedHandler.onCall(Method, std::move(Params), std::move(ID)); } bool onReply(llvm::json::Value ID, llvm::Expected Result) override { if (Result) { - Result = doPathMapping(*Result, /*IsIncoming=*/true, Mappings); + applyPathMappings(*Result, /*IsIncoming=*/true, Mappings); } return WrappedHandler.onReply(std::move(ID), std::move(Result)); } @@ -81,22 +101,20 @@ : WrappedTransport(std::move(Transp)), Mappings(std::move(Mappings)) {} void notify(llvm::StringRef Method, llvm::json::Value Params) override { - llvm::json::Value MappedParams = - doPathMapping(Params, /*IsIncoming=*/false, Mappings); - WrappedTransport->notify(Method, std::move(MappedParams)); + applyPathMappings(Params, /*IsIncoming=*/false, Mappings); + WrappedTransport->notify(Method, std::move(Params)); } void call(llvm::StringRef Method, llvm::json::Value Params, llvm::json::Value ID) override { - llvm::json::Value MappedParams = - doPathMapping(Params, /*IsIncoming=*/false, Mappings); - WrappedTransport->call(Method, std::move(MappedParams), std::move(ID)); + applyPathMappings(Params, /*IsIncoming=*/false, Mappings); + WrappedTransport->call(Method, std::move(Params), std::move(ID)); } void reply(llvm::json::Value ID, llvm::Expected Result) override { if (Result) { - Result = doPathMapping(*Result, /*IsIncoming=*/false, Mappings); + applyPathMappings(*Result, /*IsIncoming=*/false, Mappings); } WrappedTransport->reply(std::move(ID), std::move(Result)); } @@ -116,71 +134,100 @@ llvm::inconvertibleErrorCode()); } +// Returns whether a path mapping should take place for \pOrigPath +// i.e. \pMappingPath is a proper sub-path of \p OrigPath +bool mappingMatches(llvm::StringRef OrigPath, llvm::StringRef MappingPath) { + namespace path = llvm::sys::path; + auto OrigPathItr = path::begin(OrigPath, path::Style::posix); + auto OrigPathEnd = path::end(OrigPath); + auto MappingPathItr = path::begin(MappingPath, path::Style::posix); + auto MappingPathEnd = path::end(MappingPath); + if (std::distance(OrigPathItr, OrigPathEnd) < + std::distance(MappingPathItr, MappingPathEnd)) { + return false; + } + return std::equal(MappingPathItr, MappingPathEnd, OrigPathItr); +} + +// Converts \pPath to a posix-style absolute, i.e. if it's a windows path +// then the backward-slash notation will be converted to forward slash +llvm::Expected parsePath(llvm::StringRef Path) { + namespace path = llvm::sys::path; + std::string ParsedPath; + if (path::is_absolute(Path, path::Style::posix)) { + return Path; + } else if (path::is_absolute(Path, path::Style::windows)) { + std::string Converted = path::convert_to_slash(Path, path::Style::windows); + if (Converted.front() != '/') { + Converted = "/" + Converted; + } + return Converted; + } + return make_string_error("Path not absolute: " + Path); +} + } // namespace +llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const PathMapping &M) { + return OS << M.ClientPath << ", " << M.ServerPath; +} + llvm::Expected -parsePathMappings(const std::vector &RawPathMappings) { - if (!RawPathMappings.size()) { - return make_string_error("Must provide at least one path mapping"); - } - llvm::StringRef HostPath, RemotePath; +parsePathMappings(llvm::StringRef RawPathMappings) { + llvm::StringRef ClientPath, ServerPath, PathPair, Rest = RawPathMappings; PathMappings ParsedMappings; - for (llvm::StringRef PathPair : RawPathMappings) { - std::tie(HostPath, RemotePath) = PathPair.split("|"); - if (HostPath.empty() || RemotePath.empty()) { - return make_string_error("Not a valid path mapping: " + PathPair); + while (!Rest.empty()) { + std::tie(PathPair, Rest) = Rest.split(","); + std::tie(ClientPath, ServerPath) = PathPair.split("="); + if (ClientPath.empty() || ServerPath.empty()) { + return make_string_error("Not a valid path mapping pair: " + PathPair); } - if (!llvm::sys::path::is_absolute(HostPath)) { - return make_string_error("Path mapping not absolute: " + HostPath); - } else if (!llvm::sys::path::is_absolute(RemotePath)) { - return make_string_error("Path mapping not absolute: " + RemotePath); + llvm::Expected ParsedClientPath = parsePath(ClientPath); + if (!ParsedClientPath) { + return ParsedClientPath.takeError(); } - ParsedMappings.emplace_back(HostPath, RemotePath); + llvm::Expected ParsedServerPath = parsePath(ServerPath); + if (!ParsedServerPath) { + return ParsedServerPath.takeError(); + } + ParsedMappings.emplace_back(std::move(*ParsedClientPath), + std::move(*ParsedServerPath)); } - std::string S; - llvm::raw_string_ostream OS(S); - OS << "Parsed path mappings: "; - for (const auto &P : ParsedMappings) - OS << llvm::formatv("{0}:{1} ", P.first, P.second); - OS.flush(); - vlog("{0}", OS.str()); return ParsedMappings; } -llvm::json::Value doPathMapping(const llvm::json::Value &Params, - bool IsIncoming, const PathMappings &Mappings) { - llvm::json::Value MappedParams = Params; - recursivelyMap( - MappedParams, [&Mappings, IsIncoming](llvm::StringRef S) -> std::string { - if (!S.startswith("file://")) - return S; - auto Uri = URI::parse(S); - if (!Uri) { - vlog("Faled to parse URI: {0}\n", S); - return S; - } - for (const auto &Mapping : Mappings) { - const auto &From = IsIncoming ? Mapping.first : Mapping.second; - const auto &To = IsIncoming ? Mapping.second : Mapping.first; - if (Uri->body().startswith(From)) { - std::string MappedBody = Uri->body(); - MappedBody.replace(MappedBody.find(From), From.length(), To); - auto MappedUri = URI(Uri->scheme(), Uri->authority(), MappedBody); - vlog("Mapped {0} file path from {1} to {2}", - IsIncoming ? "incoming" : "outgoing", Uri->toString(), - MappedUri.toString()); - return MappedUri.toString(); - } - } - return S; - }); - return MappedParams; +llvm::Optional doPathMapping(llvm::StringRef S, bool IsIncoming, + const PathMappings &Mappings) { + if (!S.startswith("file://")) + return llvm::None; + auto Uri = URI::parse(S); + if (!Uri) { + return llvm::None; + } + for (const auto &Mapping : Mappings) { + const std::string &From = + IsIncoming ? Mapping.ClientPath : Mapping.ServerPath; + const std::string &To = + IsIncoming ? Mapping.ServerPath : Mapping.ClientPath; + if (mappingMatches(Uri->body(), From)) { + llvm::SmallString<256> MappedPath(Uri->body()); + llvm::sys::path::replace_path_prefix(MappedPath, From, To); + auto MappedUri = URI(Uri->scheme(), Uri->authority(), MappedPath.c_str()); + return MappedUri.toString(); + } + } + return llvm::None; +} + +void applyPathMappings(llvm::json::Value &Params, bool IsIncoming, + const PathMappings &Mappings) { + recursivelyMap(Params, IsIncoming, Mappings, doPathMapping); } std::unique_ptr createPathMappingTransport(std::unique_ptr Transp, PathMappings Mappings) { - return llvm::make_unique(std::move(Transp), Mappings); + return std::make_unique(std::move(Transp), Mappings); } } // namespace clangd Index: clang-tools-extra/clangd/test/Inputs/path-mappings/compile_commands.json =================================================================== --- clang-tools-extra/clangd/test/Inputs/path-mappings/compile_commands.json +++ /dev/null @@ -1,5 +0,0 @@ -[{ - "directory": "DIRECTORY", - "command": "clang DIRECTORY/remote/foo.cpp", - "file": "DIRECTORY/remote/foo.cpp" -}] Index: clang-tools-extra/clangd/test/Inputs/path-mappings/definition.jsonrpc =================================================================== --- clang-tools-extra/clangd/test/Inputs/path-mappings/definition.jsonrpc +++ /dev/null @@ -1,51 +0,0 @@ -{ - "jsonrpc": "2.0", - "id": 0, - "method": "initialize", - "params": { - "processId": 123, - "rootPath": "clangd", - "capabilities": {}, - "trace": "off" - } -} ---- -{ - "jsonrpc": "2.0", - "method": "textDocument/didOpen", - "params": { - "textDocument": { - "uri": "file://DIRECTORY/host/bar.cpp", - "languageId": "cpp", - "version": 1, - "text": "#include \"foo.h\"\nint main(){\nreturn foo();\n}" - } - } -} ---- -{ - "jsonrpc": "2.0", - "id": 1, - "method": "sync", - "params": null -} ---- -{ - "jsonrpc": "2.0", - "id": 2, - "method": "textDocument/definition", - "params": { - "textDocument": { - "uri": "file://DIRECTORY/host/bar.cpp" - }, - "position": { - "line": 2, - "character": 8 - } - } -} -# CHECK: "uri": "file://{{.*}}/host/foo.cpp" ---- -{"jsonrpc":"2.0","id":3,"method":"shutdown"} ---- -{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/clangd/test/Inputs/path-mappings/remote/foo.cpp =================================================================== --- clang-tools-extra/clangd/test/Inputs/path-mappings/remote/foo.cpp +++ /dev/null @@ -1,2 +0,0 @@ -#include "foo.h" -int foo() { return 42; } Index: clang-tools-extra/clangd/test/Inputs/path-mappings/server/foo.h =================================================================== --- clang-tools-extra/clangd/test/Inputs/path-mappings/server/foo.h +++ clang-tools-extra/clangd/test/Inputs/path-mappings/server/foo.h @@ -1,4 +1,4 @@ #ifndef FOO_H #define FOO_H -int foo(); +int foo() { return 42; } #endif Index: clang-tools-extra/clangd/test/path-mappings.test =================================================================== --- clang-tools-extra/clangd/test/path-mappings.test +++ clang-tools-extra/clangd/test/path-mappings.test @@ -1,13 +1,65 @@ -# We need to splice paths into file:// URIs for this test. -# UNSUPPORTED: win32 - -# Use a copy of inputs, as we'll mutate it +# RUN: clangd --path-mappings 'C:\client=%t/server' -lit-test < %s | FileCheck -strict-whitespace %s +# Copy over the server file into test workspace # RUN: rm -rf %t # RUN: cp -r %S/Inputs/path-mappings %t -# Need to embed the correct temp path in the actual JSON-RPC requests. -# RUN: sed -i "s|DIRECTORY|%t|" `find %t -type f` - -# We're editing bar.cpp, which includes foo.h, where foo.h/cpp only "exist" in the remote. -# With path mappings, when we go to definition on foo(), we get back a host file uri -# RUN: clangd -background-index -background-index-rebuild-period=0 --path-mappings '%t/host|%t/remote' -lit-test < %t/definition.jsonrpc | FileCheck %t/definition.jsonrpc - +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}} +--- +{ + "jsonrpc": "2.0", + "method": "textDocument/didOpen", + "params": { + "textDocument": { + "uri": "file:///C:/client/bar.cpp", + "languageId": "cpp", + "version": 1, + "text": "#include \"foo.h\"\nint main(){\nreturn foo();\n}" + } + } +} +--- +# Ensure that the client gets back the same client path (clangd thinks it edited %t/server/bar.cpp) +# CHECK: "method": "textDocument/publishDiagnostics", +# CHECK-NEXT: "params": { +# CHECK-NEXT: "diagnostics": [], +# CHECK-NEXT: "uri": "file:///C:/client/bar.cpp" +# CHECK-NEXT: } +--- +# We're editing bar.cpp, which includes foo.h, where foo.h "exists" at a server location +# With path mappings, when we go to definition on foo(), we get back a client file uri +{ + "jsonrpc": "2.0", + "id": 1, + "method": "textDocument/definition", + "params": { + "textDocument": { + "uri": "file:///C:/client/bar.cpp" + }, + "position": { + "line": 2, + "character": 8 + } + } +} +--- +# CHECK: "id": 1, +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "result": [ +# CHECK-NEXT: { +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": {{[0-9]+}}, +# CHECK-NEXT: "line": {{[0-9]+}} +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": {{[0-9]+}}, +# CHECK-NEXT: "line": {{[0-9]+}} +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "uri": "file:///C:/client/foo.h" +# CHECK-NEXT: } +# CHECK-NEXT: ] +# +--- +{"jsonrpc":"2.0","id":2,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/clangd/tool/ClangdMain.cpp =================================================================== --- clang-tools-extra/clangd/tool/ClangdMain.cpp +++ clang-tools-extra/clangd/tool/ClangdMain.cpp @@ -336,16 +336,18 @@ Hidden, }; -static llvm::cl::list PathMappingsArg( +static llvm::cl::opt PathMappingsArg( "path-mappings", - llvm::cl::desc("Comma separated list of '|' pairs " - "that can be used to map between file locations on the host " - "and and a remote " - "location where clangd is running," - "e.g. " - "/home/project/|/workarea/project,/home/project/.includes|/" - "opt/include"), - llvm::cl::CommaSeparated); + cat(Protocol), + llvm::cl::desc( + "Comma separated list of '=' pairs " + "that can be used to map between file locations on the client " + "and and a remote " + "location where clangd is running," + "e.g. " + "/home/project=/workarea/project,/home/project/.includes=/opt/include" + "opt/include"), + llvm::cl::init("")); opt InputMirrorFile{ "input-mirror-file", @@ -396,8 +398,8 @@ getAbsolutePath(llvm::StringRef /*Authority*/, llvm::StringRef Body, llvm::StringRef /*HintPath*/) const override { using namespace llvm::sys; - // Still require "/" in body to mimic file scheme, as we want lengths of an - // equivalent URI in both schemes to be the same. + // Still require "/" in body to mimic file scheme, as we want lengths of + // an equivalent URI in both schemes to be the same. if (!Body.startswith("/")) return llvm::make_error( "Expect URI body to be an absolute path starting with '/': " + Body, @@ -645,7 +647,7 @@ InputMirrorStream ? InputMirrorStream.getPointer() : nullptr, PrettyPrint, InputStyle); } - if (PathMappingsArg.size()) { + if (!PathMappingsArg.empty()) { auto Mappings = parsePathMappings(PathMappingsArg); if (!Mappings) { auto Err = Mappings.takeError(); Index: clang-tools-extra/clangd/unittests/PathMappingTests.cpp =================================================================== --- clang-tools-extra/clangd/unittests/PathMappingTests.cpp +++ clang-tools-extra/clangd/unittests/PathMappingTests.cpp @@ -10,121 +10,200 @@ #include "llvm/Support/JSON.h" #include "gmock/gmock.h" #include "gtest/gtest.h" - +#include namespace clang { namespace clangd { namespace { using ::testing::ElementsAre; -using ::testing::Pair; - -TEST(ParsePathMappingTests, ParseFailed) { - auto FailedParse = [](const std::vector &RawMappings) { - auto Mappings = parsePathMappings(RawMappings); - if (!Mappings) { - consumeError(Mappings.takeError()); - return true; - } - return false; - }; +MATCHER_P2(Mapping, ClientPath, ServerPath, "") { + return arg.ClientPath == ClientPath && arg.ServerPath == ServerPath; +} + +bool failedParse(llvm::StringRef RawMappings) { + llvm::Expected Mappings = parsePathMappings(RawMappings); + if (!Mappings) { + consumeError(Mappings.takeError()); + return true; + } + return false; +} + +TEST(ParsePathMappingTests, WindowsPath) { + // Relative path to C drive + EXPECT_TRUE(failedParse(R"(C:a=/root)")); + EXPECT_TRUE(failedParse(R"(\C:a=/root)")); + // Relative path to current drive. + EXPECT_TRUE(failedParse(R"(\a=/root)")); + // Absolute paths + llvm::Expected ParsedMappings = + parsePathMappings(R"(C:\a=/root)"); + ASSERT_TRUE(bool(ParsedMappings)); + EXPECT_THAT(*ParsedMappings, ElementsAre(Mapping("/C:/a", "/root"))); + // Absolute UNC path + ParsedMappings = parsePathMappings(R"(\\Server\C$=/root)"); + ASSERT_TRUE(bool(ParsedMappings)); + EXPECT_THAT(*ParsedMappings, ElementsAre(Mapping("//Server/C$", "/root"))); +} + +TEST(ParsePathMappingTests, UnixPath) { + // Relative unix path + EXPECT_TRUE(failedParse("a/b=/root")); + // Absolute unix path + llvm::Expected ParsedMappings = parsePathMappings("/A/b=/root"); + ASSERT_TRUE(bool(ParsedMappings)); + EXPECT_THAT(*ParsedMappings, ElementsAre(Mapping("/A/b", "/root"))); + // Aboslute unix path w/ backslash + ParsedMappings = parsePathMappings(R"(/a/b\\ar=/root)"); + ASSERT_TRUE(bool(ParsedMappings)); + EXPECT_THAT(*ParsedMappings, ElementsAre(Mapping(R"(/a/b\\ar)", "/root"))); +} + +TEST(ParsePathMappingTests, ImproperFormat) { // uneven mappings - EXPECT_TRUE(FailedParse({"/home/myuser1|"})); + EXPECT_TRUE(failedParse("/home/myuser1=")); // mappings need to be absolute - EXPECT_TRUE(FailedParse({"home/project|/workarea/project"})); - // improper delimiter - EXPECT_TRUE(FailedParse({"/home||/workarea"})); + EXPECT_TRUE(failedParse("home/project=/workarea/project")); + // duplicate delimiter + EXPECT_TRUE(failedParse("/home==/workarea")); // no delimiter - EXPECT_TRUE(FailedParse({"/home"})); + EXPECT_TRUE(failedParse("/home")); + // improper delimiter + EXPECT_TRUE(failedParse("/home,/workarea")); } -TEST(ParsePathMappingTests, AllowsWindowsAndUnixPaths) { - std::vector RawPathMappings = { - "/C:/home/project|/workarea/project", - "/home/project/.includes|/C:/opt/include"}; +TEST(ParsePathMappingTests, ParsesMultiple) { + std::string RawPathMappings = + "/home/project=/workarea/project,/home/project/.includes=/opt/include"; auto Parsed = parsePathMappings(RawPathMappings); ASSERT_TRUE(bool(Parsed)); EXPECT_THAT(*Parsed, - ElementsAre(Pair("/C:/home/project", "/workarea/project"), - Pair("/home/project/.includes", "/C:/opt/include"))); + ElementsAre(Mapping("/home/project", "/workarea/project"), + Mapping("/home/project/.includes", "/opt/include"))); } -TEST(ParsePathMappingTests, ParsesCorrectly) { - std::vector RawPathMappings = { - "/home/project|/workarea/project", - "/home/project/.includes|/opt/include"}; - auto Parsed = parsePathMappings(RawPathMappings); - ASSERT_TRUE(bool(Parsed)); - EXPECT_THAT(*Parsed, - ElementsAre(Pair("/home/project", "/workarea/project"), - Pair("/home/project/.includes", "/opt/include"))); +bool mapsProperly(llvm::StringRef Orig, llvm::StringRef Expected, + llvm::StringRef RawMappings, bool IsIncoming) { + llvm::Expected Mappings = parsePathMappings(RawMappings); + if (!Mappings) + return false; + llvm::Optional MappedPath = + doPathMapping(Orig, IsIncoming, *Mappings); + std::string Actual = MappedPath ? *MappedPath : Orig.str(); + EXPECT_STREQ(Expected.str().c_str(), Actual.c_str()); + return Expected == Actual; +} + +TEST(DoPathMappingTests, PreservesOriginal) { + // Preserves original path when no mapping + EXPECT_TRUE(mapsProperly("file:///home", "file:///home", "", true)); +} + +TEST(DoPathMappingTests, UsesFirstMatch) { + EXPECT_TRUE(mapsProperly("file:///home/foo.cpp", "file:///workarea1/foo.cpp", + "/home=/workarea1,/home=/workarea2", true)); +} + +TEST(DoPathMappingTests, IgnoresSubstrings) { + // Doesn't map substrings that aren't a proper path prefix + EXPECT_TRUE(mapsProperly("file://home/foo-bar.cpp", "file://home/foo-bar.cpp", + "/home/foo=/home/bar", true)); } -TEST(DoPathMappingTests, PreservesOriginalParams) { +TEST(DoPathMappingTests, MapsOutgoingPaths) { + // When IsIncoming is false (i.e.a response), map the other way + EXPECT_TRUE(mapsProperly("file:///workarea/foo.cpp", "file:///home/foo.cpp", + "/home=/workarea", false)); +} + +TEST(DoPathMappingTests, OnlyMapFileUris) { + EXPECT_TRUE(mapsProperly("test:///home/foo.cpp", "test:///home/foo.cpp", + "/home=/workarea", true)); +} + +TEST(DoPathMappingTests, RespectsCaseSensitivity) { + EXPECT_TRUE(mapsProperly("file:///HOME/foo.cpp", "file:///HOME/foo.cpp", + "/home=/workarea", true)); +} + +TEST(DoPathMappingTests, MapsWindowsPaths) { + // Maps windows properly + EXPECT_TRUE(mapsProperly("file:///C:/home/foo.cpp", + "file:///C:/workarea/foo.cpp", "C:/home=C:/workarea", + true)); +} + +TEST(DoPathMappingTests, MapsWindowsUnixInterop) { + // Path mappings with a windows-style client path and unix-style server path + EXPECT_TRUE(mapsProperly("file:///C:/home/foo.cpp", + "file:///C:/workarea/foo.cpp", + "C:/home=/C:/workarea", true)); +} + +TEST(ApplyPathMappingTests, PreservesOriginalParams) { auto Params = llvm::json::parse(R"({ "textDocument": {"uri": "file:///home/foo.cpp"}, "position": {"line": 0, "character": 0} })"); ASSERT_TRUE(bool(Params)); - auto MappedParams = - doPathMapping(*Params, /*IsIncoming=*/true, /*Mappings=*/{}); - EXPECT_EQ(MappedParams, *Params); -} - -TEST(DoPathMappingTests, MapsUsingFirstMatch) { - auto Params = llvm::json::parse(R"({ - "textDocument": {"uri": "file:///home/project/foo.cpp"}, - "position": {"line": 0, "character": 0} - })"); - auto ExpectedParams = llvm::json::parse(R"({ - "textDocument": {"uri": "file:///workarea1/project/foo.cpp"}, - "position": {"line": 0, "character": 0} - })"); - ASSERT_TRUE(bool(Params) && bool(ExpectedParams)); - PathMappings Mappings{{"/home", "/workarea1"}, {"/home", "/workarea2"}}; - auto MappedParams = doPathMapping(*Params, /*IsIncoming=*/true, Mappings); - EXPECT_EQ(MappedParams, *ExpectedParams); + llvm::json::Value ExpectedParams = *Params; + PathMappings Mappings; + applyPathMappings(*Params, /*IsIncoming=*/true, Mappings); + EXPECT_EQ(*Params, ExpectedParams); } -TEST(DoPathMappingTests, MapsOutgoing) { +TEST(ApplyPathMappingTests, MapsAllMatchingPaths) { + // Handles nested objects and array values auto Params = llvm::json::parse(R"({ - "result": "file:///opt/include/foo.h" - })"); + "rootUri": {"uri": "file:///home/foo.cpp"}, + "workspaceFolders": ["file:///home/src", "file:///tmp"] + })"); auto ExpectedParams = llvm::json::parse(R"({ - "result": "file:///home/project/.includes/foo.h" - })"); - ASSERT_TRUE(bool(Params) && bool(ExpectedParams)); - PathMappings Mappings{{"/home/project/.includes", "/opt/include"}}; - auto MappedParams = doPathMapping(*Params, /*IsIncoming=*/false, Mappings); - EXPECT_EQ(MappedParams, *ExpectedParams); + "rootUri": {"uri": "file:///workarea/foo.cpp"}, + "workspaceFolders": ["file:///workarea/src", "file:///tmp"] + })"); + auto Mappings = parsePathMappings("/home=/workarea"); + ASSERT_TRUE(bool(Params) && bool(ExpectedParams) && bool(Mappings)); + applyPathMappings(*Params, /*IsIncoming=*/true, *Mappings); + EXPECT_EQ(*Params, *ExpectedParams); } -TEST(DoPathMappingTests, MapsAllMatchingPaths) { +TEST(ApplyPathMappingTests, MapsOutbound) { auto Params = llvm::json::parse(R"({ - "rootUri": "file:///home/project", - "workspaceFolders": ["file:///home/misc/project2"] - })"); + "id": 1, + "result": [ + {"uri": "file:///opt/include/foo.h"}, + {"uri": "file:///workarea/src/foo.cpp"}] + })"); auto ExpectedParams = llvm::json::parse(R"({ - "rootUri": "file:///workarea/project", - "workspaceFolders": ["file:///workarea/misc/project2"] - })"); - ASSERT_TRUE(bool(Params) && bool(ExpectedParams)); - PathMappings Mappings{{"/home", "/workarea"}}; - auto MappedParams = doPathMapping(*Params, /*IsIncoming=*/true, Mappings); - EXPECT_EQ(MappedParams, *ExpectedParams); + "id": 1, + "result": [ + {"uri": "file:///home/.includes/foo.h"}, + {"uri": "file:///home/src/foo.cpp"}] + })"); + auto Mappings = + parsePathMappings("/home=/workarea,/home/.includes=/opt/include"); + ASSERT_TRUE(bool(Params) && bool(ExpectedParams) && bool(Mappings)); + applyPathMappings(*Params, /*IsIncoming=*/false, *Mappings); + EXPECT_EQ(*Params, *ExpectedParams); } -TEST(DoPathMappingTests, OnlyMapsFileUris) { +TEST(ApplyPathMappingTests, MapsKeys) { auto Params = llvm::json::parse(R"({ - "rootUri": "file:///home/project", - "workspaceFolders": ["test:///home/misc/project2"] - })"); + "changes": { + "file:///home/foo.cpp": {"newText": "..."}, + "file:///home/src/bar.cpp": {"newText": "..."} + } + })"); auto ExpectedParams = llvm::json::parse(R"({ - "rootUri": "file:///workarea/project", - "workspaceFolders": ["test:///home/misc/project2"] - })"); - ASSERT_TRUE(bool(Params) && bool(ExpectedParams)); - PathMappings Mappings{{"/home", "/workarea"}}; - auto MappedParams = doPathMapping(*Params, /*IsIncoming=*/true, Mappings); - EXPECT_EQ(MappedParams, *ExpectedParams); + "changes": { + "file:///workarea/foo.cpp": {"newText": "..."}, + "file:///workarea/src/bar.cpp": {"newText": "..."} + } + })"); + auto Mappings = parsePathMappings("/home=/workarea"); + ASSERT_TRUE(bool(Params) && bool(ExpectedParams) && bool(Mappings)); + applyPathMappings(*Params, /*IsIncoming=*/true, *Mappings); + EXPECT_EQ(*Params, *ExpectedParams); } } // namespace