diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c1ea1b9..0a770de 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: otp: [23.x, 24.x, 25.x, 26.x] - elixir: [1.11.x, 1.12.x, 1.13.x, 1.14.x, 1.15.x] + elixir: [1.11.x, 1.12.x, 1.13.x, 1.14.x, 1.15.x, 1.16.x] exclude: - otp: 26.x elixir: 1.11.x @@ -26,6 +26,8 @@ jobs: elixir: 1.12.x - otp: 23.x elixir: 1.15.x + - otp: 23.x + elixir: 1.16.x steps: - uses: actions/checkout@v2 @@ -50,14 +52,14 @@ jobs: formatter: runs-on: ubuntu-latest - name: Formatter (1.15.x.x/26.x) + name: Formatter (1.16.x/26.x) steps: - uses: actions/checkout@v2 - uses: erlef/setup-beam@v1 with: otp-version: 26.x - elixir-version: 1.15.x + elixir-version: 1.16.x - uses: actions/cache@v3 with: path: | diff --git a/lib/gen_lsp.ex b/lib/gen_lsp.ex index 63e698b..3c2f9cd 100644 --- a/lib/gen_lsp.ex +++ b/lib/gen_lsp.ex @@ -317,111 +317,140 @@ defmodule GenLSP do lsp, "Last message received: handle_request #{inspect(request)}", [:gen_lsp, :request, :client], - fn -> - case GenLSP.Requests.new(request) do - {:ok, %{id: id} = req} -> - result = - :telemetry.span([:gen_lsp, :handle_request], %{method: req.method}, fn -> - {lsp.mod.handle_request(req, lsp), %{}} - end) - - case result do - {:reply, reply, %LSP{} = lsp} -> - response_key = - case reply do - %GenLSP.ErrorResponse{} -> "error" - _ -> "result" - end - - # if result is valid, continue, if not, we return an internal error - {response_key, response} = - case Schematic.dump(req.__struct__.result(), reply) do - {:ok, output} -> - {response_key, output} - - {:error, errors} -> - exception = InvalidResponse.exception({req.method, reply, errors}) - - Logger.error(Exception.format(:error, exception)) - - {:ok, output} = - Schematic.dump( - GenLSP.ErrorResponse.schematic(), - %GenLSP.ErrorResponse{ - code: GenLSP.Enumerations.ErrorCodes.internal_error(), - message: exception.message - } - ) - - {"error", output} - end - - packet = %{ - "jsonrpc" => "2.0", - "id" => id, - response_key => response - } - - deb = - :sys.handle_debug(deb, &write_debug/3, __MODULE__, {:out, :request, from}) - - GenLSP.Buffer.outgoing(lsp.buffer, packet) - - duration = System.system_time(:microsecond) - start - - Logger.debug( - "handled request client -> server #{req.method} in #{format_time(duration)}", - id: req.id, - method: req.method - ) - - :telemetry.execute([:gen_lsp, :request, :client, :stop], %{duration: duration}) - - loop(lsp, parent, deb) - - {:noreply, lsp} -> - duration = System.system_time(:microsecond) - start - - Logger.debug( - "handled request client -> server #{req.method} in #{format_time(duration)}", - id: req.id, - method: req.method + fn + {:error, error} -> + {:ok, output} = + Schematic.dump( + GenLSP.ErrorResponse.schematic(), + %GenLSP.ErrorResponse{ + code: GenLSP.Enumerations.ErrorCodes.internal_error(), + message: error + } + ) + + packet = %{ + "jsonrpc" => "2.0", + "id" => Process.get(:request_id), + "error" => output + } + + deb = + :sys.handle_debug(deb, &write_debug/3, __MODULE__, {:out, :request, from}) + + GenLSP.Buffer.outgoing(lsp.buffer, packet) + loop(lsp, parent, deb) + + _ -> + case GenLSP.Requests.new(request) do + {:ok, %{id: id} = req} -> + Process.put(:request_id, id) + + result = + :telemetry.span([:gen_lsp, :handle_request], %{method: req.method}, fn -> + {lsp.mod.handle_request(req, lsp), %{}} + end) + + case result do + {:reply, reply, %LSP{} = lsp} -> + response_key = + case reply do + %GenLSP.ErrorResponse{} -> "error" + _ -> "result" + end + + # if result is valid, continue, if not, we return an internal error + {response_key, response} = + case Schematic.dump(req.__struct__.result(), reply) do + {:ok, output} -> + {response_key, output} + + {:error, errors} -> + exception = InvalidResponse.exception({req.method, reply, errors}) + + Logger.error(Exception.format(:error, exception)) + + {:ok, output} = + Schematic.dump( + GenLSP.ErrorResponse.schematic(), + %GenLSP.ErrorResponse{ + code: GenLSP.Enumerations.ErrorCodes.internal_error(), + message: exception.message + } + ) + + {"error", output} + end + + packet = %{ + "jsonrpc" => "2.0", + "id" => id, + response_key => response + } + + deb = + :sys.handle_debug(deb, &write_debug/3, __MODULE__, {:out, :request, from}) + + GenLSP.Buffer.outgoing(lsp.buffer, packet) + + duration = System.system_time(:microsecond) - start + + Logger.debug( + "handled request client -> server #{req.method} in #{format_time(duration)}", + id: req.id, + method: req.method + ) + + :telemetry.execute([:gen_lsp, :request, :client, :stop], %{ + duration: duration + }) + + loop(lsp, parent, deb) + + {:noreply, lsp} -> + duration = System.system_time(:microsecond) - start + + Logger.debug( + "handled request client -> server #{req.method} in #{format_time(duration)}", + id: req.id, + method: req.method + ) + + :telemetry.execute([:gen_lsp, :request, :client, :stop], %{ + duration: duration + }) + + loop(lsp, parent, deb) + end + + {:error, errors} -> + # the payload is not parseable at all, other than being valid JSON and having + # an `id` property to signal its a request + exception = InvalidRequest.exception({request, errors}) + + Logger.error(Exception.format(:error, exception)) + + {:ok, output} = + Schematic.dump( + GenLSP.ErrorResponse.schematic(), + %GenLSP.ErrorResponse{ + code: GenLSP.Enumerations.ErrorCodes.invalid_request(), + message: exception.message + } ) - :telemetry.execute([:gen_lsp, :request, :client, :stop], %{duration: duration}) + packet = %{ + "jsonrpc" => "2.0", + "id" => request["id"], + "error" => output + } - loop(lsp, parent, deb) - end + deb = + :sys.handle_debug(deb, &write_debug/3, __MODULE__, {:out, :request, from}) - {:error, errors} -> - # the payload is not parseable at all, other than being valid JSON and having - # an `id` property to signal its a request - exception = InvalidRequest.exception({request, errors}) + GenLSP.Buffer.outgoing(lsp.buffer, packet) - Logger.error(Exception.format(:error, exception)) - - {:ok, output} = - Schematic.dump( - GenLSP.ErrorResponse.schematic(), - %GenLSP.ErrorResponse{ - code: GenLSP.Enumerations.ErrorCodes.invalid_request(), - message: exception.message - } - ) - - packet = %{ - "jsonrpc" => "2.0", - "id" => request["id"], - "error" => output - } - - deb = - :sys.handle_debug(deb, &write_debug/3, __MODULE__, {:out, :request, from}) - - GenLSP.Buffer.outgoing(lsp.buffer, packet) - - loop(lsp, parent, deb) - end + loop(lsp, parent, deb) + end end ) @@ -434,41 +463,49 @@ defmodule GenLSP do lsp, "Last message received: handle_notification #{inspect(notification)}", [:gen_lsp, :notification, :client], - fn -> - case GenLSP.Notifications.new(notification) do - {:ok, note} -> - result = - :telemetry.span([:gen_lsp, :handle_notification], %{method: note.method}, fn -> - {lsp.mod.handle_notification(note, lsp), %{}} - end) - - case result do - {:noreply, %LSP{} = lsp} -> - duration = System.system_time(:microsecond) - start - - Logger.debug( - "handled notification client -> server #{note.method} in #{format_time(duration)}", - method: note.method + fn + {:error, _} -> + loop(lsp, parent, deb) + + _ -> + case GenLSP.Notifications.new(notification) do + {:ok, note} -> + result = + :telemetry.span( + [:gen_lsp, :handle_notification], + %{method: note.method}, + fn -> + {lsp.mod.handle_notification(note, lsp), %{}} + end ) - :telemetry.execute([:gen_lsp, :notification, :client, :stop], %{ - duration: duration - }) + case result do + {:noreply, %LSP{} = lsp} -> + duration = System.system_time(:microsecond) - start + + Logger.debug( + "handled notification client -> server #{note.method} in #{format_time(duration)}", + method: note.method + ) - loop(lsp, parent, deb) - end + :telemetry.execute([:gen_lsp, :notification, :client, :stop], %{ + duration: duration + }) - {:error, errors} -> - # the payload is not parseable at all, other than being valid JSON - exception = InvalidNotification.exception({notification, errors}) + loop(lsp, parent, deb) + end - Logger.warning(Exception.format(:error, exception)) + {:error, errors} -> + # the payload is not parseable at all, other than being valid JSON + exception = InvalidNotification.exception({notification, errors}) - deb = - :sys.handle_debug(deb, &write_debug/3, __MODULE__, {:out, :request, from}) + Logger.warning(Exception.format(:error, exception)) - loop(lsp, parent, deb) - end + deb = + :sys.handle_debug(deb, &write_debug/3, __MODULE__, {:out, :request, from}) + + loop(lsp, parent, deb) + end end ) @@ -481,18 +518,22 @@ defmodule GenLSP do lsp, "Last message received: handle_info #{inspect(message)}", [:gen_lsp, :info], - fn -> - result = - :telemetry.span([:gen_lsp, :handle_info], %{}, fn -> - {lsp.mod.handle_info(message, lsp), %{}} - end) - - case result do - {:noreply, %LSP{} = lsp} -> - duration = System.system_time(:microsecond) - start - :telemetry.execute([:gen_lsp, :info, :stop], %{duration: duration}) - loop(lsp, parent, deb) - end + fn + {:error, _} -> + loop(lsp, parent, deb) + + _ -> + result = + :telemetry.span([:gen_lsp, :handle_info], %{}, fn -> + {lsp.mod.handle_info(message, lsp), %{}} + end) + + case result do + {:noreply, %LSP{} = lsp} -> + duration = System.system_time(:microsecond) - start + :telemetry.execute([:gen_lsp, :info, :stop], %{duration: duration}) + loop(lsp, parent, deb) + end end ) end @@ -506,26 +547,19 @@ defmodule GenLSP do "#{System.convert_time_unit(time, :microsecond, :millisecond)}ms" end - @spec attempt(LSP.t(), String.t(), list(atom()), (-> any())) :: no_return() + @spec attempt(LSP.t(), String.t(), list(atom()), (:try | {:error, String.t()} -> any())) :: + no_return() defp attempt(lsp, message, prefix, callback) do - callback.() + callback.(:try) rescue e -> - message = """ - LSP Exited. - - #{message} - - #{Exception.format(:error, e, __STACKTRACE__)} - - """ - :telemetry.execute(prefix ++ [:exception], %{message: message}) - error(lsp, message) + message = Exception.format(:error, e, __STACKTRACE__) Logger.error(message) + error(lsp, message) - reraise e, __STACKTRACE__ + callback.({:error, message}) end defp dump!(schematic, structure) do diff --git a/test/gen_lsp_test.exs b/test/gen_lsp_test.exs index 0b177cf..05cf627 100644 --- a/test/gen_lsp_test.exs +++ b/test/gen_lsp_test.exs @@ -176,8 +176,38 @@ defmodule GenLSPTest do "type" => 1 } - assert message =~ - "LSP Exited.\n\nLast message received: handle_info :boom\n\n** (RuntimeError) boom" + assert message =~ "** (RuntimeError) boom" + end + + test "returns an internal error when user code fails", %{client: client} do + log = + capture_log(fn -> + :ok = + request(client, %{ + "jsonrpc" => "2.0", + "method" => "workspace/symbol", + "params" => %{"query" => ""}, + "id" => 2 + }) + + assert_notification "window/logMessage", %{ + "message" => message, + "type" => 1 + } + + assert message =~ "(RuntimeError) boom" + + assert_error(2, %{ + "code" => -32603, + "message" => + """ + ** (RuntimeError) boom + (gen_lsp 0.9.0) test/support/example_server.ex:35: GenLSPTest.ExampleServer.handle_request/2 + """ <> _ + }) + end) + + assert log =~ "[error] ** (RuntimeError) boom" end test "can receive a normal message with handle_info/2", %{server: server} do diff --git a/test/support/example_server.ex b/test/support/example_server.ex index 93d411b..c67daba 100644 --- a/test/support/example_server.ex +++ b/test/support/example_server.ex @@ -31,6 +31,11 @@ defmodule GenLSPTest.ExampleServer do {:reply, [nil, []], lsp} end + def handle_request(%Requests.WorkspaceSymbol{}, lsp) do + raise "boom" + {:reply, [], lsp} + end + def handle_request(_, lsp) do {:reply, %GenLSP.ErrorResponse{