Packages
ex_doc
0.21.1
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/retriever.ex
defmodule ExDoc.Retriever do
# Functions to extract documentation information from modules.
@moduledoc false
defmodule Error do
@moduledoc false
defexception [:message]
end
alias ExDoc.{GroupMatcher}
alias ExDoc.Retriever.Error
@doc """
Extract documentation from all modules in the specified directory or directories.
"""
@spec docs_from_dir(Path.t() | [Path.t()], ExDoc.Config.t()) :: [ExDoc.ModuleNode.t()]
def docs_from_dir(dir, config) when is_binary(dir) do
pattern = if config.filter_prefix, do: "Elixir.#{config.filter_prefix}*.beam", else: "*.beam"
files = Path.wildcard(Path.expand(pattern, dir))
docs_from_files(files, config)
end
def docs_from_dir(dirs, config) when is_list(dirs) do
Enum.flat_map(dirs, &docs_from_dir(&1, config))
end
@doc """
Extract documentation from all modules in the specified list of files
"""
@spec docs_from_files([Path.t()], ExDoc.Config.t()) :: [ExDoc.ModuleNode.t()]
def docs_from_files(files, config) when is_list(files) do
files
|> Enum.map(&filename_to_module(&1))
|> docs_from_modules(config)
end
@doc """
Extract documentation from all modules in the list `modules`
"""
@spec docs_from_modules([atom], ExDoc.Config.t()) :: [ExDoc.ModuleNode.t()]
def docs_from_modules(modules, config) when is_list(modules) do
modules
|> Enum.flat_map(&get_module(&1, config))
|> Enum.sort_by(fn module ->
{GroupMatcher.group_index(config.groups_for_modules, module.group), module.id}
end)
end
defp filename_to_module(name) do
name = Path.basename(name, ".beam")
String.to_atom(name)
end
# Get all the information from the module and compile
# it. If there is an error while retrieving the information (like
# the module is not available or it was not compiled
# with --docs flag), we raise an exception.
defp get_module(module, config) do
unless Code.ensure_loaded?(module) do
raise Error, "module #{inspect(module)} is not defined/available"
end
if docs_chunk = docs_chunk(module) do
generate_node(module, docs_chunk, config)
else
[]
end
end
defp nesting_info(title, prefixes) do
prefixes
|> Enum.find(&String.starts_with?(title, &1 <> "."))
|> case do
nil -> {nil, nil}
prefix -> {String.trim_leading(title, prefix <> "."), prefix}
end
end
# Special case required for Elixir
defp docs_chunk(:elixir_bootstrap), do: false
defp docs_chunk(module) do
unless function_exported?(Code, :fetch_docs, 1) do
raise Error,
"ExDoc 0.19+ requires Elixir v1.7 and later. " <>
"For earlier Elixir versions, make sure to depend on {:ex_doc, \"~> 0.18.0\"}"
end
if function_exported?(module, :__info__, 1) do
case Code.fetch_docs(module) do
{:docs_v1, _, _, _, :hidden, _, _} ->
false
{:docs_v1, _, _, _, _, _, _} = docs ->
docs
{:error, reason} ->
raise Error,
"module #{inspect(module)} was not compiled with flag --docs: #{inspect(reason)}"
_ ->
raise Error,
"unknown format in Docs chunk. This likely means you are running on " <>
"a more recent Elixir version that is not supported by ExDoc. Please update."
end
else
false
end
end
defp generate_node(module, docs_chunk, config) do
module_data = get_module_data(module, docs_chunk)
case module_data do
%{type: :impl} -> []
_ -> [do_generate_node(module, module_data, config)]
end
end
defp do_generate_node(module, module_data, config) do
source_url = config.source_url_pattern
source_path = source_path(module, config)
source = %{url: source_url, path: source_path}
{doc_line, moduledoc, metadata} = get_module_docs(module_data)
line = find_module_line(module_data) || doc_line
{function_groups, function_docs} = get_docs(module_data, source, config)
docs = function_docs ++ get_callbacks(module_data, source)
types = get_types(module_data, source)
{title, id} = module_title_and_id(module_data)
module_group = GroupMatcher.match_module(config.groups_for_modules, module, id)
{nested_title, nested_context} = nesting_info(title, config.nest_modules_by_prefix)
%ExDoc.ModuleNode{
id: id,
title: title,
nested_title: nested_title,
nested_context: nested_context,
module: module_data.name,
group: module_group,
type: module_data.type,
deprecated: metadata[:deprecated],
function_groups: function_groups,
docs: Enum.sort_by(docs, &{&1.name, &1.arity}),
doc: moduledoc,
doc_line: doc_line,
typespecs: Enum.sort_by(types, &{&1.name, &1.arity}),
source_path: source_path,
source_url: source_link(source, line)
}
end
# Module Helpers
defp get_module_data(module, docs_chunk) do
%{
name: module,
type: get_type(module),
specs: get_specs(module),
impls: get_impls(module),
abst_code: get_abstract_code(module),
docs: docs_chunk
}
end
defp get_type(module) do
cond do
function_exported?(module, :__struct__, 0) and
match?(%{__exception__: true}, module.__struct__) ->
:exception
function_exported?(module, :__protocol__, 1) ->
:protocol
function_exported?(module, :__impl__, 1) ->
:impl
function_exported?(module, :behaviour_info, 1) ->
:behaviour
match?("Elixir.Mix.Tasks." <> _, Atom.to_string(module)) ->
:task
true ->
:module
end
end
defp get_module_docs(%{docs: {:docs_v1, anno, _, _, moduledoc, metadata, _}}) do
{anno_line(anno), docstring(moduledoc), metadata}
end
defp get_abstract_code(module) do
{^module, binary, _file} = :code.get_object_code(module)
case :beam_lib.chunks(binary, [:abstract_code]) do
{:ok, {_, [{:abstract_code, {_vsn, abstract_code}}]}} -> abstract_code
_otherwise -> []
end
end
## Function helpers
defp get_docs(%{type: type, docs: docs} = module_data, source, config) do
{:docs_v1, _, _, _, _, _, docs} = docs
groups_for_functions =
Enum.map(config.groups_for_functions, fn {group, filter} ->
{Atom.to_string(group), filter}
end) ++ [{"Functions", fn _ -> true end}]
function_docs =
for doc <- docs, doc?(doc, type) do
get_function(doc, source, module_data, groups_for_functions)
end
{Enum.map(groups_for_functions, &elem(&1, 0)), function_docs}
end
# We are only interested in functions and macros for now
defp doc?({{kind, _, _}, _, _, _, _}, _) when kind not in [:function, :macro] do
false
end
# Skip impl_for and impl_for! for protocols
defp doc?({{_, name, _}, _, _, :none, _}, :protocol) when name in [:impl_for, :impl_for!] do
false
end
# Skip docs explicitly marked as hidden
defp doc?({_, _, _, :hidden, _}, _) do
false
end
# Skip default docs if starting with _
defp doc?({{_, name, _}, _, _, :none, _}, _type) do
hd(Atom.to_charlist(name)) != ?_
end
# Everything else is ok
defp doc?(_, _) do
true
end
defp get_function(function, source, module_data, groups_for_functions) do
{{type, name, arity}, anno, signature, doc, metadata} = function
actual_def = actual_def(name, arity, type)
doc_line = anno_line(anno)
annotations = annotations_from_metadata(metadata)
line = find_function_line(module_data, actual_def) || doc_line
doc = docstring(doc, name, arity, type, Map.fetch(module_data.impls, {name, arity}))
defaults = get_defaults(name, arity, Map.get(metadata, :defaults, 0))
specs =
module_data.specs
|> Map.get(actual_def, [])
|> Enum.map(&Code.Typespec.spec_to_quoted(name, &1))
specs =
if type == :macro do
Enum.map(specs, &remove_first_macro_arg/1)
else
specs
end
annotations =
case {type, name, arity} do
{:macro, _, _} -> ["macro" | annotations]
{_, :__struct__, 0} -> ["struct" | annotations]
_ -> annotations
end
group =
Enum.find_value(groups_for_functions, fn {group, filter} ->
filter.(metadata) && group
end)
%ExDoc.FunctionNode{
id: "#{name}/#{arity}",
name: name,
arity: arity,
deprecated: metadata[:deprecated],
doc: doc || delegate_doc(metadata[:delegate_to]),
doc_line: doc_line,
defaults: defaults,
signature: Enum.join(signature, " "),
specs: specs,
source_path: source.path,
source_url: source_link(source, line),
type: type,
group: group,
annotations: annotations
}
end
defp delegate_doc(nil), do: nil
defp delegate_doc({m, f, a}), do: "See `#{Exception.format_mfa(m, f, a)}`."
defp docstring(:none, name, arity, type, {:ok, behaviour}) do
info = "Callback implementation for `c:#{inspect(behaviour)}.#{name}/#{arity}`."
with {:docs_v1, _, _, _, _, _, docs} <- Code.fetch_docs(behaviour),
key = {definition_to_callback(type), name, arity},
{_, _, _, doc, _} <- List.keyfind(docs, key, 0),
docstring when is_binary(docstring) <- docstring(doc) do
"#{docstring}\n\n#{info}"
else
_ -> info
end
end
defp docstring(doc, _, _, _, _), do: docstring(doc)
defp definition_to_callback(:function), do: :callback
defp definition_to_callback(:macro), do: :macrocallback
defp get_defaults(_name, _arity, 0), do: []
defp get_defaults(name, arity, defaults) do
for default <- (arity - defaults)..(arity - 1), do: "#{name}/#{default}"
end
## Callback helpers
defp get_callbacks(%{type: :behaviour, name: name, abst_code: abst_code, docs: docs}, source) do
{:docs_v1, _, _, _, _, _, docs} = docs
optional_callbacks = name.behaviour_info(:optional_callbacks)
for {{kind, _, _}, _, _, _, _} = doc <- docs, kind in [:callback, :macrocallback] do
get_callback(doc, source, optional_callbacks, abst_code)
end
end
defp get_callbacks(_, _), do: []
defp get_callback(callback, source, optional_callbacks, abst_code) do
{{kind, name, arity}, anno, _, doc, metadata} = callback
actual_def = actual_def(name, arity, kind)
doc_line = anno_line(anno)
annotations = annotations_from_metadata(metadata)
{:attribute, anno, :callback, {^actual_def, specs}} =
Enum.find(abst_code, &match?({:attribute, _, :callback, {^actual_def, _}}, &1))
line = anno_line(anno) || doc_line
specs = Enum.map(specs, &Code.Typespec.spec_to_quoted(name, &1))
annotations =
if actual_def in optional_callbacks, do: ["optional" | annotations], else: annotations
%ExDoc.FunctionNode{
id: "#{name}/#{arity}",
name: name,
arity: arity,
deprecated: metadata[:deprecated],
doc: docstring(doc),
doc_line: doc_line,
signature: get_typespec_signature(hd(specs), arity),
specs: specs,
source_path: source.path,
source_url: source_link(source, line),
type: kind,
annotations: annotations
}
end
## Typespecs
# Returns a map of {name, arity} => spec.
defp get_specs(module) do
case Code.Typespec.fetch_specs(module) do
{:ok, specs} -> Map.new(specs)
:error -> %{}
end
end
# Returns a map of {name, arity} => behaviour.
defp get_impls(module) do
for behaviour <- behaviours_implemented_by(module),
callback <- callbacks_defined_by(behaviour),
do: {callback, behaviour},
into: %{}
end
defp callbacks_defined_by(module) do
case Code.Typespec.fetch_callbacks(module) do
{:ok, callbacks} -> Keyword.keys(callbacks)
:error -> []
end
end
defp behaviours_implemented_by(module) do
for {:behaviour, list} <- module.module_info(:attributes),
behaviour <- list,
do: behaviour
end
defp get_types(%{docs: docs} = module_data, source) do
{:docs_v1, _, _, _, _, _, docs} = docs
for {{:type, _, _}, _, _, content, _} = doc <- docs, content != :hidden do
get_type(doc, source, module_data.abst_code)
end
end
defp get_type(type, source, abst_code) do
{{_, name, arity}, anno, _, doc, metadata} = type
doc_line = anno_line(anno)
annotations = annotations_from_metadata(metadata)
{:attribute, anno, type, spec} =
Enum.find(abst_code, fn
{:attribute, _, type, {^name, _, args}} ->
type in [:opaque, :type] and length(args) == arity
_ ->
false
end)
spec = spec |> Code.Typespec.type_to_quoted() |> process_type_ast(type)
line = anno_line(anno) || doc_line
annotations = if type == :opaque, do: ["opaque" | annotations], else: annotations
%ExDoc.TypeNode{
id: "#{name}/#{arity}",
name: name,
arity: arity,
type: type,
spec: spec,
deprecated: metadata[:deprecated],
doc: docstring(doc),
doc_line: doc_line,
signature: get_typespec_signature(spec, arity),
source_path: source.path,
source_url: source_link(source, line),
annotations: annotations
}
end
# Cut off the body of an opaque type while leaving it on a normal type.
defp process_type_ast({:"::", _, [d | _]}, :opaque), do: d
defp process_type_ast(ast, _), do: ast
defp get_typespec_signature({:when, _, [{:"::", _, [{name, meta, args}, _]}, _]}, arity) do
Macro.to_string({name, meta, strip_types(args, arity)})
end
defp get_typespec_signature({:"::", _, [{name, meta, args}, _]}, arity) do
Macro.to_string({name, meta, strip_types(args, arity)})
end
defp get_typespec_signature({name, meta, args}, arity) do
Macro.to_string({name, meta, strip_types(args, arity)})
end
defp strip_types(args, arity) do
args
|> Enum.take(-arity)
|> Enum.with_index(1)
|> Enum.map(fn
{{:"::", _, [left, _]}, position} -> to_var(left, position)
{{:|, _, _}, position} -> to_var({}, position)
{left, position} -> to_var(left, position)
end)
end
defp to_var({:%, meta, [name, _]}, _), do: {:%, meta, [name, {:%{}, meta, []}]}
defp to_var({name, meta, _}, _) when is_atom(name), do: {name, meta, nil}
defp to_var([{:->, _, _} | _], _), do: {:function, [], nil}
defp to_var({:<<>>, _, _}, _), do: {:binary, [], nil}
defp to_var({:%{}, _, _}, _), do: {:map, [], nil}
defp to_var({:{}, _, _}, _), do: {:tuple, [], nil}
defp to_var({_, _}, _), do: {:tuple, [], nil}
defp to_var(integer, _) when is_integer(integer), do: {:integer, [], nil}
defp to_var(float, _) when is_integer(float), do: {:float, [], nil}
defp to_var(list, _) when is_list(list), do: {:list, [], nil}
defp to_var(atom, _) when is_atom(atom), do: {:atom, [], nil}
defp to_var(_, position), do: {:"arg#{position}", [], nil}
## General helpers
defp actual_def(name, arity, :macrocallback) do
{String.to_atom("MACRO-" <> to_string(name)), arity + 1}
end
defp actual_def(name, arity, :macro) do
{String.to_atom("MACRO-" <> to_string(name)), arity + 1}
end
defp actual_def(name, arity, _), do: {name, arity}
defp annotations_from_metadata(metadata) do
annotations = []
annotations =
if since = metadata[:since] do
["since #{since}" | annotations]
else
annotations
end
annotations
end
defp remove_first_macro_arg({:"::", info, [{name, info2, [_term_arg | rest_args]}, return]}) do
{:"::", info, [{name, info2, rest_args}, return]}
end
defp find_module_line(%{abst_code: abst_code, name: name}) do
Enum.find_value(abst_code, fn
{:attribute, anno, :module, ^name} -> anno_line(anno)
_ -> nil
end)
end
defp find_function_line(%{abst_code: abst_code}, {name, arity}) do
Enum.find_value(abst_code, fn
{:function, anno, ^name, ^arity, _} -> anno_line(anno)
_ -> nil
end)
end
defp docstring(%{"en" => doc}), do: doc
defp docstring(_), do: nil
defp anno_line(line) when is_integer(line), do: abs(line)
defp anno_line(anno), do: anno |> :erl_anno.line() |> abs()
defp source_link(%{path: _, url: nil}, _line), do: nil
defp source_link(source, line) do
source_url = Regex.replace(~r/%{path}/, source.url, source.path)
Regex.replace(~r/%{line}/, source_url, to_string(line))
end
defp source_path(module, config) do
source = String.Chars.to_string(module.__info__(:compile)[:source])
if root = config.source_root do
Path.relative_to(source, root)
else
source
end
end
defp module_title_and_id(%{name: module, type: :task}) do
{"mix " <> task_name(module), module_id(module)}
end
defp module_title_and_id(%{name: module}) do
id = module_id(module)
{id, id}
end
defp module_id(module) do
case inspect(module) do
":" <> inspected -> inspected
inspected -> inspected
end
end
defp task_name(module) do
"Elixir.Mix.Tasks." <> name = Atom.to_string(module)
name
|> String.split(".")
|> Enum.map_join(".", &Macro.underscore/1)
end
end