[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