Packages
ex_doc
0.25.0
0.40.3
0.40.2
0.40.1
0.40.0
0.39.3
0.39.2
0.39.1
0.39.0
0.38.4
0.38.3
0.38.2
0.38.1
0.38.0
0.37.3
0.37.2
0.37.1
0.37.0
0.37.0-rc.2
0.37.0-rc.1
0.37.0-rc.0
0.36.1
0.36.0
0.35.1
0.35.0
0.34.2
0.34.1
0.34.0
0.33.0
0.32.2
0.32.1
0.32.0
0.31.2
0.31.1
0.31.0
0.30.9
0.30.8
0.30.7
0.30.6
0.30.5
0.30.4
0.30.3
0.30.2
0.30.1
0.30.0
0.29.4
0.29.3
0.29.2
0.29.1
0.29.0
0.28.6
0.28.5
0.28.4
0.28.3
0.28.2
0.28.1
0.28.0
0.27.3
0.27.2
0.27.1
0.27.0
0.26.0
0.25.5
0.25.4
0.25.3
0.25.2
0.25.1
0.25.0
0.24.2
0.24.1
0.24.0
0.23.0
0.22.6
0.22.5
0.22.4
0.22.2
0.22.1
0.22.0
0.21.3
0.21.2
0.21.1
0.21.0
0.20.2
0.20.1
0.20.0
0.19.3
0.19.2
0.19.1
0.19.0
0.19.0-rc
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
retired
0.17.1
0.17.0
retired
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.1
0.15.0
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.13.2
0.13.1
0.13.0
0.12.0
0.11.5
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.0
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.3
0.7.2
0.7.1
0.7.0
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
ExDoc is a documentation generation tool for Elixir
Current section
Files
Jump to
Current section
Files
lib/ex_doc/language/erlang.ex
defmodule ExDoc.Language.Erlang do
@moduledoc false
@behaviour ExDoc.Language
alias ExDoc.{Autolink, Refs}
@impl true
def module_data(module) do
":" <> id = inspect(module)
%{
id: id,
title: id,
type: module_type(module),
skip: false,
extra_callback_types: []
}
end
@impl true
def function_data(entry, module_state) do
{{_kind, name, arity}, _anno, _signature, _doc_content, _metadata} = entry
specs =
case Map.fetch(module_state.specs, {name, arity}) do
{:ok, specs} ->
[{:attribute, 0, :spec, {{name, arity}, specs}}]
:error ->
[]
end
%{
doc_fallback: fn -> nil end,
extra_annotations: [],
line: nil,
specs: specs
}
end
@impl true
def callback_data(entry, module_state) do
{{_kind, name, arity}, anno, signature, _doc, _metadata} = entry
specs =
case Map.fetch(module_state.callbacks, {name, arity}) do
{:ok, specs} ->
[{:attribute, 0, :callback, {{name, arity}, specs}}]
:error ->
[]
end
%{
actual_def: {name, arity},
line: anno_line(anno),
signature: signature,
specs: specs
}
end
@impl true
def type_data(entry, module_state) do
{{_kind, name, arity}, _anno, signature, _doc, _metadata} = entry
%{type: type, spec: spec, line: line} =
ExDoc.Language.Elixir.type_from_module_state(module_state, name, arity)
%{
type: type,
line: line,
spec: {:attribute, 0, :type, spec},
signature: signature
}
end
@impl true
def autolink_doc(ast, opts) do
config = struct!(Autolink, opts)
walk_doc(ast, config)
end
@impl true
def autolink_spec(nil, _opts) do
nil
end
def autolink_spec(ast, opts) do
config = struct!(Autolink, opts)
{name, quoted} =
case ast do
{:attribute, _, kind, {{name, _arity}, ast}} when kind in [:spec, :callback] ->
{name, Enum.map(ast, &Code.Typespec.spec_to_quoted(name, &1))}
{:attribute, _, kind, ast} when kind in [:type, :opaque] ->
{name, _, _} = ast
{name, Code.Typespec.type_to_quoted(ast)}
end
formatted = format_spec(ast)
autolink_spec(quoted, name, formatted, config)
end
@impl true
def highlight_info() do
%{
language_name: "erlang",
lexer: Makeup.Lexers.ErlangLexer,
opts: []
}
end
## Autolink
defp walk_doc(list, config) when is_list(list) do
Enum.map(list, &walk_doc(&1, config))
end
defp walk_doc(binary, _) when is_binary(binary) do
binary
end
defp walk_doc({:a, attrs, inner, _meta} = ast, config) do
case attrs[:rel] do
"https://erlang.org/doc/link/seeerl" ->
case String.split(attrs[:href], ":") do
[_] ->
autolink(:module, attrs[:href], inner, config)
[app, module] ->
autolink_app({:module, app, module}, config)
_ ->
# TODO investigate building OTP docs
ast
end
"https://erlang.org/doc/link/seemfa" ->
case String.split(attrs[:href], ":") do
[_] ->
autolink(:function, attrs[:href], inner, config)
[app, mfa] ->
autolink_app({:function, app, mfa}, config)
_ ->
# TODO investigate building OTP docs
ast
end
"https://erlang.org/doc/link/seetype" ->
case String.split(attrs[:href], ":") do
[_] ->
autolink(:type, attrs[:href], inner, config)
[app, mfa] ->
autolink_app({:type, app, mfa}, config)
end
"https://erlang.org/doc/link/seeapp" ->
case String.split(attrs[:href], ":") do
[app, "index"] ->
autolink_app({:app, app}, config)
_ ->
# TODO investigate building OTP docs
ast
end
_ ->
ast
end
end
defp walk_doc({tag, attrs, ast, meta}, config) do
{tag, attrs, walk_doc(ast, config), meta}
end
defp autolink_app({:app, app}, config) do
message = "application references are not supported: //#{app}"
Autolink.maybe_warn(message, config, nil, %{})
{:code, [], [app], %{}}
end
defp autolink_app({:module, app, module}, config) do
message = "application references are not supported: //#{app}/#{module}"
Autolink.maybe_warn(message, config, nil, %{})
{:code, [], [module], %{}}
end
defp autolink_app({:function, app, mfa}, config) do
mfa = String.replace(mfa, "#", ":")
message = "application references are not supported: //#{app}/#{mfa}"
Autolink.maybe_warn(message, config, nil, %{})
{:code, [], [mfa], %{}}
end
defp autolink_app({:type, app, mfa}, config) do
mfa = String.replace(mfa, "#", ":")
mfa = String.trim_trailing(mfa, "/0") <> "()"
message = "application references are not supported: //#{app}/#{mfa}"
Autolink.maybe_warn(message, config, nil, %{})
{:code, [], [mfa], %{}}
end
defp autolink(kind, string, inner, config) do
if url = url(kind, string, config) do
{:a, [href: url], inner, %{}}
else
inner
end
end
defp url(:module, string, config) do
ref = {:module, String.to_atom(string)}
do_url(ref, string, config)
end
defp url(kind, string, config) do
[module, name, arity] = String.split(string, ["#", "/"])
name = String.to_atom(name)
arity = String.to_integer(arity)
original_text =
if kind == :type do
"#{name}()"
else
"#{name}/#{arity}"
end
if module == "" do
ref = {kind, config.current_module, name, arity}
visibility = Refs.get_visibility(ref)
if visibility == :public do
final_url({kind, name, arity}, config)
else
Autolink.maybe_warn(ref, config, visibility, %{original_text: original_text})
nil
end
else
ref = {kind, String.to_atom(module), name, arity}
original_text = "#{module}:#{original_text}"
do_url(ref, original_text, config)
end
end
defp do_url(ref, original_text, config) do
visibility = Refs.get_visibility(ref)
if visibility == :public do
final_url(ref, config)
else
Autolink.maybe_warn(ref, config, visibility, %{original_text: original_text})
nil
end
end
defp final_url({:module, module}, config) do
tool = Autolink.tool(module)
Autolink.app_module_url(tool, module, config)
end
defp final_url({kind, name, arity}, _config) do
fragment(:ex_doc, kind, name, arity)
end
defp final_url({kind, module, name, arity}, config) do
tool = Autolink.tool(module)
module_url = Autolink.app_module_url(tool, module, config)
module_url && module_url <> fragment(tool, kind, name, arity)
end
defp fragment(:otp, :function, name, arity) do
"##{name}-#{arity}"
end
defp fragment(:otp, :type, name, _arity) do
"#type-#{name}"
end
defp fragment(_, :function, name, arity) do
"##{name}/#{arity}"
end
defp fragment(_, :type, name, arity) do
"#t:#{name}/#{arity}"
end
# Traverses quoted and formatted string of the typespec AST, replacing refs with links.
#
# Let's say we have this typespec:
#
# -spec f(X) -> #{atom() => bar(), integer() => X}.
#
# We traverse the AST and find types and their string representations:
#
# -spec f(X) -> #{atom() => bar(), integer() => X}.
# ^^^^ ^^^ ^^^^^^^
#
# atom/0 => atom
# bar/0 => bar
# integer/0 => integer
#
# We then traverse the formatted string, *in order*, replacing the type strings with links:
#
# "atom(" => "atom("
# "bar(" => "<a>bar</a>("
# "integer(" => "integer("
#
# Finally we end up with:
#
# -spec f(X) -> #{atom() => <a>bar</a>(), integer() => X}.
#
# All of this hassle is to preserve the original *text layout* of the initial representation,
# all the spaces, newlines, etc.
defp autolink_spec(quoted, name, formatted, config) do
acc =
for quoted <- List.wrap(quoted) do
{_, acc} =
Macro.prewalk(quoted, [], fn
# module.name(args)
{{:., _, [module, name]}, _, args}, acc ->
{{:t, [], args}, [{pp({module, name}), {module, name, length(args)}} | acc]}
{name, _, args} = ast, acc when is_atom(name) and is_list(args) ->
arity = length(args)
cond do
name in [:"::", :when, :%{}, :{}, :|, :->, :record] ->
{ast, acc}
# %{required(...) => ..., optional(...) => ...}
name in [:required, :optional] and arity == 1 ->
{ast, acc}
# name(args)
true ->
{ast, [{pp(name), {name, arity}} | acc]}
end
other, acc ->
{other, acc}
end)
acc |> Enum.reverse() |> Enum.drop(1)
end
|> Enum.concat()
put(acc)
# Drop and re-add type name (it, the first element in acc, is dropped there too)
#
# 1. foo() :: bar()
# 2. ) :: bar()
# 3. ) :: <a>bar</a>()
# 4. foo() :: <a>bar</a>()
name = pp(name)
formatted = String.trim_leading(formatted, name <> "(")
formatted = replace(formatted, acc, config)
name <> "(" <> formatted
end
defp replace(formatted, [], _config) do
formatted
end
defp replace(formatted, acc, config) do
String.replace(formatted, Enum.map(acc, &"#{elem(&1, 0)}("), fn string ->
string = String.trim_trailing(string, "(")
{other, ref} = pop()
if string != other do
Autolink.maybe_warn(
"internal inconsistency, please submit bug: #{inspect(string)} != #{inspect(other)}",
config,
nil,
nil
)
end
url =
case ref do
{name, arity} ->
visibility = Refs.get_visibility({:type, config.current_module, name, arity})
if visibility == :public do
final_url({:type, name, arity}, config)
end
{module, name, arity} ->
ref = {:type, module, name, arity}
visibility = Refs.get_visibility(ref)
if visibility == :public do
final_url(ref, config)
else
original_text = "#{string}/#{arity}"
Autolink.maybe_warn(ref, config, visibility, %{original_text: original_text})
nil
end
end
if url do
~s|<a href="#{url}">#{string}</a>(|
else
string <> "("
end
end)
end
defp put(items) do
Process.put({__MODULE__, :stack}, items)
end
defp pop() do
[head | tail] = Process.get({__MODULE__, :stack})
put(tail)
head
end
defp pp(name) when is_atom(name) do
:io_lib.format("~p", [name]) |> IO.iodata_to_binary()
end
defp pp({module, name}) when is_atom(module) and is_atom(name) do
:io_lib.format("~p:~p", [module, name]) |> IO.iodata_to_binary()
end
defp format_spec(ast) do
{:attribute, _, type, _} = ast
# `-type ` => 6
offset = byte_size(Atom.to_string(type)) + 2
options = [linewidth: 98 + offset]
:erl_pp.attribute(ast, options) |> IO.iodata_to_binary() |> trim_offset(offset)
end
## Helpers
defp module_type(module) do
cond do
function_exported?(module, :behaviour_info, 1) ->
:behaviour
true ->
:module
end
end
# `-type t() :: atom()` becomes `t() :: atom().`
defp trim_offset(binary, offset) do
binary
|> String.trim()
|> String.split("\n")
|> Enum.map(fn line ->
binary_part(line, offset, byte_size(line) - offset)
end)
|> Enum.join("\n")
end
defp anno_line(line) when is_integer(line), do: abs(line)
defp anno_line(anno), do: anno |> :erl_anno.line() |> abs()
end