diff --git a/lib/plug/debugger.ex b/lib/plug/debugger.ex index 2bbe8654..1974db6b 100644 --- a/lib/plug/debugger.ex +++ b/lib/plug/debugger.ex @@ -233,7 +233,7 @@ defmodule Plug.Debugger do assigns = Keyword.merge(assigns, conn: conn, - message: message, + message: maybe_autolink(message), markdown: markdown, style: style, banner: banner, @@ -549,4 +549,40 @@ defmodule Plug.Debugger do defp maybe_merge_dark_styles(style, default_dark_style) do Map.put(style, :dark, default_dark_style) end + + defp maybe_autolink(message) do + splitted = + Regex.split(~r/`[A-Z][A-Za-z0-9_.]+\.[a-z][A-Za-z0-9_!?]*\/\d+`/, message, + include_captures: true, + trim: true + ) + + Enum.map(splitted, &maybe_format_function_reference/1) + |> IO.iodata_to_binary() + end + + defp maybe_format_function_reference("`" <> reference = text) do + reference = String.trim_trailing(reference, "`") + + with {:ok, m, f, a} <- get_mfa(reference), + url when is_binary(url) <- get_doc(m, f, a, Application.get_application(m)) do + ~s[`#{h(reference)}`] + else + _ -> h(text) + end + end + + defp maybe_format_function_reference(text), do: h(text) + + def get_mfa(capture) do + [function_path, arity] = String.split(capture, "/") + {arity, ""} = Integer.parse(arity) + parts = String.split(function_path, ".") + {function_str, parts} = List.pop_at(parts, -1) + module = Module.safe_concat(parts) + function = String.to_existing_atom(function_str) + {:ok, module, function, arity} + rescue + _ -> :error + end end diff --git a/lib/plug/templates/debugger.html.eex b/lib/plug/templates/debugger.html.eex index 7f4b4d6a..fdc8f20a 100644 --- a/lib/plug/templates/debugger.html.eex +++ b/lib/plug/templates/debugger.html.eex @@ -884,7 +884,7 @@ at <%= h method(@conn) %> <%= h @conn.request_path %> -
<%= h @message %>
+
<%= @message %>
<%= for %{label: label, encoded_handler: encoded_handler} <- @actions do %> diff --git a/mix.exs b/mix.exs index e6ee853a..90ce33fc 100644 --- a/mix.exs +++ b/mix.exs @@ -26,7 +26,8 @@ defmodule Plug.MixProject do groups_for_extras: groups_for_extras(), source_ref: "v#{@version}", source_url: "https://github.com/elixir-plug/plug" - ] + ], + test_ignore_filters: [&String.starts_with?(&1, "test/fixtures/")] ] end diff --git a/test/plug/adapters/test/conn_test.exs b/test/plug/adapters/test/conn_test.exs index 2977aa5c..84a80fe1 100644 --- a/test/plug/adapters/test/conn_test.exs +++ b/test/plug/adapters/test/conn_test.exs @@ -161,13 +161,13 @@ defmodule Plug.Adapters.Test.ConnTest do end test "use existing conn.remote_ip if exists" do - conn_with_remote_ip = %Plug.Conn{conn(:get, "/") | remote_ip: {151, 236, 219, 228}} + conn_with_remote_ip = %{conn(:get, "/") | remote_ip: {151, 236, 219, 228}} child_conn = Plug.Adapters.Test.Conn.conn(conn_with_remote_ip, :get, "/", foo: "bar") assert child_conn.remote_ip == {151, 236, 219, 228} end test "use existing conn.port if exists" do - conn_with_port = %Plug.Conn{conn(:get, "/") | port: 4200} + conn_with_port = %{conn(:get, "/") | port: 4200} child_conn = Plug.Adapters.Test.Conn.conn(conn_with_port, :get, "/", foo: "bar") assert child_conn.port == 4200 end diff --git a/test/plug/debugger_test.exs b/test/plug/debugger_test.exs index e77bdcf4..618a84d3 100644 --- a/test/plug/debugger_test.exs +++ b/test/plug/debugger_test.exs @@ -599,4 +599,30 @@ defmodule Plug.DebuggerTest do assert conn.resp_body =~ " end" end + + test "links to hexdocs" do + conn = + conn(:get, "/foo/bar") + |> put_req_header("accept", "text/html") + |> render([], fn -> raise "please use `Plug.Conn.send_resp/3` instead" end) + + assert conn.resp_body =~ + ~r(`Plug.Conn.send_resp/3`) + end + + test "does not create new atoms" do + conn = + conn(:get, "/foo/bar") + |> put_req_header("accept", "text/html") + |> render([], fn -> + raise "please use `NotExisting.not_existing_atom_for_test_does_not_create_new_atom/1` instead" + end) + + assert conn.resp_body =~ + ~r(`NotExisting.not_existing_atom_for_test_does_not_create_new_atom/1`) + + assert_raise ArgumentError, fn -> + String.to_existing_atom("not_existing_atom_for_test_does_not_create_new_atom") + end + end end