Skip to content

Commit

Permalink
Support autolinking for multiple arities (#1080)
Browse files Browse the repository at this point in the history
  • Loading branch information
cybrox authored and José Valim committed Aug 20, 2019
1 parent 81db137 commit 29b161e
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 12 deletions.
49 changes: 37 additions & 12 deletions lib/ex_doc/formatter/html/autolink.ex
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ defmodule ExDoc.Formatter.HTML.Autolink do
lib_dirs = options[:lib_dirs] || default_lib_dirs(:erlang)

fn all, text, match ->
pmfa = {_prefix, module, function, arity} = split_match(kind, match)
pmfa = {_prefix, module, function, arities} = split_match(kind, match)
text = default_text(":", link_type, pmfa, text)

if doc = module_docs(:erlang, module, lib_dirs) do
Expand All @@ -395,7 +395,7 @@ defmodule ExDoc.Formatter.HTML.Autolink do
"[#{text}](#{doc}#{module}.html)"

:function ->
"[#{text}](#{doc}#{module}.html##{function}-#{arity})"
"[#{text}](#{doc}#{module}.html##{function}-#{first_arity_of(arities)})"
end
else
all
Expand Down Expand Up @@ -443,17 +443,34 @@ defmodule ExDoc.Formatter.HTML.Autolink do
skip_warnings_on = options[:skip_undefined_reference_warnings_on] || []

fn all, text, original_match ->
pmfa = {prefix, original_module, function, arity} = split_match(:function, original_match)
pmfa = {prefix, original_module, function, arities} = split_match(:function, original_match)
text = default_text("", link_type, pmfa, text)
match = strip_elixir_namespace(original_match)
module = strip_elixir_namespace(original_module)
multiple_arities? = Enum.count(arities) > 1
first_arity = first_arity_of(arities)

cond do
multiple_arities? and not arities_sorted?(arities) ->
all

multiple_arities? and
Enum.all?(arities, fn a -> "#{prefix}#{enc(function)}/#{a}" in locals end) ->
"[#{text}](##{prefix}#{enc(function)}/#{first_arity})"

multiple_arities? and
Enum.all?(arities, fn a -> "#{prefix}#{module}.#{enc(function)}/#{a}" in docs_refs end) ->
"[#{text}](#{module}#{extension}##{prefix}#{enc(function)}/#{first_arity})"

multiple_arities? and
Enum.all?(arities, fn a -> "#{enc(function)}/#{a}" in @kernel_function_strings end) ->
"[#{text}](#{elixir_docs}Kernel#{extension}##{prefix}#{enc(function)}/#{first_arity})"

match in locals ->
"[#{text}](##{prefix}#{enc(function)}/#{arity})"
"[#{text}](##{prefix}#{enc(function)}/#{first_arity})"

match in docs_refs ->
"[#{text}](#{module}#{extension}##{prefix}#{enc(function)}/#{arity})"
"[#{text}](#{module}#{extension}##{prefix}#{enc(function)}/#{first_arity})"

match in @basic_type_strings ->
"[#{text}](#{basic_types_page_for(elixir_docs, extension)})"
Expand All @@ -462,17 +479,17 @@ defmodule ExDoc.Formatter.HTML.Autolink do
"[#{text}](#{built_in_types_page_for(elixir_docs, extension)})"

match in @kernel_function_strings ->
"[#{text}](#{elixir_docs}Kernel#{extension}##{prefix}#{enc(function)}/#{arity})"
"[#{text}](#{elixir_docs}Kernel#{extension}##{prefix}#{enc(function)}/#{first_arity})"

match in @special_form_strings ->
"[#{text}](#{elixir_docs}Kernel.SpecialForms" <>
"#{extension}##{prefix}#{enc(function)}/#{arity})"
"#{extension}##{prefix}#{enc(function)}/#{first_arity})"

module in modules_refs ->
maybe_warn(text, match, module_id, id, skip_warnings_on)

doc = module_docs(:elixir, module, lib_dirs) ->
"[#{text}](#{doc}#{module}#{extension}##{prefix}#{enc(function)}/#{arity})"
"[#{text}](#{doc}#{module}#{extension}##{prefix}#{enc(function)}/#{first_arity})"

link_type == :custom ->
maybe_warn(text, match, module_id, id, skip_warnings_on)
Expand Down Expand Up @@ -541,13 +558,13 @@ defmodule ExDoc.Formatter.HTML.Autolink do
do: link_text

defp default_text(_, _, {_, "", fun, arity}, _link_text),
do: "`#{fun}/#{arity}`"
do: "`#{fun}/#{print_arities(arity)}`"

defp default_text(module_prefix, _, {_, module, "", ""}, _link_text),
do: "`#{module_prefix}#{module}`"

defp default_text(module_prefix, _, {_, module, fun, arity}, _link_text),
do: "`#{module_prefix}#{module}.#{fun}/#{arity}`"
do: "`#{module_prefix}#{module}.#{fun}/#{print_arities(arity)}`"

defp default_lib_dirs(),
do: default_lib_dirs(:elixir) ++ default_lib_dirs(:erlang)
Expand Down Expand Up @@ -596,14 +613,22 @@ defmodule ExDoc.Formatter.HTML.Autolink do
|> String.split(" ")
|> Enum.split(-1)

{"", Enum.join(mod, "."), hd(name), arity}
{"", Enum.join(mod, "."), hd(name), split_arities(arity)}
end

# handles "/" function
defp split_function_list([modules, "", arity]) when is_binary(modules) do
split_function_list([modules <> "/", arity])
end

defp split_arities(arities), do: String.split(arities, ",", trim: true)

defp print_arities(arities), do: Enum.join(arities, ",")

defp arities_sorted?(arities), do: arities == Enum.sort(arities)

defp first_arity_of([arity | _]), do: arity

defp strip_elixir_namespace("Elixir." <> rest), do: rest
defp strip_elixir_namespace(rest), do: rest

Expand Down Expand Up @@ -762,7 +787,7 @@ defmodule ExDoc.Formatter.HTML.Autolink do
defp re(:fa, language) when language in [:elixir, :erlang] do
~r{
(#{re_source(:f, language)}) # function_name
/\d+ # /arity
/\d+(,\d+)* # /arity
}x
end

Expand Down
49 changes: 49 additions & 0 deletions test/ex_doc/formatter/html/autolink_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ defmodule ExDoc.Formatter.HTML.AutolinkTest do
test "autolinks fun/arity in docs" do
assert project_doc("`example/2`", %{locals: ["example/2"]}) == "[`example/2`](#example/2)"

assert project_doc("`example/1,2`", %{locals: ["example/1", "example/2"]}) ==
"[`example/1,2`](#example/1)"

assert project_doc("`__ENV__/0`", %{locals: ["__ENV__/0"]}) == "[`__ENV__/0`](#__ENV__/0)"

assert project_doc("`example/2` then `example/2`", %{locals: ["example/2"]}) ==
Expand All @@ -32,6 +35,9 @@ defmodule ExDoc.Formatter.HTML.AutolinkTest do
# `split_function` must also return locally defined callbacks
# format should be: "c:<fun>/<arity>"
assert project_doc("`c:fun/2`", %{locals: ["c:fun/2"]}) == "[`fun/2`](#c:fun/2)"

assert project_doc("`c:fun/1,2`", %{locals: ["c:fun/1", "c:fun/2"]}) ==
"[`fun/1,2`](#c:fun/1)"
end

test "autolinks to local types" do
Expand All @@ -41,6 +47,9 @@ defmodule ExDoc.Formatter.HTML.AutolinkTest do
assert project_doc("`t:my_type/1`", %{locals: ["t:my_type/1"]}) ==
"[`my_type/1`](#t:my_type/1)"

assert project_doc("`t:my_type/1,2`", %{locals: ["t:my_type/1", "t:my_type/2"]}) ==
"[`my_type/1,2`](#t:my_type/1)"

# links to types without arity don't work
assert project_doc("`t:my_type`", %{locals: ["t:my_type/0"]}) == "`t:my_type`"
end
Expand All @@ -55,6 +64,11 @@ defmodule ExDoc.Formatter.HTML.AutolinkTest do

test "does not autolink undefined functions" do
assert project_doc("`example/1`", %{locals: ["example/2"]}) == "`example/1`"
assert project_doc("`example/1,2`", %{locals: ["example/1"]}) == "`example/1,2`"

assert project_doc("`example/1,2,3`", %{locals: ["example/1", "example/2"]}) ==
"`example/1,2,3`"

assert project_doc("`example/1`", %{}) == "`example/1`"
end

Expand All @@ -63,6 +77,11 @@ defmodule ExDoc.Formatter.HTML.AutolinkTest do
assert project_doc("[the `example/1`]()", %{locals: ["example/1"]}) == "[the `example/1`]()"
end

test "does not autolink to functions with multiple arities in the wrong order" do
assert project_doc("`example/2,1`", %{locals: ["example/1", "example/2"]}) ==
"`example/2,1`"
end

test "autolinks special forms" do
assert project_doc("`{}/1`", %{locals: ["{}/1"]}) == "[`{}/1`](#%7B%7D/1)"
assert project_doc("`<<>>/1`", %{locals: ["<<>>/1"]}) == "[`<<>>/1`](#%3C%3C%3E%3E/1)"
Expand All @@ -82,6 +101,9 @@ defmodule ExDoc.Formatter.HTML.AutolinkTest do
assert project_doc("`!/1`", %{locals: ["!/1"]}) == "[`!/1`](#!/1)"
assert project_doc("`!/1`", %{}) == "[`!/1`](#{@elixir_docs}elixir/Kernel.html#!/1)"
assert project_doc("`!/1`", %{aliases: [Kernel]}) == "[`!/1`](Kernel.html#!/1)"

assert project_doc("`raise/1,2`", %{aliases: [Kernel]}) ==
"[`raise/1,2`](Kernel.html#raise/1)"
end
end

Expand All @@ -90,6 +112,9 @@ defmodule ExDoc.Formatter.HTML.AutolinkTest do
assert project_doc("`Mod.example/2`", %{docs_refs: ["Mod.example/2"]}) ==
"[`Mod.example/2`](Mod.html#example/2)"

assert project_doc("`Mod.example/1,2`", %{docs_refs: ["Mod.example/1", "Mod.example/2"]}) ==
"[`Mod.example/1,2`](Mod.html#example/1)"

assert project_doc("`Mod.__ENV__/2`", %{docs_refs: ["Mod.__ENV__/2"]}) ==
"[`Mod.__ENV__/2`](Mod.html#__ENV__/2)"

Expand Down Expand Up @@ -147,6 +172,11 @@ defmodule ExDoc.Formatter.HTML.AutolinkTest do
"title"
end) =~ ~r"references example/2 .* \(parsing Mod.foo/0 docs\)"

assert capture_io(:stderr, fn ->
assert Autolink.project_doc("[title](`Mod.example/1,2`)", "Mod.foo/0", compiled) ==
"title"
end) =~ ~r"references Mod.example/1,2 .* \(parsing Mod.foo/0 docs\)"

# skip warning when module page is blacklisted
overwritten = Map.put(compiled, :module_id, "Bar")
assert assert project_doc("`Mod.example/2`", "Mod.bar/0", overwritten) == "`Mod.example/2`"
Expand All @@ -159,6 +189,9 @@ defmodule ExDoc.Formatter.HTML.AutolinkTest do
assert project_doc("`String.upcase/1`", %{}) ==
"[`String.upcase/1`](#{@elixir_docs}elixir/String.html#upcase/1)"

assert project_doc("`File.cd!/1,2`", %{}) ==
"[`File.cd!/1,2`](#{@elixir_docs}elixir/File.html#cd!/1)"

assert project_doc("`Mix.env/0`", %{}) ==
"[`Mix.env/0`](#{@elixir_docs}mix/Mix.html#env/0)"
end
Expand Down Expand Up @@ -199,6 +232,11 @@ defmodule ExDoc.Formatter.HTML.AutolinkTest do
test "autolinks callbacks" do
assert project_doc("`c:Mod.++/2`", %{docs_refs: ["c:Mod.++/2"]}) ==
"[`Mod.++/2`](Mod.html#c:++/2)"

assert project_doc(
"`c:Mod.++/1,2`",
%{docs_refs: ["c:Mod.++/1", "c:Mod.++/2"]}
) == "[`Mod.++/1,2`](Mod.html#c:++/1)"
end

test "autolinks types" do
Expand All @@ -208,6 +246,11 @@ defmodule ExDoc.Formatter.HTML.AutolinkTest do
%{docs_refs: ["t:MyModule.my_type/0"]}
) == "[`MyModule.my_type/0`](MyModule.html#t:my_type/0)"

