Packages
ex_doc
0.27.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
files = Path.wildcard(Path.expand("*.beam", 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
defp get_module(module, config) do
with {:docs_v1, _, language, _, _, metadata, _} = docs_chunk <- docs_chunk(module),
true <- config.filter_modules.(module, metadata),
{:ok, language} <- ExDoc.Language.get(language, module),
%{} = module_data <- language.module_data(module, docs_chunk, config) do
[generate_node(module, module_data, config)]
else
_ ->
[]
end
end
defp docs_chunk(module) do
result = ExDoc.Utils.Code.fetch_docs(module)
Refs.insert_from_chunk(module, result)
case result do
{:docs_v1, _, _, _, :hidden, _, _} ->
false
{:docs_v1, _, _, _, _, _, _} = docs ->
case Code.ensure_loaded(module) do
{:module, _} ->
docs
{:error, reason} ->
IO.warn("skipping module #{inspect(module)}, reason: #{reason}", [])
false
end
{: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, 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)
# TODO: The default function groups must be returned by the language
groups_for_functions =
config.groups_for_functions ++ [Callbacks: & &1[:__callback__], Functions: fn _ -> true end]
function_groups = Enum.map(groups_for_functions, &elem(&1, 0))
function_docs = get_docs(module_data, source, groups_for_functions)
docs = function_docs ++ get_callbacks(module_data, source, groups_for_functions)
types = get_types(module_data, source)
metadata = Map.put(metadata, :__type__, module_data.type)
group = GroupMatcher.match_module(config.groups_for_modules, module, module_data.id, metadata)
{nested_title, nested_context} = module_data.nesting_info || {nil, nil}
%ExDoc.ModuleNode{
id: module_data.id,
title: module_data.title,
nested_title: nested_title,
nested_context: nested_context,
group: group,
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, module_data.line),
language: module_data.language,
annotations: List.wrap(metadata[:tags])
}
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_content}, options) do
DocAST.parse!(doc_content, format, options)
end
defp doc_ast(_, _, _options) do
nil
end
# Module Helpers
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
## Function helpers
defp get_docs(module_data, source, groups_for_functions) do
{:docs_v1, _, _, _, _, _, doc_elements} = module_data.docs
nodes =
Enum.flat_map(doc_elements, fn doc_element ->
case module_data.language.function_data(doc_element, module_data) do
:skip ->
[]
function_data ->
[get_function(doc_element, function_data, source, module_data, groups_for_functions)]
end
end)
filter_defaults(nodes)
end
defp get_function(doc_element, function_data, source, module_data, groups_for_functions) do
{:docs_v1, _, _, content_type, _, _, _} = module_data.docs
{{type, name, arity}, anno, signature, doc_content, metadata} = doc_element
doc_line = anno_line(anno)
annotations = annotations_from_metadata(metadata) ++ function_data.extra_annotations
line = function_data.line || doc_line
defaults = get_defaults(name, arity, Map.get(metadata, :defaults, 0))
doc_ast =
(doc_content && doc_ast(content_type, doc_content, file: source.path, line: doc_line + 1)) ||
function_data.doc_fallback.()
group = GroupMatcher.match_function(groups_for_functions, metadata)
%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: function_data.specs,
source_path: source.path,
source_url: source_link(source, line),
type: type,
group: group,
annotations: annotations
}
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(nodes) do
Enum.map(nodes, &filter_defaults(&1, nodes))
end
defp filter_defaults(node, nodes) do
update_in(node.defaults, fn defaults ->
Enum.reject(defaults, fn {name, arity} ->
Enum.any?(nodes, &match?(%{name: ^name, arity: ^arity}, &1))
end)
end)
end
## Callback helpers
defp get_callbacks(%{type: :behaviour} = module_data, source, groups_for_functions) do
{:docs_v1, _, _, _, _, _, docs} = module_data.docs
for {{kind, _, _}, _, _, _, _} = doc <- docs, kind in module_data.callback_types do
get_callback(doc, source, groups_for_functions, module_data)
end
end
defp get_callbacks(_, _, _), do: []
defp get_callback(callback, source, groups_for_functions, module_data) do
callback_data = module_data.language.callback_data(callback, module_data)
{:docs_v1, _, _, content_type, _, _, _} = module_data.docs
{{kind, name, arity}, anno, _signature, doc, metadata} = callback
doc_line = anno_line(anno)
signature = signature(callback_data.signature)
specs = callback_data.specs
annotations = callback_data.extra_annotations ++ annotations_from_metadata(metadata)
doc_ast = doc_ast(content_type, doc, file: source.path, line: doc_line + 1)
metadata = Map.put(metadata, :__callback__, true)
group = GroupMatcher.match_function(groups_for_functions, metadata)
%ExDoc.FunctionNode{
id: "c:#{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, callback_data.line),
type: kind,
annotations: annotations,
group: group
}
end
## Typespecs
defp get_types(module_data, source) do
{:docs_v1, _, _, _, _, _, docs} = module_data.docs
for {{:type, _, _}, _, _, content, _} = doc <- docs, content != :hidden do
get_type(doc, source, module_data)
end
end
defp get_type(type_entry, source, module_data) do
{:docs_v1, _, _, content_type, _, _, _} = module_data.docs
{{_, name, arity}, anno, _signature, doc, metadata} = type_entry
doc_line = anno_line(anno)
annotations = annotations_from_metadata(metadata)
type_data = module_data.language.type_data(type_entry, module_data)
signature = signature(type_data.signature)
annotations = if type_data.type == :opaque, do: ["opaque" | annotations], else: annotations
doc_ast = doc_ast(content_type, doc, file: source.path)
%ExDoc.TypeNode{
id: "t:#{name}/#{arity}",
name: name,
arity: arity,
type: type_data.type,
spec: type_data.spec,
deprecated: metadata[:deprecated],
doc: doc_ast,
doc_line: doc_line,
signature: signature,
source_path: source.path,
source_url: source_link(source, type_data.line),
annotations: annotations
}
end
## General helpers
defp signature([]), do: nil
defp signature(list) when is_list(list), do: Enum.join(list, " ")
defp annotations_from_metadata(metadata) do
annotations = []
annotations =
if since = metadata[:since] do
["since #{since}" | annotations]
else
annotations
end
annotations
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
module.module_info(:compile)[:source]
|> String.Chars.to_string()
|> Path.relative_to(File.cwd!())
end
end