wwagner19 created this revision.
wwagner19 added reviewers: sammccall, ilya-biryukov.
Herald added subscribers: cfe-commits, kadircet, arphaman, jkorous, MaskRay, 
mgorny.
Herald added a project: clang.

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.


Repository:
  rCTE Clang Tools Extra

https://reviews.llvm.org/D64305

Files:
  clang-tools-extra/clangd/CMakeLists.txt
  clang-tools-extra/clangd/PathMapping.cpp
  clang-tools-extra/clangd/PathMapping.h
  clang-tools-extra/clangd/test/Inputs/path-mappings/compile_commands.json
  clang-tools-extra/clangd/test/Inputs/path-mappings/definition.jsonrpc
  clang-tools-extra/clangd/test/Inputs/path-mappings/remote/foo.cpp
  clang-tools-extra/clangd/test/Inputs/path-mappings/remote/foo.h
  clang-tools-extra/clangd/test/path-mappings.test
  clang-tools-extra/clangd/tool/ClangdMain.cpp
  clang-tools-extra/clangd/unittests/CMakeLists.txt
  clang-tools-extra/clangd/unittests/PathMappingTests.cpp

Index: clang-tools-extra/clangd/unittests/PathMappingTests.cpp
===================================================================
--- /dev/null
+++ clang-tools-extra/clangd/unittests/PathMappingTests.cpp
@@ -0,0 +1,132 @@
+//===-- 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"
+
+namespace clang {
+namespace clangd {
+namespace {
+using ::testing::ElementsAre;
+using ::testing::Pair;
+
+TEST(ParsePathMappingTests, ParseFailed) {
+  auto FailedParse = [](const std::vector<std::string> &RawMappings) {
+    auto Mappings = parsePathMappings(RawMappings);
+    if (!Mappings) {
+      consumeError(Mappings.takeError());
+      return true;
+    }
+    return false;
+  };
+  // uneven mappings
+  EXPECT_TRUE(FailedParse({"/home/myuser1|"}));
+  // mappings need to be absolute
+  EXPECT_TRUE(FailedParse({"home/project|/workarea/project"}));
+  // improper delimiter
+  EXPECT_TRUE(FailedParse({"/home||/workarea"}));
+  // no delimiter
+  EXPECT_TRUE(FailedParse({"/home"}));
+}
+
+TEST(ParsePathMappingTests, AllowsWindowsAndUnixPaths) {
+  std::vector<std::string> RawPathMappings = {
+      "/C:/home/project|/workarea/project",
+      "/home/project/.includes|/C:/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")));
+}
+
+TEST(ParsePathMappingTests, ParsesCorrectly) {
+  std::vector<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("/home/project", "/workarea/project"),
+                          Pair("/home/project/.includes", "/opt/include")));
+}
+
+TEST(DoPathMappingTests, 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);
+}
+
+TEST(DoPathMappingTests, MapsOutgoing) {
+  auto Params = llvm::json::parse(R"({
+        "result": "file:///opt/include/foo.h"
+    })");
+  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);
+}
+
+TEST(DoPathMappingTests, MapsAllMatchingPaths) {
+  auto Params = llvm::json::parse(R"({
+        "rootUri": "file:///home/project",
+        "workspaceFolders": ["file:///home/misc/project2"]
+    })");
+  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);
+}
+
+TEST(DoPathMappingTests, OnlyMapsFileUris) {
+  auto Params = llvm::json::parse(R"({
+        "rootUri": "file:///home/project",
+        "workspaceFolders": ["test:///home/misc/project2"]
+    })");
+  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);
+}
+
+} // namespace
+} // namespace clangd
+} // namespace clang
Index: clang-tools-extra/clangd/unittests/CMakeLists.txt
===================================================================
--- clang-tools-extra/clangd/unittests/CMakeLists.txt
+++ clang-tools-extra/clangd/unittests/CMakeLists.txt
@@ -48,6 +48,7 @@
   IndexActionTests.cpp
   IndexTests.cpp
   JSONTransportTests.cpp
+  PathMappingTests.cpp
   PrintASTTests.cpp
   QualityTests.cpp
   RenameTests.cpp
