[clang-tools-extra] c69ae83 - [clangd] Add path mappings functionality
Sam McCall via cfe-commits
cfe-commits at lists.llvm.org
Tue Jan 7 03:41:30 PST 2020
Author: Sam McCall
Date: 2020-01-07T12:40:51+01:00
New Revision: c69ae835d0e0dc493eb09e75f0687a1390525440
URL: https://github.com/llvm/llvm-project/commit/c69ae835d0e0dc493eb09e75f0687a1390525440
DIFF: https://github.com/llvm/llvm-project/commit/c69ae835d0e0dc493eb09e75f0687a1390525440.diff
LOG: [clangd] Add path mappings functionality
Summary: Add path mappings to clangd which translate file URIs on inbound and outbound LSP messages. This mapping allows clangd to run in a remote environment (e.g. docker), where the source files and dependencies may be at different locations than the host. See http://lists.llvm.org/pipermail/clangd-dev/2019-January/000231.htm for more.
Patch by William Wagner!
Reviewers: sammccall, ilya-biryukov
Reviewed By: sammccall
Subscribers: usaxena95, ormris, mgorny, MaskRay, jkorous, arphaman, kadircet, cfe-commits
Tags: #clang
Differential Revision: https://reviews.llvm.org/D64305
Added:
clang-tools-extra/clangd/PathMapping.cpp
clang-tools-extra/clangd/PathMapping.h
clang-tools-extra/clangd/test/Inputs/path-mappings/server/foo.h
clang-tools-extra/clangd/test/path-mappings.test
clang-tools-extra/clangd/unittests/PathMappingTests.cpp
Modified:
clang-tools-extra/clangd/CMakeLists.txt
clang-tools-extra/clangd/tool/ClangdMain.cpp
clang-tools-extra/clangd/unittests/CMakeLists.txt
Removed:
################################################################################
diff --git a/clang-tools-extra/clangd/CMakeLists.txt b/clang-tools-extra/clangd/CMakeLists.txt
index c0ad99dd6b69..e3eccb50a496 100644
--- a/clang-tools-extra/clangd/CMakeLists.txt
+++ b/clang-tools-extra/clangd/CMakeLists.txt
@@ -62,6 +62,7 @@ add_clang_library(clangDaemon
IncludeFixer.cpp
JSONTransport.cpp
Logger.cpp
+ PathMapping.cpp
Protocol.cpp
Quality.cpp
ParsedAST.cpp
diff --git a/clang-tools-extra/clangd/PathMapping.cpp b/clang-tools-extra/clangd/PathMapping.cpp
new file mode 100644
index 000000000000..e130f3865c64
--- /dev/null
+++ b/clang-tools-extra/clangd/PathMapping.cpp
@@ -0,0 +1,199 @@
+//===--- PathMapping.cpp - apply path mappings to LSP messages -===//
+//
+// 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 "PathMapping.h"
+#include "Transport.h"
+#include "URI.h"
+#include "llvm/ADT/None.h"
+#include "llvm/ADT/STLExtras.h"
+#include "llvm/Support/Errno.h"
+#include "llvm/Support/Error.h"
+#include "llvm/Support/Path.h"
+#include <algorithm>
+#include <tuple>
+
+namespace clang {
+namespace clangd {
+llvm::Optional<std::string> doPathMapping(llvm::StringRef S,
+ PathMapping::Direction Dir,
+ const PathMappings &Mappings) {
+ // Retrun early to optimize for the common case, wherein S is not a file URI
+ if (!S.startswith("file://"))
+ return llvm::None;
+ auto Uri = URI::parse(S);
+ if (!Uri) {
+ llvm::consumeError(Uri.takeError());
+ return llvm::None;
+ }
+ for (const auto &Mapping : Mappings) {
+ const std::string &From = Dir == PathMapping::Direction::ClientToServer
+ ? Mapping.ClientPath
+ : Mapping.ServerPath;
+ const std::string &To = Dir == PathMapping::Direction::ClientToServer
+ ? Mapping.ServerPath
+ : Mapping.ClientPath;
+ llvm::StringRef Body = Uri->body();
+ if (Body.consume_front(From) && (Body.empty() || Body.front() == '/')) {
+ std::string MappedBody = (To + Body).str();
+ return URI(Uri->scheme(), Uri->authority(), MappedBody.c_str())
+ .toString();
+ }
+ }
+ return llvm::None;
+}
+
+void applyPathMappings(llvm::json::Value &V, PathMapping::Direction Dir,
+ const PathMappings &Mappings) {
+ using Kind = llvm::json::Value::Kind;
+ Kind K = V.kind();
+ if (K == Kind::Object) {
+ llvm::json::Object *Obj = V.getAsObject();
+ llvm::json::Object MappedObj;
+ // 1. Map all the Keys
+ for (auto &KV : *Obj) {
+ if (llvm::Optional<std::string> MappedKey =
+ doPathMapping(KV.first.str(), Dir, 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)
+ applyPathMappings(KV.second, Dir, Mappings);
+ } else if (K == Kind::Array) {
+ for (llvm::json::Value &Val : *V.getAsArray())
+ applyPathMappings(Val, Dir, Mappings);
+ } else if (K == Kind::String) {
+ if (llvm::Optional<std::string> Mapped =
+ doPathMapping(*V.getAsString(), Dir, Mappings))
+ V = std::move(*Mapped);
+ }
+}
+
+namespace {
+
+class PathMappingMessageHandler : public Transport::MessageHandler {
+public:
+ PathMappingMessageHandler(MessageHandler &Handler,
+ const PathMappings &Mappings)
+ : WrappedHandler(Handler), Mappings(Mappings) {}
+
+ bool onNotify(llvm::StringRef Method, llvm::json::Value Params) override {
+ applyPathMappings(Params, PathMapping::Direction::ClientToServer, Mappings);
+ return WrappedHandler.onNotify(Method, std::move(Params));
+ }
+
+ bool onCall(llvm::StringRef Method, llvm::json::Value Params,
+ llvm::json::Value ID) override {
+ applyPathMappings(Params, PathMapping::Direction::ClientToServer, Mappings);
+ return WrappedHandler.onCall(Method, std::move(Params), std::move(ID));
+ }
+
+ bool onReply(llvm::json::Value ID,
+ llvm::Expected<llvm::json::Value> Result) override {
+ if (Result)
+ applyPathMappings(*Result, PathMapping::Direction::ClientToServer,
+ Mappings);
+ return WrappedHandler.onReply(std::move(ID), std::move(Result));
+ }
+
+private:
+ Transport::MessageHandler &WrappedHandler;
+ const PathMappings &Mappings;
+};
+
+// Apply path mappings to all LSP messages by intercepting all params/results
+// and then delegating to the normal transport
+class PathMappingTransport : public Transport {
+public:
+ PathMappingTransport(std::unique_ptr<Transport> Transp, PathMappings Mappings)
+ : WrappedTransport(std::move(Transp)), Mappings(std::move(Mappings)) {}
+
+ void notify(llvm::StringRef Method, llvm::json::Value Params) override {
+ applyPathMappings(Params, PathMapping::Direction::ServerToClient, Mappings);
+ WrappedTransport->notify(Method, std::move(Params));
+ }
+
+ void call(llvm::StringRef Method, llvm::json::Value Params,
+ llvm::json::Value ID) override {
+ applyPathMappings(Params, PathMapping::Direction::ServerToClient, Mappings);
+ WrappedTransport->call(Method, std::move(Params), std::move(ID));
+ }
+
+ void reply(llvm::json::Value ID,
+ llvm::Expected<llvm::json::Value> Result) override {
+ if (Result)
+ applyPathMappings(*Result, PathMapping::Direction::ServerToClient,
+ Mappings);
+ WrappedTransport->reply(std::move(ID), std::move(Result));
+ }
+
+ llvm::Error loop(MessageHandler &Handler) override {
+ PathMappingMessageHandler WrappedHandler(Handler, Mappings);
+ return WrappedTransport->loop(WrappedHandler);
+ }
+
+private:
+ std::unique_ptr<Transport> WrappedTransport;
+ PathMappings Mappings;
+};
+
+// Converts a unix/windows path to the path portion of a file URI
+// e.g. "C:\foo" -> "/C:/foo"
+llvm::Expected<std::string> parsePath(llvm::StringRef Path) {
+ namespace path = llvm::sys::path;
+ 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 llvm::createStringError(llvm::inconvertibleErrorCode(),
+ "Path not absolute: " + Path);
+}
+
+} // namespace
+
+llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const PathMapping &M) {
+ return OS << M.ClientPath << "=" << M.ServerPath;
+}
+
+llvm::Expected<PathMappings>
+parsePathMappings(llvm::StringRef RawPathMappings) {
+ llvm::StringRef ClientPath, ServerPath, PathPair, Rest = RawPathMappings;
+ PathMappings ParsedMappings;
+ while (!Rest.empty()) {
+ std::tie(PathPair, Rest) = Rest.split(",");
+ std::tie(ClientPath, ServerPath) = PathPair.split("=");
+ if (ClientPath.empty() || ServerPath.empty())
+ return llvm::createStringError(llvm::inconvertibleErrorCode(),
+ "Not a valid path mapping pair: " +
+ PathPair);
+ llvm::Expected<std::string> ParsedClientPath = parsePath(ClientPath);
+ if (!ParsedClientPath)
+ return ParsedClientPath.takeError();
+ llvm::Expected<std::string> ParsedServerPath = parsePath(ServerPath);
+ if (!ParsedServerPath)
+ return ParsedServerPath.takeError();
+ ParsedMappings.push_back(
+ {std::move(*ParsedClientPath), std::move(*ParsedServerPath)});
+ }
+ return ParsedMappings;
+}
+
+std::unique_ptr<Transport>
+createPathMappingTransport(std::unique_ptr<Transport> Transp,
+ PathMappings Mappings) {
+ return std::make_unique<PathMappingTransport>(std::move(Transp), Mappings);
+}
+
+} // namespace clangd
+} // namespace clang
diff --git a/clang-tools-extra/clangd/PathMapping.h b/clang-tools-extra/clangd/PathMapping.h
new file mode 100644
index 000000000000..1d98feb7287f
--- /dev/null
+++ b/clang-tools-extra/clangd/PathMapping.h
@@ -0,0 +1,67 @@
+//===--- PathMapping.h - apply path mappings to LSP messages -===//
+//
+// 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 "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 <memory>
+#include <string>
+#include <vector>
+
+namespace clang {
+namespace clangd {
+
+class Transport;
+
+/// 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 client's environment may have source files or
+/// dependencies at
diff erent locations than the server. Therefore, both
+/// paths are stored as they appear in file URI bodies, e.g. /usr/include or
+/// /C:/config
+///
+/// For example, if the mappings were {{"/home/user", "/workarea"}}, then
+/// a client-to-server 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).
+struct PathMapping {
+ std::string ClientPath;
+ std::string ServerPath;
+ enum class Direction { ClientToServer, ServerToClient };
+};
+using PathMappings = std::vector<PathMapping>;
+
+llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const PathMapping &M);
+
+/// Parse the command line \p RawPathMappings (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<PathMappings> parsePathMappings(llvm::StringRef RawPathMappings);
+
+/// Returns a modified \p S with the first matching path in \p Mappings
+/// substituted, if applicable
+llvm::Optional<std::string> doPathMapping(llvm::StringRef S,
+ PathMapping::Direction Dir,
+ const PathMappings &Mappings);
+
+/// Applies the \p Mappings to all the file:// URIs in \p Params.
+/// NOTE: The first matching mapping will be applied, otherwise \p Params will
+/// be untouched.
+void applyPathMappings(llvm::json::Value &Params, PathMapping::Direction Dir,
+ const PathMappings &Mappings);
+
+/// Creates a wrapping transport over \p Transp that applies the \p Mappings to
+/// all inbound and outbound LSP messages. All calls are then delegated to the
+/// regular transport (e.g. XPC, JSON).
+std::unique_ptr<Transport>
+createPathMappingTransport(std::unique_ptr<Transport> Transp,
+ PathMappings Mappings);
+
+} // namespace clangd
+} // namespace clang
diff --git a/clang-tools-extra/clangd/test/Inputs/path-mappings/server/foo.h b/clang-tools-extra/clangd/test/Inputs/path-mappings/server/foo.h
new file mode 100644
index 000000000000..fe41b1f5dd3b
--- /dev/null
+++ b/clang-tools-extra/clangd/test/Inputs/path-mappings/server/foo.h
@@ -0,0 +1,4 @@
+#ifndef FOO_H
+#define FOO_H
+int foo() { return 42; }
+#endif
diff --git a/clang-tools-extra/clangd/test/path-mappings.test b/clang-tools-extra/clangd/test/path-mappings.test
new file mode 100644
index 000000000000..b0e54a61e067
--- /dev/null
+++ b/clang-tools-extra/clangd/test/path-mappings.test
@@ -0,0 +1,64 @@
+# Copy over the server file into test workspace
+# RUN: rm -rf %t
+# RUN: cp -r %S/Inputs/path-mappings %t
+#
+# RUN: clangd --path-mappings 'C:\client=%t/server' -lit-test < %s | FileCheck -strict-whitespace %s
+{"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"}
diff --git a/clang-tools-extra/clangd/tool/ClangdMain.cpp b/clang-tools-extra/clangd/tool/ClangdMain.cpp
index b8385a0c9e5d..c0c4c18b73c2 100644
--- a/clang-tools-extra/clangd/tool/ClangdMain.cpp
+++ b/clang-tools-extra/clangd/tool/ClangdMain.cpp
@@ -10,6 +10,7 @@
#include "CodeComplete.h"
#include "Features.inc"
#include "Path.h"
+#include "PathMapping.h"
#include "Protocol.h"
#include "Shutdown.h"
#include "Trace.h"
@@ -350,6 +351,18 @@ opt<bool> EnableTestScheme{
Hidden,
};
+opt<std::string> PathMappingsArg{
+ "path-mappings",
+ cat(Protocol),
+ desc(
+ "Translates between client paths (as seen by a remote editor) and "
+ "server paths (where clangd sees files on disk). "
+ "Comma separated list of '<client_path>=<server_path>' pairs, the "
+ "first entry matching a given path is used. "
+ "e.g. /home/project/incl=/opt/include,/home/project=/workarea/project"),
+ init(""),
+};
+
opt<Path> InputMirrorFile{
"input-mirror-file",
cat(Protocol),
@@ -654,7 +667,15 @@ clangd accepts flags on the commandline, and in the CLANGD_FLAGS environment var
InputMirrorStream ? InputMirrorStream.getPointer() : nullptr,
PrettyPrint, InputStyle);
}
-
+ if (!PathMappingsArg.empty()) {
+ auto Mappings = parsePathMappings(PathMappingsArg);
+ if (!Mappings) {
+ elog("Invalid -path-mappings: {0}", Mappings.takeError());
+ return 1;
+ }
+ TransportLayer = createPathMappingTransport(std::move(TransportLayer),
+ std::move(*Mappings));
+ }
// Create an empty clang-tidy option.
std::mutex ClangTidyOptMu;
std::unique_ptr<tidy::ClangTidyOptionsProvider>
diff --git a/clang-tools-extra/clangd/unittests/CMakeLists.txt b/clang-tools-extra/clangd/unittests/CMakeLists.txt
index f8b24c606962..62113c6e4bbd 100644
--- a/clang-tools-extra/clangd/unittests/CMakeLists.txt
+++ b/clang-tools-extra/clangd/unittests/CMakeLists.txt
@@ -55,6 +55,7 @@ add_unittest(ClangdUnitTests ClangdTests
IndexTests.cpp
JSONTransportTests.cpp
ParsedASTTests.cpp
+ PathMappingTests.cpp
PrintASTTests.cpp
QualityTests.cpp
RenameTests.cpp
diff --git a/clang-tools-extra/clangd/unittests/PathMappingTests.cpp b/clang-tools-extra/clangd/unittests/PathMappingTests.cpp
new file mode 100644
index 000000000000..3811776bed01
--- /dev/null
+++ b/clang-tools-extra/clangd/unittests/PathMappingTests.cpp
@@ -0,0 +1,216 @@
+//===-- PathMappingTests.cpp ------------------------*- C++ -*-----------===//
+//
+// 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 "PathMapping.h"
+#include "llvm/Support/JSON.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include <string>
+namespace clang {
+namespace clangd {
+namespace {
+using ::testing::ElementsAre;
+MATCHER_P2(Mapping, ClientPath, ServerPath, "") {
+ return arg.ClientPath == ClientPath && arg.ServerPath == ServerPath;
+}
+
+bool failedParse(llvm::StringRef RawMappings) {
+ llvm::Expected<PathMappings> 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<PathMappings> 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<PathMappings> 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="));
+ // mappings need to be absolute
+ EXPECT_TRUE(failedParse("home/project=/workarea/project"));
+ // duplicate delimiter
+ EXPECT_TRUE(failedParse("/home==/workarea"));
+ // no delimiter
+ EXPECT_TRUE(failedParse("/home"));
+ // improper delimiter
+ EXPECT_TRUE(failedParse("/home,/workarea"));
+}
+
+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(Mapping("/home/project", "/workarea/project"),
+ Mapping("/home/project/.includes", "/opt/include")));
+}
+
+bool mapsProperly(llvm::StringRef Orig, llvm::StringRef Expected,
+ llvm::StringRef RawMappings, PathMapping::Direction Dir) {
+ llvm::Expected<PathMappings> Mappings = parsePathMappings(RawMappings);
+ if (!Mappings)
+ return false;
+ llvm::Optional<std::string> MappedPath = doPathMapping(Orig, Dir, *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", "",
+ PathMapping::Direction::ClientToServer));
+}
+
+TEST(DoPathMappingTests, UsesFirstMatch) {
+ EXPECT_TRUE(mapsProperly("file:///home/foo.cpp", "file:///workarea1/foo.cpp",
+ "/home=/workarea1,/home=/workarea2",
+ PathMapping::Direction::ClientToServer));
+}
+
+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",
+ PathMapping::Direction::ClientToServer));
+}
+
+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",
+ PathMapping::Direction::ServerToClient));
+}
+
+TEST(DoPathMappingTests, OnlyMapFileUris) {
+ EXPECT_TRUE(mapsProperly("test:///home/foo.cpp", "test:///home/foo.cpp",
+ "/home=/workarea",
+ PathMapping::Direction::ClientToServer));
+}
+
+TEST(DoPathMappingTests, RespectsCaseSensitivity) {
+ EXPECT_TRUE(mapsProperly("file:///HOME/foo.cpp", "file:///HOME/foo.cpp",
+ "/home=/workarea",
+ PathMapping::Direction::ClientToServer));
+}
+
+TEST(DoPathMappingTests, MapsWindowsPaths) {
+ // Maps windows properly
+ EXPECT_TRUE(mapsProperly("file:///C:/home/foo.cpp",
+ "file:///C:/workarea/foo.cpp", R"(C:\home=C:\workarea)",
+ PathMapping::Direction::ClientToServer));
+}
+
+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:///workarea/foo.cpp",
+ R"(C:\home=/workarea)", PathMapping::Direction::ClientToServer));
+}
+
+TEST(ApplyPathMappingTests, PreservesOriginalParams) {
+ auto Params = llvm::json::parse(R"({
+ "textDocument": {"uri": "file:///home/foo.cpp"},
+ "position": {"line": 0, "character": 0}
+ })");
+ ASSERT_TRUE(bool(Params));
+ llvm::json::Value ExpectedParams = *Params;
+ PathMappings Mappings;
+ applyPathMappings(*Params, PathMapping::Direction::ClientToServer, Mappings);
+ EXPECT_EQ(*Params, ExpectedParams);
+}
+
+TEST(ApplyPathMappingTests, MapsAllMatchingPaths) {
+ // Handles nested objects and array values
+ auto Params = llvm::json::parse(R"({
+ "rootUri": {"uri": "file:///home/foo.cpp"},
+ "workspaceFolders": ["file:///home/src", "file:///tmp"]
+ })");
+ auto ExpectedParams = llvm::json::parse(R"({
+ "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, PathMapping::Direction::ClientToServer, *Mappings);
+ EXPECT_EQ(*Params, *ExpectedParams);
+}
+
+TEST(ApplyPathMappingTests, MapsOutbound) {
+ auto Params = llvm::json::parse(R"({
+ "id": 1,
+ "result": [
+ {"uri": "file:///opt/include/foo.h"},
+ {"uri": "file:///workarea/src/foo.cpp"}]
+ })");
+ auto ExpectedParams = llvm::json::parse(R"({
+ "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, PathMapping::Direction::ServerToClient, *Mappings);
+ EXPECT_EQ(*Params, *ExpectedParams);
+}
+
+TEST(ApplyPathMappingTests, MapsKeys) {
+ auto Params = llvm::json::parse(R"({
+ "changes": {
+ "file:///home/foo.cpp": {"newText": "..."},
+ "file:///home/src/bar.cpp": {"newText": "..."}
+ }
+ })");
+ auto ExpectedParams = llvm::json::parse(R"({
+ "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, PathMapping::Direction::ClientToServer, *Mappings);
+ EXPECT_EQ(*Params, *ExpectedParams);
+}
+
+} // namespace
+} // namespace clangd
+} // namespace clang
More information about the cfe-commits
mailing list