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 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 +#include +#include +#include + +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>; + +/// 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 +parsePathMappings(const std::vector &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 +createPathMappingTransport(std::unique_ptr 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 +#include +#include + +namespace clang { +namespace clangd { +namespace { + +// Recurively apply the \pMF function on every string value in \pV +template +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 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 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 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 WrappedTransport; + PathMappings Mappings; +}; + +inline llvm::Error make_string_error(const llvm::Twine &Message) { + return llvm::make_error(Message, + llvm::inconvertibleErrorCode()); +} + +} // namespace + +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; + 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 +createPathMappingTransport(std::unique_ptr Transp, + PathMappings Mappings) { + return llvm::make_unique(std::move(Transp), Mappings); +} + +} // namespace clangd +} // namespace clang 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/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/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/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/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 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); static llvm::cl::list 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 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/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 &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 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 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