Packages
ex_doc
0.27.3
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
# TODO: Move :hidden handling to retriever, as it is shared across all BEAM languages
def module_data(module, docs_chunk, _config) do
{:docs_v1, _, _, _, doc, _, _} = docs_chunk
if doc != :hidden do
module_data(module, docs_chunk)
else
:skip
end
end
def module_data(module, docs_chunk) do
# Make sure the module is loaded for future checks
_ = Code.ensure_loaded(module)
id = Atom.to_string(module)
abst_code = get_abstract_code(module)
line = find_module_line(module, abst_code)
type = module_type(module)
optional_callbacks = type == :behaviour && module.behaviour_info(:optional_callbacks)
%{
module: module,
docs: docs_chunk,
language: __MODULE__,
id: id,
title: id,
type: type,
line: line,
callback_types: [:callback],
nesting_info: nil,
private: %{
abst_code: abst_code,
specs: get_specs(module),
callbacks: get_callbacks(module),
optional_callbacks: optional_callbacks
}
}
end
@impl true
def function_data(entry, module_data) do
{{kind, name, arity}, _anno, _signature, doc_content, _metadata} = entry
# TODO: Edoc on Erlang/OTP24.1+ includes private functions in
# the chunk, so we manually yank them out for now.
if kind == :function and doc_content != :hidden and
function_exported?(module_data.module, name, arity) do
function_data(name, arity, doc_content, module_data)
else
:skip
end
end
defp function_data(name, arity, _doc_content, module_data) do
specs =
case Map.fetch(module_data.private.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_data) do
{{_kind, name, arity}, anno, signature, _doc, _metadata} = entry
extra_annotations =
if {name, arity} in module_data.private.optional_callbacks, do: ["optional"], else: []
specs =
case Map.fetch(module_data.private.callbacks, {name, arity}) do
{:ok, specs} ->
[{:attribute, 0, :callback, {{name, arity}, specs}}]
:error ->
[]
end
%{
line: anno_line(anno),
signature: signature,
specs: specs,
extra_annotations: extra_annotations
}
end
@impl true
def type_data(entry, module_data) do
{{kind, name, arity}, anno, signature, _doc, _metadata} = entry
case ExDoc.Language.Elixir.type_from_module_data(module_data, name, arity) do
%{} = map ->
%{
type: map.type,
line: map.line,
spec: {:attribute, 0, map.type, map.spec},
signature: signature
}
nil ->
%{
type: kind,
line: anno_line(anno),
spec: nil,
signature: signature
}
end
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({:attribute, _, :opaque, ast}, _opts) do
{name, _, args} = ast
args =
for arg <- args do
{:var, _, name} = arg
Atom.to_string(name)
end
|> Enum.intersperse(", ")
IO.iodata_to_binary([Atom.to_string(name), "(", args, ")"])
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, _, :type, ast} ->
{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
## Shared between Erlang & Elixir
@doc false
def get_abstract_code(module) do
case :code.get_object_code(module) do
{^module, binary, _file} ->
case :beam_lib.chunks(binary, [:abstract_code]) do
{:ok, {_, [{:abstract_code, {_vsn, abstract_code}}]}} -> abstract_code
_otherwise -> []
end
:error ->
[]
end
end
@doc false
def find_module_line(module, abst_code) do
Enum.find_value(abst_code, fn
{:attribute, anno, :module, ^module} -> anno_line(anno)
_ -> nil
end)
end
# Returns a map of {name, arity} => spec.
def get_specs(module) do
case Code.Typespec.fetch_specs(module) do
{:ok, specs} -> Map.new(specs)
:error -> %{}
end
end
def get_callbacks(module) do
case Code.Typespec.fetch_callbacks(module) do
{:ok, callbacks} -> Map.new(callbacks)
:error -> %{}
end
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" ->
{fragment, url} = extract_fragment(attrs[:href] || "")
case String.split(url, ":") do
[module] ->
autolink(:module, module, fragment, inner, config)
[app, module] ->
inner = strip_app(inner, app)
autolink(:module, module, fragment, inner, config)
_ ->
warn_ref(attrs[:href], config)
inner
end
"https://erlang.org/doc/link/seemfa" ->
{kind, url} =
case String.split(attrs[:href], "Module:") do
[url] -> {:function, url}
[left, right] -> {:callback, left <> right}
end
case String.split(url, ":") do
[mfa] ->
autolink(kind, mfa, "", inner, config)
[app, mfa] ->
inner = strip_app(inner, app)
autolink(kind, mfa, "", inner, config)
end
"https://erlang.org/doc/link/seetype" ->
case String.split(attrs[:href], ":") do
[type] ->
autolink(:type, type, "", inner, config)
[app, type] ->
inner = strip_app(inner, app)
autolink(:type, type, "", inner, config)
end
"https://erlang.org/doc/link/" <> see ->
warn_ref(attrs[:href] <> " (#{see})", config)
inner
_ ->
ast
end
end
defp walk_doc({tag, attrs, ast, meta}, config) do
{tag, attrs, walk_doc(ast, config), meta}
end
defp extract_fragment(url) do
case String.split(url, "#", parts: 2) do
[url] -> {"", url}
[url, fragment] -> {"#" <> fragment, url}
end
end
defp strip_app([{:code, attrs, [code], meta}], app) do
[{:code, attrs, strip_app(code, app), meta}]
end
defp strip_app(code, app) when is_binary(code) do
String.trim_leading(code, "//#{app}/")
end
defp strip_app(other, _app) do
other
end
defp warn_ref(href, config) do
message = "invalid reference: #{href}"
Autolink.maybe_warn(message, config, nil, %{})
end
defp autolink(kind, string, fragment, inner, config) do
if url = url(kind, string, config) do
{:a, [href: url <> fragment], 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] =
case String.split(string, ["#", "/"]) do
[module, name, arity] ->
[module, name, arity]
# this is what docgen_xml_to_chunk returns
[module, name] when kind == :type ->
# TODO: don't assume 0-arity, instead find first {:type, module, name, arity} ref
# and use that arity.
[module, name, "0"]
end
name = String.to_atom(name)
arity = String.to_integer(arity)
original_text =
if kind == :type and arity == 0 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)
# TODO: type with content = %{} in otp xml is marked as :hidden, it should be :public
if visibility == :public or (visibility == :hidden and elem(ref, 0) == :type) 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, config)
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, config)
module_url = Autolink.app_module_url(tool, module, config)
# TODO: fix me
module_url = String.trim_trailing(module_url, "#content")
module_url <> fragment(tool, kind, name, arity)
end
defp fragment(:otp, :function, name, arity) do
"##{name}-#{arity}"
end
defp fragment(:otp, :callback, name, arity) do
"#Module:#{name}-#{arity}"
end
defp fragment(:otp, :type, name, _arity) do
"#type-#{name}"
end
defp fragment(:ex_doc, :function, name, arity) do
"##{name}/#{arity}"
end
defp fragment(:ex_doc, :callback, name, arity) do
"#c:#{name}/#{arity}"
end
defp fragment(:ex_doc, :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
{_quoted, acc} =
Macro.prewalk(quoted, [], fn
# module.name(args)
{{:., _, [module, name]}, _, args}, acc ->
{{:t, [], args}, [{pp({module, name}), {module, name, length(args)}} | acc]}
{name, _, _}, acc when name in [:<<>>, :..] ->
{nil, acc}
# -1
{:-, _, [int]}, acc when is_integer(int) ->
{nil, acc}
# fun() (spec_to_quoted expands it to (... -> any())
{:->, _, [[{name, _, _}], {:any, _, _}]}, acc when name == :... ->
{nil, acc}
# #{x :: t()}
{:field_type, _, [name, type]}, acc when is_atom(name) ->
{type, 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()
# drop the name of the typespec
|> 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 = trim_name(formatted, name)
formatted = replace(formatted, acc, config)
name <> formatted
end
defp trim_name(string, name) do
name_size = byte_size(name)
binary_part(string, name_size, byte_size(string) - name_size)
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 in [:public, :hidden] do
final_url({:type, name, arity}, config)
end
{module, name, arity} ->
ref = {:type, module, name, arity}
visibility = Refs.get_visibility(ref)
if visibility in [:public, :hidden] 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