Packages
ex_doc
0.24.2
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.{DocAST, GroupMatcher, Refs}
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.nested_context,
module.nested_title, 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
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(Elixir), do: false
defp docs_chunk(module) do
result = ExDoc.Utils.Code.fetch_docs(module)
Refs.insert_from_chunk(module, result)
case result do
# TODO: Once we require Elixir v1.12, we only keep modules that have map contents
{:docs_v1, _, _, _, :hidden, _, _} ->
false
{:docs_v1, _, _, _, _, _, _} = docs ->
_ = Code.ensure_loaded(module)
docs
{:error, :chunk_not_found} ->
false
{:error, :module_not_found} ->
unless Code.ensure_loaded?(module) do
raise Error, "module #{inspect(module)} is not defined/available"
end
{:error, _} = error ->
raise Error, "error accessing #{inspect(module)}: #{inspect(error)}"
_ ->
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
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, source_path)
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)
{nested_title, nested_context} = nesting_info(title, config.nest_modules_by_prefix)
node = %ExDoc.ModuleNode{
id: id,
title: title,
nested_title: nested_title,
nested_context: nested_context,
module: module,
type: module_data.type,
deprecated: metadata[:deprecated],
function_groups: function_groups,
docs: Enum.sort_by(docs, &sort_key(&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)
}
put_in(node.group, GroupMatcher.match_module(config.groups_for_modules, node))
end
defp sort_key(name, arity) do
first = name |> Atom.to_charlist() |> hd()
{first in ?a..?z, name, arity}
end
defp doc_ast(format, %{"en" => doc}, options),
do: DocAST.parse!(doc, format, options)
defp doc_ast(_, _, _options),
do: nil
# Module Helpers
defp get_module_data(module, docs_chunk) do
%{
name: module,
type: get_type(module),
specs: get_specs(module),
impls: get_impls(module),
callbacks: get_callbacks(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(module_data, source_path) do
{:docs_v1, anno, _, content_type, moduledoc, metadata, _} = module_data.docs
doc_line = anno_line(anno)
options = [file: source_path, line: doc_line + 1]
{doc_line, doc_ast(content_type, moduledoc, options), 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)), filter_defaults(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, _}, _, _, _, _}, :protocol) when name in [:impl_for, :impl_for!] do
false
end
# If content is a map, then it is ok.
defp doc?({_, _, _, %{}, _}, _) do
true
end
# We keep this clause with backwards compatibility with Elixir,
# from v1.12+, functions not starting with _ always default to %{}.
# TODO: Remove me once we require Elixir v1.12.
defp doc?({{_, name, _}, _, _, :none, _}, _type) do
hd(Atom.to_charlist(name)) != ?_
end
# Everything else is hidden.
defp doc?({_, _, _, _, _}, _) do
false
end
defp get_function(function, source, module_data, groups_for_functions) do
{:docs_v1, _, _, content_type, _, _, _} = module_data.docs
{{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
impl = Map.fetch(module_data.impls, actual_def)
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} ->
# TODO: should we call filter with the whole %FunctionNode{}, not just metadata?
# also, should we save off metadata on the node?
filter.(metadata) && group
end)
doc_ast =
(doc && doc_ast(content_type, doc, file: source.path, line: doc_line + 1)) ||
callback_doc_ast(name, arity, impl) ||
delegate_doc_ast(metadata[:delegate_to])
%ExDoc.FunctionNode{
id: "#{name}/#{arity}",
name: name,
arity: arity,
deprecated: metadata[:deprecated],
doc: doc_ast,
doc_line: doc_line,
defaults: Enum.sort_by(defaults, fn {name, arity} -> sort_key(name, arity) end),
signature: signature(signature),
specs: specs,
source_path: source.path,
source_url: source_link(source, line),
type: type,
group: group,
annotations: annotations
}
end
defp delegate_doc_ast({m, f, a}) do
[
{:p, [], ["See ", {:code, [class: "inline"], [Exception.format_mfa(m, f, a)], %{}}, "."],
%{}}
]
end
defp delegate_doc_ast(nil) do
nil
end
defp callback_doc_ast(name, arity, {:ok, behaviour}) do
[
{:p, [],
[
"Callback implementation for ",
{:code, [class: "inline"], ["c:#{inspect(behaviour)}.#{name}/#{arity}"], %{}},
"."
], %{}}
]
end
defp callback_doc_ast(_, _, _) do
nil
end
defp get_defaults(_name, _arity, 0), do: []
defp get_defaults(name, arity, defaults) do
for default <- (arity - defaults)..(arity - 1), do: {name, default}
end
defp filter_defaults(docs) do
Enum.map(docs, &filter_defaults(&1, docs))
end
defp filter_defaults(doc, docs) do
update_in(doc.defaults, fn defaults ->
Enum.reject(defaults, fn {name, arity} ->
Enum.any?(docs, &match?(%{name: ^name, arity: ^arity}, &1))
end)
end)
end
## Callback helpers
defp get_callbacks(%{type: :behaviour} = module_data, source) do
{:docs_v1, _, _, _, _, _, docs} = module_data.docs
optional_callbacks = module_data.name.behaviour_info(:optional_callbacks)
for {{kind, _, _}, _, _, _, _} = doc <- docs, kind in [:callback, :macrocallback] do
get_callback(doc, source, optional_callbacks, module_data)
end
end
defp get_callbacks(_, _), do: []
defp get_callback(callback, source, optional_callbacks, module_data) do
{:docs_v1, _, _, content_type, _, _, _} = module_data.docs
{{kind, name, arity}, anno, signature, doc, metadata} = callback
actual_def = actual_def(name, arity, kind)
doc_line = anno_line(anno)
signature = signature(signature)
{specs, line, signature} =
case Map.fetch(module_data.callbacks, actual_def) do
{:ok, specs} ->
{:type, anno, _, _} = hd(specs)
line = anno_line(anno)
specs = Enum.map(specs, &Code.Typespec.spec_to_quoted(name, &1))
signature = signature || get_typespec_signature(hd(specs), arity)
{specs, line, signature}
:error ->
{[], doc_line, signature || "#{name}/#{arity}"}
end
annotations = annotations_from_metadata(metadata)
annotations =
if actual_def in optional_callbacks, do: ["optional" | annotations], else: annotations
doc_ast = doc_ast(content_type, doc, file: source.path, line: doc_line + 1)
%ExDoc.FunctionNode{
id: "#{name}/#{arity}",
name: name,
arity: arity,
deprecated: metadata[:deprecated],
doc: doc_ast,
doc_line: doc_line,
signature: signature,
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, _} <- get_callbacks(behaviour),
do: {callback, behaviour},
into: %{}
end
defp get_callbacks(module) do
case Code.Typespec.fetch_callbacks(module) do
{:ok, callbacks} -> Map.new(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(module_data, source) do
{:docs_v1, _, _, _, _, _, docs} = module_data.docs
# TODO: When we require Elixir v1.12, we only keep contents that are maps
for {{:type, _, _}, _, _, content, _} = doc <- docs, content != :hidden do
get_type(doc, source, module_data)
end
end
defp get_type(type, source, module_data) do
{:docs_v1, _, _, content_type, _, _, _} = module_data.docs
{{_, name, arity}, anno, signature, doc, metadata} = type
doc_line = anno_line(anno)
annotations = annotations_from_metadata(metadata)
{:attribute, anno, type, spec} =
Enum.find(module_data.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)
signature = signature(signature) || get_typespec_signature(spec, arity)
annotations = if type == :opaque, do: ["opaque" | annotations], else: annotations
doc_ast = doc_ast(content_type, doc, file: source.path)
%ExDoc.TypeNode{
id: "#{name}/#{arity}",
name: name,
arity: arity,
type: type,
spec: spec,
deprecated: metadata[:deprecated],
doc: doc_ast,
doc_line: doc_line,
signature: signature,
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 signature([]), do: nil
defp signature(list) when is_list(list), do: Enum.join(list, " ")
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 remove_first_macro_arg({:when, meta, [lhs, rhs]}) do
{:when, meta, [remove_first_macro_arg(lhs), rhs]}
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 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.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