Index: clang-tools-extra/clangd/tool/ClangdMain.cpp
===================================================================
--- clang-tools-extra/clangd/tool/ClangdMain.cpp
+++ 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 "Trace.h"
 #include "Transport.h"
@@ -278,6 +279,16 @@
         "will be used to extract system includes. e.g. "
         "/usr/bin/**/clang-*,/path/to/repo/**/g++-*"),
     llvm::cl::CommaSeparated);
+static llvm::cl::list<std::string> PathMappingsArg(
+    "path-mappings",
+    llvm::cl::desc("Comma separated list of '<host_path>|<remote_path>' 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);
 
 static llvm::cl::list<std::string> TweakList(
     "tweaks",
@@ -518,7 +529,16 @@
         InputMirrorStream ? InputMirrorStream.getPointer() : nullptr,
         PrettyPrint, InputStyle);
   }
-
+  if (PathMappingsArg.size()) {
+    auto Mappings = parsePathMappings(PathMappingsArg);
+    if (!Mappings) {
+      auto Err = Mappings.takeError();
+      llvm::errs() << llvm::toString(std::move(Err)) << "\n";
+      return 1;
+    }
+    TransportLayer =
+        createPathMappingTransport(std::move(TransportLayer), *Mappings);
+  }
   // Create an empty clang-tidy option.
   std::mutex ClangTidyOptMu;
   std::unique_ptr<tidy::ClangTidyOptionsProvider>
Index: clang-tools-extra/clangd/test/path-mappings.test
===================================================================
--- /dev/null
+++ clang-tools-extra/clangd/test/path-mappings.test
@@ -0,0 +1,13 @@
+# We need to splice paths into file:// URIs for this test.
+# UNSUPPORTED: win32
+
+# Use a copy of inputs, as we'll mutate it
+# 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
+
Index: clang-tools-extra/clangd/test/Inputs/path-mappings/remote/foo.h
===================================================================
--- /dev/null
+++ clang-tools-extra/clangd/test/Inputs/path-mappings/remote/foo.h
@@ -0,0 +1,4 @@
+#ifndef FOO_H
+#define FOO_H
+int foo();
+#endif
Index: clang-tools-extra/clangd/test/Inputs/path-mappings/remote/foo.cpp
===================================================================
--- /dev/null
+++ clang-tools-extra/clangd/test/Inputs/path-mappings/remote/foo.cpp
@@ -0,0 +1,2 @@
+#include "foo.h"
+int foo() { return 42; }
Index: clang-tools-extra/clangd/test/Inputs/path-mappings/definition.jsonrpc
===================================================================
--- /dev/null
+++ clang-tools-extra/clangd/test/Inputs/path-mappings/definition.jsonrpc
@@ -0,0 +1,51 @@
+{
+  "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/compile_commands.json
===================================================================
--- /dev/null
+++ clang-tools-extra/clangd/test/Inputs/path-mappings/compile_commands.json
@@ -0,0 +1,5 @@
+[{
+  "directory": "DIRECTORY",
+  "command": "clang DIRECTORY/remote/foo.cpp",
+  "file": "DIRECTORY/remote/foo.cpp"
+}]
Index: clang-tools-extra/clangd/PathMapping.h
===================================================================
--- /dev/null
+++ clang-tools-extra/clangd/PathMapping.h
@@ -0,0 +1,53 @@
+//===--- 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/StringRef.h"
+#include "llvm/Support/Error.h"
+#include "llvm/Support/JSON.h"
+#include <memory>
+#include <tuple>
+#include <utility>
+#include <vector>
+
+namespace clang {
+namespace clangd {
+
+class Transport;
+
+/// PathMappings are a collection of paired host and remote 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.
+///
+/// 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<std::pair<std::string, std::string>>;
+
+/// Parse the command line \pRawPathMappings (e.g. "/host|/remote") into
+/// pairs. Returns an error if the mappings are malformed, i.e. not absolute or
+/// not a proper pair.
+llvm::Expected<PathMappings>
+parsePathMappings(const std::vector<std::string> &RawPathMappings);
+
+/// Returns an altered \pParams, where all the file:// URIs have the \pMappings
+/// applied. \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);
+
+/// Creates a wrapping transport over \pTransp that applies the \pMappings to
+/// all inbound and outbound LSP messages. All calls are then delegated to the
+/// regular \pTransp (e.g. XPC, JSON).
+std::unique_ptr<Transport>
+createPathMappingTransport(std::unique_ptr<Transport> Transp,
+                           PathMappings Mappings);
+
+} // namespace clangd
+} // namespace clang
Index: clang-tools-extra/clangd/PathMapping.cpp
===================================================================
--- /dev/null
+++ clang-tools-extra/clangd/PathMapping.cpp
@@ -0,0 +1,187 @@
+//===--- 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 "Logger.h"
+#include "Transport.h"
+#include "URI.h"
+#include "llvm/ADT/STLExtras.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/Support/Errno.h"
+#include "llvm/Support/Path.h"
+#include "llvm/Support/raw_ostream.h"
+#include <memory>
+#include <tuple>
+#include <vector>
+
+namespace clang {
+namespace clangd {
+namespace {
+
+// Recurively apply the \pMF function on every string value in \pV
+template <typename MapperFunc>
+void recursivelyMap(llvm::json::Value &V, const MapperFunc &MF) {
+  using Kind = llvm::json::Value::Kind;
+  const auto &K = V.kind();
+  if (K == Kind::Object) {
+    for (auto &KV : *V.getAsObject()) {
+      recursivelyMap(KV.second, MF);
+    }
+  } else if (K == Kind::Array) {
+    for (auto &Val : *V.getAsArray()) {
+      recursivelyMap(Val, MF);
+    }
+  } else if (K == Kind::String) {
+    V = MF(*V.getAsString());
+  }
+}
+
+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 {
+    llvm::json::Value MappedParams =
+        doPathMapping(Params, /*IsIncoming=*/true, Mappings);
+    return WrappedHandler.onNotify(Method, std::move(MappedParams));
+  }
+
+  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));
+  }
+
+  bool onReply(llvm::json::Value ID,
+               llvm::Expected<llvm::json::Value> Result) override {
+    if (Result) {
+      Result = doPathMapping(*Result, /*IsIncoming=*/true, 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 {
+    llvm::json::Value MappedParams =
+        doPathMapping(Params, /*IsIncoming=*/false, Mappings);
+    WrappedTransport->notify(Method, std::move(MappedParams));
+  }
+
+  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));
+  }
+
+  void reply(llvm::json::Value ID,
+             llvm::Expected<llvm::json::Value> Result) override {
+    if (Result) {
+      Result = doPathMapping(*Result, /*IsIncoming=*/false, 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;
+};
+
+inline llvm::Error make_string_error(const llvm::Twine &Message) {
+  return llvm::make_error<llvm::StringError>(Message,
+                                             llvm::inconvertibleErrorCode());
+}
+
+} // namespace
+
+llvm::Expected<PathMappings>
+parsePathMappings(const std::vector<std::string> &RawPathMappings) {
+  if (!RawPathMappings.size()) {
+    return make_string_error("Must provide at least one path mapping");
+  }
+  llvm::StringRef HostPath, RemotePath;
+  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);
+    }
+    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);
+    }
+    ParsedMappings.emplace_back(HostPath, RemotePath);
+  }
+  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;
+}
+
+std::unique_ptr<Transport>
+createPathMappingTransport(std::unique_ptr<Transport> Transp,
+                           PathMappings Mappings) {
+  return llvm::make_unique<PathMappingTransport>(std::move(Transp), Mappings);
+}
+
+} // namespace clangd
+} // namespace clang
Index: clang-tools-extra/clangd/CMakeLists.txt
===================================================================
--- clang-tools-extra/clangd/CMakeLists.txt
+++ clang-tools-extra/clangd/CMakeLists.txt
@@ -59,6 +59,7 @@
   IncludeFixer.cpp
   JSONTransport.cpp
   Logger.cpp
+  PathMapping.cpp
   Protocol.cpp
   Quality.cpp
   RIFF.cpp
_______________________________________________
cfe-commits mailing list
cfe-commits@lists.llvm.org
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to