From 24b4fc39600e02b91d9d9b569622f63d8763f98a Mon Sep 17 00:00:00 2001 From: Wojtek Mach Date: Mon, 4 Dec 2017 00:46:00 +0100 Subject: [PATCH 1/5] Autolink basic and built-in types in typespecs --- lib/ex_doc/formatter/html/autolink.ex | 98 ++++++++++++++++--- test/ex_doc/formatter/html/autolink_test.exs | 41 ++++++-- test/ex_doc/formatter/html/templates_test.exs | 8 +- 3 files changed, 122 insertions(+), 25 deletions(-) diff --git a/lib/ex_doc/formatter/html/autolink.ex b/lib/ex_doc/formatter/html/autolink.ex index 0fc59b5d9..9fa575d73 100644 --- a/lib/ex_doc/formatter/html/autolink.ex +++ b/lib/ex_doc/formatter/html/autolink.ex @@ -7,6 +7,8 @@ defmodule ExDoc.Formatter.HTML.Autolink do @elixir_docs "https://hexdocs.pm/" @erlang_docs "http://www.erlang.org/doc/man/" + @basic_types_page "typespecs.html#basic-types" + @built_in_types_page "typespecs.html#built-in-types" @doc """ Receives a list of module nodes and autolink all docs and typespecs. @@ -151,6 +153,62 @@ defmodule ExDoc.Formatter.HTML.Autolink do format_typespec(ast, typespecs, aliases, lib_dirs) end + @basic_types [ + any: 0, + none: 0, + atom: 0, + map: 0, + pid: 0, + port: 0, + reference: 0, + struct: 0, + tuple: 0, + + # Numbers + integer: 0, + float: 0, + neg_integer: 0, + non_neg_integer: 0, + pos_integer: 0, + + # Lists + list: 1, + nonempty_list: 1, + improper_list: 2, + maybe_improper_list: 2, + ] + + @built_in_types [ + term: 0, + arity: 0, + as_boolean: 1, + binary: 0, + bitstring: 0, + boolean: 0, + byte: 0, + char: 0, + charlist: 0, + nonempty_charlist: 0, + fun: 0, + function: 0, + identifier: 0, + iodata: 0, + iolist: 0, + keyword: 0, + keyword: 1, + list: 0, + nonempty_list: 0, + maybe_improper_list: 0, + nonempty_maybe_improper_list: 0, + mfa: 0, + module: 0, + no_return: 0, + node: 0, + number: 0, + struct: 0, + timeout: 0 + ] + defp format_typespec(ast, typespecs, aliases, lib_dirs) do ref = make_ref() @@ -166,26 +224,34 @@ defmodule ExDoc.Formatter.HTML.Autolink do {name, _, args} = form, placeholders when is_atom(name) and is_list(args) -> arity = length(args) - if {name, arity} in typespecs do - string = Macro.to_string(form) - n = enc_h("#{name}") - {string_to_link, _string_with_parens} = split_string_to_link(string) - string = ~s[#{h(string_to_link)}] - - put_placeholder(form, string, placeholders) - else - {form, placeholders} + cond do + {name, arity} in typespecs -> + n = enc_h("#{name}") + url = "#t:#{n}/#{arity}" + string = format_typespec_form(form, url) + put_placeholder(form, string, placeholders) + + {name, arity} in @basic_types -> + url = @elixir_docs <> "elixir/" <> @basic_types_page + string = format_typespec_form(form, url) + put_placeholder(form, string, placeholders) + + {name, arity} in @built_in_types -> + url = @elixir_docs <> "elixir/" <> @built_in_types_page + string = format_typespec_form(form, url) + put_placeholder(form, string, placeholders) + + true -> + {form, placeholders} end {{:., _, [alias, name]}, _, args} = form, placeholders when is_atom(name) and is_list(args) -> alias = expand_alias(alias) if source = get_source(alias, aliases, lib_dirs) do - string = Macro.to_string(form) n = enc_h("#{name}") - {string_to_link, _string_with_parens} = split_string_to_link(string) - string = ~s[#{h(string_to_link)}] - + url = "#{source}#{enc_h(inspect alias)}.html#t:#{n}/#{length(args)}" + string = format_typespec_form(form, url) put_placeholder(form, string, placeholders) else {form, placeholders} @@ -200,6 +266,12 @@ defmodule ExDoc.Formatter.HTML.Autolink do |> replace_placeholders(placeholders) end + defp format_typespec_form(form, url) do + string = Macro.to_string(form) + {string_to_link, _string_with_parens} = split_string_to_link(string) + ~s[#{h(string_to_link)}] + end + defp put_placeholder(form, string, placeholders) do id = map_size(placeholders) placeholder = :"_p#{id}_" diff --git a/test/ex_doc/formatter/html/autolink_test.exs b/test/ex_doc/formatter/html/autolink_test.exs index d1b2114ff..3cdd92b15 100644 --- a/test/ex_doc/formatter/html/autolink_test.exs +++ b/test/ex_doc/formatter/html/autolink_test.exs @@ -181,11 +181,11 @@ defmodule ExDoc.Formatter.HTML.AutolinkTest do # typespec test "operators" do - assert Autolink.typespec(quote(do: +number() :: number()), [], [], []) == - ~s[+number() :: number()] + assert Autolink.typespec(quote(do: +foo() :: foo()), [], [], []) == + ~s[+foo() :: foo()] - assert Autolink.typespec(quote(do: number() + number() :: number()), [], [], []) == - ~s[number() + number() :: number()] + assert Autolink.typespec(quote(do: foo() + foo() :: foo()), [], [], []) == + ~s[foo() + foo() :: foo()] end test "strip parens in typespecs" do @@ -256,14 +256,14 @@ defmodule ExDoc.Formatter.HTML.AutolinkTest do test "complex types with formatter" do ast = quote do t() :: %{ - foo: term(), + foo: bar(), really_long_name_that_will_trigger_multiple_line_breaks: String.t() } end assert Autolink.typespec(ast, [], []) == String.trim(""" t() :: %{ - foo: term(), + foo: bar(), really_long_name_that_will_trigger_multiple_line_breaks: String.t() } """) @@ -273,13 +273,13 @@ defmodule ExDoc.Formatter.HTML.AutolinkTest do test "complex types without formatter" do ast = quote do t() :: %{ - foo: term(), + foo: bar(), really_long_name_that_will_trigger_multiple_line_breaks: String.t() } end assert Autolink.typespec(ast, [], []) == - ~s[t() :: %{foo: term(), really_long_name_that_will_trigger_multiple_line_breaks: String.t()}] + ~s[t() :: %{foo: bar(), really_long_name_that_will_trigger_multiple_line_breaks: String.t()}] end test "autolink Elixir types in typespecs" do @@ -290,12 +290,27 @@ defmodule ExDoc.Formatter.HTML.AutolinkTest do ~s[Unknown.bar()] end + test "autolink Elixir basic types in typespecs" do + assert Autolink.typespec(quote(do: atom()), [], []) == + ~s[atom()] + end + + test "autolink Elixir built-in types in typespecs" do + assert Autolink.typespec(quote(do: term()), [], []) == + ~s[term()] + end + + test "autolink local takes precedence over basic/built-in types in typespecs" do + assert Autolink.typespec(quote(do: term()), [term: 0], []) == + ~s[term()] + end + test "autolink shared aliases in typespecs" do assert Autolink.typespec(quote(do: Foo.t), [], [Foo]) == ~s[Foo.t()] end - test "autolink local and remote types inside parameterized types" do + test "autolink local remote basic built-in types inside parameterized types" do assert Autolink.typespec(quote(do: parameterized_t(foo())), [parameterized_t: 1, foo: 0], []) == ~s[parameterized_t(foo())] @@ -316,5 +331,13 @@ defmodule ExDoc.Formatter.HTML.AutolinkTest do assert Autolink.typespec(quote(do: parameterized_t(foo())), [foo: 0], []) == ~s[parameterized_t(foo())] + + assert Autolink.typespec(quote(do: parameterized_t(atom())), [], []) == + ~s[parameterized_t(atom())] + + assert Autolink.typespec(quote(do: parameterized_t(atom()) :: list(function())), [], []) == + ~s[parameterized_t(atom()) :: ] <> + ~s[list(] <> + ~s[function())] end end diff --git a/test/ex_doc/formatter/html/templates_test.exs b/test/ex_doc/formatter/html/templates_test.exs index 57dea9eed..520fa9802 100644 --- a/test/ex_doc/formatter/html/templates_test.exs +++ b/test/ex_doc/formatter/html/templates_test.exs @@ -252,6 +252,8 @@ defmodule ExDoc.Formatter.HTML.TemplatesTest do test "module_page outputs the types and function specs" do content = get_module_page([TypesAndSpecs, TypesAndSpecs.Sub]) + any = ~s[any()] + integer = ~s[integer()] public_html = ~S[public(t) :: {t, ] <> @@ -259,7 +261,7 @@ defmodule ExDoc.Formatter.HTML.TemplatesTest do ~S[TypesAndSpecs.Sub.t(), ] <> ~S[opaque(), :ok | :error}] - ref_html = ~S[ref() :: {:binary.part(), public(any())}] + ref_html = ~s[ref() :: {:binary.part(), public(#{any})}] assert content =~ ~s[public(t)] refute content =~ ~s[private] @@ -267,8 +269,8 @@ defmodule ExDoc.Formatter.HTML.TemplatesTest do assert content =~ ref_html refute content =~ ~s[private\(t\)] assert content =~ ~s[A public type] - assert content =~ ~s[add(integer(), opaque()) :: integer()] - refute content =~ ~s[minus(integer(), integer()) :: integer()] + assert content =~ ~s[add(#{integer}, opaque()) :: #{integer}] + refute content =~ ~s[minus(#{integer}, #{integer}) :: #{integer}] end test "module_page outputs summaries" do From 4ce95fb542a0b01cf4772eef5c4e2d6873b5acd6 Mon Sep 17 00:00:00 2001 From: Wojtek Mach Date: Mon, 4 Dec 2017 13:16:56 +0100 Subject: [PATCH 2/5] When building Elixir docs create relative link to typespecs.html --- lib/ex_doc/formatter/html/autolink.ex | 10 ++++------ test/ex_doc/formatter/html/autolink_test.exs | 5 +++++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/ex_doc/formatter/html/autolink.ex b/lib/ex_doc/formatter/html/autolink.ex index 9fa575d73..687fe03a5 100644 --- a/lib/ex_doc/formatter/html/autolink.ex +++ b/lib/ex_doc/formatter/html/autolink.ex @@ -163,15 +163,11 @@ defmodule ExDoc.Formatter.HTML.Autolink do reference: 0, struct: 0, tuple: 0, - - # Numbers integer: 0, float: 0, neg_integer: 0, non_neg_integer: 0, pos_integer: 0, - - # Lists list: 1, nonempty_list: 1, improper_list: 2, @@ -232,12 +228,14 @@ defmodule ExDoc.Formatter.HTML.Autolink do put_placeholder(form, string, placeholders) {name, arity} in @basic_types -> - url = @elixir_docs <> "elixir/" <> @basic_types_page + source = get_source(Kernel, aliases, lib_dirs) + url = source <> @basic_types_page string = format_typespec_form(form, url) put_placeholder(form, string, placeholders) {name, arity} in @built_in_types -> - url = @elixir_docs <> "elixir/" <> @built_in_types_page + source = get_source(Kernel, aliases, lib_dirs) + url = source <> @built_in_types_page string = format_typespec_form(form, url) put_placeholder(form, string, placeholders) diff --git a/test/ex_doc/formatter/html/autolink_test.exs b/test/ex_doc/formatter/html/autolink_test.exs index 3cdd92b15..953c03599 100644 --- a/test/ex_doc/formatter/html/autolink_test.exs +++ b/test/ex_doc/formatter/html/autolink_test.exs @@ -300,6 +300,11 @@ defmodule ExDoc.Formatter.HTML.AutolinkTest do ~s[term()] end + test "autolink Elixir built-in types in Elixir typespecs" do + assert Autolink.typespec(quote(do: term()), [], [Kernel]) == + ~s[term()] + end + test "autolink local takes precedence over basic/built-in types in typespecs" do assert Autolink.typespec(quote(do: term()), [term: 0], []) == ~s[term()] From 3ab55024a52971fd4af5072b93eac566b87be9f4 Mon Sep 17 00:00:00 2001 From: Wojtek Mach Date: Mon, 4 Dec 2017 16:06:47 +0100 Subject: [PATCH 3/5] Improve typespec formatting placeholders --- lib/ex_doc/formatter/html/autolink.ex | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/ex_doc/formatter/html/autolink.ex b/lib/ex_doc/formatter/html/autolink.ex index 687fe03a5..d78dae3b2 100644 --- a/lib/ex_doc/formatter/html/autolink.ex +++ b/lib/ex_doc/formatter/html/autolink.ex @@ -271,14 +271,18 @@ defmodule ExDoc.Formatter.HTML.Autolink do end defp put_placeholder(form, string, placeholders) do - id = map_size(placeholders) - placeholder = :"_p#{id}_" + count = map_size(placeholders) + 1 + type_size = form |> Macro.to_string() |> byte_size() + int_size = count |> Integer.to_string() |> byte_size() + parens_size = 2 + pad = String.duplicate("p", max(type_size - int_size - parens_size, 1)) + placeholder = :"#{pad}#{count}" form = put_elem(form, 0, placeholder) {form, Map.put(placeholders, Atom.to_string(placeholder), string)} end defp replace_placeholders(string, placeholders) do - Regex.replace(~r"_p\d+_", string, &Map.fetch!(placeholders, &1)) + Regex.replace(~r"p+\d+", string, &Map.fetch!(placeholders, &1)) end defp format_ast(ast) do From f53c6daafdb1b0880e6edfb9fae139993f6e916d Mon Sep 17 00:00:00 2001 From: Wojtek Mach Date: Mon, 4 Dec 2017 18:36:30 +0100 Subject: [PATCH 4/5] Refactoring --- lib/ex_doc/formatter/html/autolink.ex | 111 +++++++++++++------------- 1 file changed, 55 insertions(+), 56 deletions(-) diff --git a/lib/ex_doc/formatter/html/autolink.ex b/lib/ex_doc/formatter/html/autolink.ex index d78dae3b2..cba5d12c5 100644 --- a/lib/ex_doc/formatter/html/autolink.ex +++ b/lib/ex_doc/formatter/html/autolink.ex @@ -10,6 +10,58 @@ defmodule ExDoc.Formatter.HTML.Autolink do @basic_types_page "typespecs.html#basic-types" @built_in_types_page "typespecs.html#built-in-types" + @basic_types [ + any: 0, + none: 0, + atom: 0, + map: 0, + pid: 0, + port: 0, + reference: 0, + struct: 0, + tuple: 0, + integer: 0, + float: 0, + neg_integer: 0, + non_neg_integer: 0, + pos_integer: 0, + list: 1, + nonempty_list: 1, + improper_list: 2, + maybe_improper_list: 2, + ] + + @built_in_types [ + term: 0, + arity: 0, + as_boolean: 1, + binary: 0, + bitstring: 0, + boolean: 0, + byte: 0, + char: 0, + charlist: 0, + nonempty_charlist: 0, + fun: 0, + function: 0, + identifier: 0, + iodata: 0, + iolist: 0, + keyword: 0, + keyword: 1, + list: 0, + nonempty_list: 0, + maybe_improper_list: 0, + nonempty_maybe_improper_list: 0, + mfa: 0, + module: 0, + no_return: 0, + node: 0, + number: 0, + struct: 0, + timeout: 0 + ] + @doc """ Receives a list of module nodes and autolink all docs and typespecs. """ @@ -153,60 +205,9 @@ defmodule ExDoc.Formatter.HTML.Autolink do format_typespec(ast, typespecs, aliases, lib_dirs) end - @basic_types [ - any: 0, - none: 0, - atom: 0, - map: 0, - pid: 0, - port: 0, - reference: 0, - struct: 0, - tuple: 0, - integer: 0, - float: 0, - neg_integer: 0, - non_neg_integer: 0, - pos_integer: 0, - list: 1, - nonempty_list: 1, - improper_list: 2, - maybe_improper_list: 2, - ] - - @built_in_types [ - term: 0, - arity: 0, - as_boolean: 1, - binary: 0, - bitstring: 0, - boolean: 0, - byte: 0, - char: 0, - charlist: 0, - nonempty_charlist: 0, - fun: 0, - function: 0, - identifier: 0, - iodata: 0, - iolist: 0, - keyword: 0, - keyword: 1, - list: 0, - nonempty_list: 0, - maybe_improper_list: 0, - nonempty_maybe_improper_list: 0, - mfa: 0, - module: 0, - no_return: 0, - node: 0, - number: 0, - struct: 0, - timeout: 0 - ] - defp format_typespec(ast, typespecs, aliases, lib_dirs) do ref = make_ref() + elixir_source = get_source(Kernel, aliases, lib_dirs) {ast, placeholders} = Macro.prewalk(ast, %{}, fn @@ -228,14 +229,12 @@ defmodule ExDoc.Formatter.HTML.Autolink do put_placeholder(form, string, placeholders) {name, arity} in @basic_types -> - source = get_source(Kernel, aliases, lib_dirs) - url = source <> @basic_types_page + url = elixir_source <> @basic_types_page string = format_typespec_form(form, url) put_placeholder(form, string, placeholders) {name, arity} in @built_in_types -> - source = get_source(Kernel, aliases, lib_dirs) - url = source <> @built_in_types_page + url = elixir_source <> @built_in_types_page string = format_typespec_form(form, url) put_placeholder(form, string, placeholders) From 5c2e4a32b5f74f83bae7d2591bf92033f6f7f8d9 Mon Sep 17 00:00:00 2001 From: Wojtek Mach Date: Mon, 4 Dec 2017 20:40:25 +0100 Subject: [PATCH 5/5] Remove incorrect test, cannot overwrite built-in types --- lib/ex_doc/formatter/html/autolink.ex | 12 ++++++------ test/ex_doc/formatter/html/autolink_test.exs | 5 ----- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/lib/ex_doc/formatter/html/autolink.ex b/lib/ex_doc/formatter/html/autolink.ex index cba5d12c5..aaa7596a7 100644 --- a/lib/ex_doc/formatter/html/autolink.ex +++ b/lib/ex_doc/formatter/html/autolink.ex @@ -222,12 +222,6 @@ defmodule ExDoc.Formatter.HTML.Autolink do arity = length(args) cond do - {name, arity} in typespecs -> - n = enc_h("#{name}") - url = "#t:#{n}/#{arity}" - string = format_typespec_form(form, url) - put_placeholder(form, string, placeholders) - {name, arity} in @basic_types -> url = elixir_source <> @basic_types_page string = format_typespec_form(form, url) @@ -238,6 +232,12 @@ defmodule ExDoc.Formatter.HTML.Autolink do string = format_typespec_form(form, url) put_placeholder(form, string, placeholders) + {name, arity} in typespecs -> + n = enc_h("#{name}") + url = "#t:#{n}/#{arity}" + string = format_typespec_form(form, url) + put_placeholder(form, string, placeholders) + true -> {form, placeholders} end diff --git a/test/ex_doc/formatter/html/autolink_test.exs b/test/ex_doc/formatter/html/autolink_test.exs index 953c03599..3461ba1d3 100644 --- a/test/ex_doc/formatter/html/autolink_test.exs +++ b/test/ex_doc/formatter/html/autolink_test.exs @@ -305,11 +305,6 @@ defmodule ExDoc.Formatter.HTML.AutolinkTest do ~s[term()] end - test "autolink local takes precedence over basic/built-in types in typespecs" do - assert Autolink.typespec(quote(do: term()), [term: 0], []) == - ~s[term()] - end - test "autolink shared aliases in typespecs" do assert Autolink.typespec(quote(do: Foo.t), [], [Foo]) == ~s[Foo.t()]