assert project_doc(
"`t:MyModule.my_type/0,1`",
%{docs_refs: ["t:MyModule.my_type/0", "t:MyModule.my_type/1"]}
) == "[`MyModule.my_type/0,1`](MyModule.html#t:my_type/0)"

assert project_doc(
"`t:MyModule.my_type`",
%{docs_refs: ["t:MyModule.my_type/0"]}
Expand Down Expand Up @@ -444,12 +487,18 @@ defmodule ExDoc.Formatter.HTML.AutolinkTest do

assert project_doc("`:zlib.deflateInit/2`", %{}) ==
"[`:zlib.deflateInit/2`](#{@erlang_docs}zlib.html#deflateInit-2)"

assert project_doc("`:file.change_owner/2,3`", %{}) ==
"[`:file.change_owner/2,3`](#{@erlang_docs}file.html#change_owner-2)"
end

test "autolinks to Erlang functions with custom links" do
assert project_doc("[`example`](`:lists.reverse/1`)", %{}) ==
"[`example`](#{@erlang_docs}lists.html#reverse-1)"

assert project_doc("[`example`](`:lists.reverse/1,2`)", %{}) ==
"[`example`](#{@erlang_docs}lists.html#reverse-1)"

assert project_doc("[example](`:lists.reverse/1`)", %{}) ==
"[example](#{@erlang_docs}lists.html#reverse-1)"
end
Expand Down

0 comments on commit 29b161e

Please sign in to comment.