Packages
ex_doc
0.30.4
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, docs_chunk, _config) do
if abst_code = get_abstract_code(module) do
id = Atom.to_string(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
}
}
else
IO.warn("skipping docs for module #{inspect(module)}, reason: :no_debug_info", [])
end
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
@impl true
def format_spec_attribute(%ExDoc.TypeNode{type: type}), do: "-#{type}"
def format_spec_attribute(%ExDoc.FunctionNode{type: :callback}), do: "-callback"
def format_spec_attribute(%ExDoc.FunctionNode{}), do: "-spec"
## Shared between Erlang & Elixir
@doc false
def get_abstract_code(module) do
with {^module, binary, _file} <- :code.get_object_code(module),
{:ok, {_, [{:abstract_code, {_vsn, abstract_code}}]}} <-
:beam_lib.chunks(binary, [:abstract_code]) do
abstract_code
else
_ -> nil
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({:code, attrs, [code], meta} = ast, config) do
{text, url} =
case parse_autolink(code) do
{:module, module} = ref ->
text = Atom.to_string(module)
# Modules are parsed very permissively, so undefined/private
# modules do not emit warnings
url = do_url(ref, code, config, false)
{text, url}
{:local, kind, name, arity} ->
{"#{name}/#{arity}", local_url(kind, name, arity, config)}
{:remote, kind, module, name, arity} ->
text =
if kind == :type and arity == 0 do
"#{module}:#{name}()"
else
"#{module}:#{name}/#{arity}"
end
url = remote_url(kind, module, name, arity, config)
{text, url}
:error ->
{code, nil}
end
if url do
{:a, [href: url], {:code, attrs, [text], meta}, %{}}
else
ast
end
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, true)
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)
if module == "" do
local_url(kind, name, arity, config)
else
remote_url(kind, String.to_atom(module), name, arity, config)
end
end
defp remote_url(kind, module, name, arity, config) do
ref = {kind, module, name, arity}
text =
if kind == :type and arity == 0 do
"#{module}:#{name}()"
else
"#{module}:#{name}/#{arity}"
end
do_url(ref, text, config, true)
end
defp local_url(kind, name, arity, config) do
ref = {kind, config.current_module, name, arity}
visibility = Refs.get_visibility(ref)
if visibility == :public do
final_url({kind, name, arity}, config)
else
original_text =
if kind == :type and arity == 0 do
"#{name}()"
else
"#{name}/#{arity}"
end
Autolink.maybe_warn(ref, config, visibility, %{original_text: original_text})
nil
end
end
defp do_url(ref, original_text, config, emit_warning) 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
if emit_warning do
Autolink.maybe_warn(ref, config, visibility, %{original_text: original_text})
end
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
defp kind("c:" <> rest), do: {:callback, rest}
defp kind("t:" <> rest), do: {:type, rest}
defp kind(rest), do: {:function, rest}
defp parse_autolink(string) do
case Regex.run(~r{^(.+)(/\d+|\(\))$}, string) do
[_, left, right] ->
with {:ok, arity} <- parse_arity(right) do
{kind, rest} = kind(left)
case parse_module_function(rest) do
{:local, name} ->
{:local, kind, name, arity}
{:remote, module, name} ->
{:remote, kind, module, name, arity}
:error ->
:error
end
end
nil ->
parse_module(string)
_ ->
:error
end
end
# 0-arity types may take the form `t:module:type()`.
defp parse_arity("()") do
{:ok, 0}
end
defp parse_arity("/" <> arity_string) do
case Integer.parse(arity_string) do
{arity, ""} -> {:ok, arity}
_ -> :error
end
end
defp parse_module_function(string) do
case String.split(string, ":") do
[module_string, function_string] ->
with {:module, module} <- parse_module(module_string),
{:function, function} <- parse_function(function_string) do
{:remote, module, function}
end
[function_string] ->
with {:function, function} <- parse_function(function_string) do
{:local, function}
end
_ ->
:error
end
end
defp parse_function(string) do
case Code.string_to_quoted("& #{string}/0") do
{:ok, {:&, _, [{:/, _, [{function, _, _}, 0]}]}} when is_atom(function) ->
{:function, function}
_ ->
:error
end
end
defp parse_module(string) do
case Code.string_to_quoted(":#{string}", warn_on_unnecessary_quotes: false) do
{:ok, module} when is_atom(module) ->
{:module, module}
_ ->
:error
end
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 == :record and acc != [] ->
{ast, acc}
name in [:"::", :when, :%{}, :{}, :|, :->] ->
{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