Packages
ex_doc
0.11.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.ModuleNode do
@moduledoc """
Structure that represents a *module*
"""
defstruct id: nil, module: nil, moduledoc: nil,
docs: [], typespecs: [], source: nil, type: nil
end
defmodule ExDoc.FunctionNode do
@moduledoc """
Structure that holds all the elements of an individual *function*
"""
defstruct id: nil, name: nil, arity: 0, doc: [],
source: nil, type: nil, signature: nil, specs: []
end
defmodule ExDoc.TypeNode do
@moduledoc """
Structure that holds all the elements of an individual *type*
"""
defstruct id: nil, name: nil, arity: 0, type: nil,
spec: nil, doc: nil, signature: nil
end
defmodule ExDoc.Retriever.Error do
@moduledoc """
Structure that hold the message of a given exception
"""
defexception [:message]
end
defmodule ExDoc.Retriever do
@moduledoc """
Functions to extract documentation information from modules.
"""
alias ExDoc.Retriever.Error
alias Kernel.Typespec
@doc """
Extract documentation from all modules in the specified directory
"""
@spec docs_from_dir(Path.t, %ExDoc.Config{}) :: [%ExDoc.ModuleNode{}]
def docs_from_dir(dir, config) when is_binary(dir) do
files = Path.wildcard Path.expand("Elixir.*.beam", dir)
docs_from_files(files, config)
end
@doc """
Extract documentation from all modules in the specified list of files
"""
@spec docs_from_files([Path.t], %ExDoc.Config{}) :: [%ExDoc.ModuleNode{}]
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{}) :: [%ExDoc.ModuleNode{}]
def docs_from_modules(modules, config) when is_list(modules) do
modules
|> Enum.map(&get_module(&1, config))
|> Enum.filter(fn(x) -> x end)
|> Enum.sort(&(&1.id <= &2.id))
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, message: "module #{inspect module} is not defined/available")
type = detect_type(module)
module
|> verify_module()
|> generate_node(type, config)
end
defp verify_module(module) do
if function_exported?(module, :__info__, 1) do
case Code.get_docs(module, :moduledoc) do
{_line, false} ->
nil
{_, _} ->
module
nil ->
raise("module #{inspect module} was not compiled with flag --docs")
end
else
IO.puts(:stderr, "module #{inspect module} does not export __info__/1")
nil
end
end
defp generate_node(nil, _, _), do: nil
defp generate_node(module, type, config) do
source_url = config.source_url_pattern
source_path = source_path(module, config)
specs = get_specs(module)
impls = get_impls(module)
abst_code = get_abstract_code(module)
moduledoc = get_moduledoc(module)
line = find_actual_line(abst_code, module, :module)
docs = get_docs(type, module, source_path, source_url, specs, impls, abst_code) ++
get_callbacks(type, module, source_path, source_url, abst_code)
%ExDoc.ModuleNode{
id: inspect(module),
module: module,
type: type,
moduledoc: moduledoc,
docs: docs,
typespecs: get_types(module),
source: source_link(source_path, source_url, line)
}
end
# Helpers
defp get_moduledoc(module) do
{_, moduledoc} = Code.get_docs(module, :moduledoc)
moduledoc
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
defp get_docs(type, module, source_path, source_url, specs, impls, abst_code) do
docs = Enum.sort_by Code.get_docs(module, :docs), &elem(&1, 0)
for doc <- docs, doc?(doc, type) do
get_function(doc, source_path, source_url, specs, impls, abst_code)
end
end
# Skip impl_for and impl_for! for protocols
defp doc?({{name, _}, _, _, _, nil}, :protocol) when name in [:impl_for, :impl_for!] do
false
end
# Skip docs explicitly marked as false
defp doc?({_, _, _, _, false}, _) do
false
end
# Skip default docs if starting with _
defp doc?({{name, _}, _, _, _, nil}, _type) do
hd(Atom.to_char_list(name)) != ?_
end
# Everything else is ok
defp doc?(_, _) do
true
end
defp get_function(function, source_path, source_url, all_specs, cb_impls, abst_code) do
{{name, arity}, doc_line, type, signature, doc} = function
function = actual_def(name, arity, type)
line = find_actual_line(abst_code, function, :function) || doc_line
behaviour = Map.get(cb_impls, {name, arity})
if is_nil(doc) && behaviour do
doc = "Callback implementation for `c:#{inspect behaviour}.#{name}/#{arity}`."
end
specs = all_specs
|> Map.get(function, [])
|> Enum.map(&Typespec.spec_to_ast(name, &1))
%ExDoc.FunctionNode{
id: "#{name}/#{arity}",
name: name,
arity: arity,
doc: doc,
signature: get_call_signature(name, signature),
specs: specs,
source: source_link(source_path, source_url, line),
type: type
}
end
defp get_callbacks(:behaviour, module, source_path, source_url, abst_code) do
callbacks = Enum.into(Typespec.beam_callbacks(module) || [], %{})
docs =
if function_exported?(module, :__behaviour__, 1) do
module.__behaviour__(:docs)
else
Code.get_docs(module, :all)[:callback_docs]
end
docs = Enum.sort_by docs || [], &elem(&1, 0)
Enum.map(docs, &get_callback(&1, source_path, source_url, callbacks, abst_code))
end
defp get_callbacks(_, _, _, _, _), do: []
defp get_callback(callback, source_path, source_url, callbacks, abst_code) do
{{name, arity}, _, kind, doc} = callback
function = actual_def(name, arity, kind)
line = find_actual_line(abst_code, function, :callback)
# TODO: Remove defcallback and defmacrocallback
# once we no longer supported __behaviour__
kind =
case kind do
:def -> :callback
:defmacro -> :macrocallback
other -> other
end
specs =
callbacks
|> Map.get(function, [])
|> Enum.map(&Typespec.spec_to_ast(name, &1))
%ExDoc.FunctionNode{
id: "#{name}/#{arity}",
name: name,
arity: arity,
doc: doc || nil,
signature: get_typespec_signature(hd(specs), arity),
specs: specs,
source: source_link(source_path, source_url, line),
type: kind
}
end
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()
|> Enum.map(fn
{{:::, _, [left, _]}, i} -> to_var(left, i)
{{:|, _, _}, i} -> to_var({}, i)
{left, i} -> to_var(left, i)
end)
end
defp to_var({name, meta, _}, _) when is_atom(name),
do: {name, meta, 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(_, i),
do: {:"arg#{i}", [], nil}
defp get_call_signature(name, args) do
cond do
name in [:__aliases__, :__block__] ->
"#{name}(args)"
name in [:__ENV__, :__MODULE__, :__DIR__, :__CALLER__, :"%", :"%{}"] ->
"#{name}"
true ->
Macro.to_string {name, [], args}
end
end
defp actual_def(name, arity, :defmacro) do
{String.to_atom("MACRO-" <> to_string(name)), arity + 1}
end
defp actual_def(name, arity, _), do: {name, arity}
defp find_actual_line(abst_code, function, :callback) do
abst_code
|> Enum.find(&match?({:attribute, _, :callback, {^function, _}}, &1))
|> elem(1)
end
defp find_actual_line(abst_code, name, :module) do
abst_code
|> Enum.find(&match?({:attribute, _, :module, ^name}, &1))
|> elem(1)
end
defp find_actual_line(abst_code, {name, arity}, :function) do
case Enum.find(abst_code, &match?({:function, _, ^name, ^arity, _}, &1)) do
nil -> nil
tuple -> elem(tuple, 1)
end
end
# Detect if a module is an exception, struct,
# protocol, implementation or simply a module
defp detect_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
true -> :module
end
end
# Returns a dict of {name, arity} -> spec.
defp get_specs(module) do
Enum.into(Typespec.beam_specs(module) || [], %{})
end
# Returns a dict 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
:attributes
|> module.module_info
|> Enum.filter_map(&match?({:callback, _}, &1), fn {_, [{t,_}|_]} -> t end)
end
defp behaviours_implemented_by(module) do
:attributes
|> module.module_info
|> Stream.filter(&match?({:behaviour, _}, &1))
|> Stream.map(fn {_, l} -> l end)
|> Enum.concat()
end
defp get_types(module) do
all = Typespec.beam_types(module) || []
docs = Enum.into(Typespec.beam_typedocs(module) || [], %{})
types =
for {type, {name, _, args} = tuple} <- all, type != :typep do
spec = process_type_ast(Typespec.type_to_ast(tuple), type)
arity = length(args)
doc = docs[{name, arity}]
%ExDoc.TypeNode{
id: "#{name}/#{arity}",
name: name,
arity: arity,
type: type,
spec: spec,
doc: doc,
signature: get_typespec_signature(spec, arity)
}
end
Enum.sort_by types, &{&1.name, &1.arity}
end
defp source_link(_source_path, nil, _line), do: nil
defp source_link(source_path, source_url, 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 = module.__info__(:compile)[:source]
if root = config.source_root do
Path.relative_to(source, root)
else
source
end
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
end