Skip to content

Commit

Permalink
Link Module.function/arity to hexdocs in exception messages (#1263)
Browse files Browse the repository at this point in the history
Idea by @Gazler

This is similar to ExDoc autolinking, but only works for the simple
Elixir `Module.function/arity` format
  • Loading branch information
SteffenDE authored Feb 27, 2025
1 parent 4d12de4 commit 587f365
Show file tree
Hide file tree
Showing 5 changed files with 68 additions and 5 deletions.
38 changes: 37 additions & 1 deletion lib/plug/debugger.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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[<a href="#{url}" target="_blank">`#{h(reference)}`</a>]
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
2 changes: 1 addition & 1 deletion lib/plug/templates/debugger.html.eex
Original file line number Diff line number Diff line change
Expand Up @@ -884,7 +884,7 @@
<small>at <%= h method(@conn) %></small>
<small class="path"><%= h @conn.request_path %></small>
</h5>
<code><pre class="exception-details-text"><%= h @message %></pre></code>
<code><pre class="exception-details-text"><%= @message %></pre></code>
</header>

<%= for %{label: label, encoded_handler: encoded_handler} <- @actions do %>
Expand Down
3 changes: 2 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions test/plug/adapters/test/conn_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions test/plug/debugger_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -599,4 +599,30 @@ defmodule Plug.DebuggerTest do

assert conn.resp_body =~ "<span class=\"code\"> end</span>"
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(<a href="https://hexdocs.pm/plug/.*/Plug.Conn.html#send_resp/3" target="_blank">`Plug.Conn.send_resp/3`</a>)
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

0 comments on commit 587f365

Please sign in to comment.