Packages
ex_doc
0.22.5
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/autolink.ex
defmodule ExDoc.Autolink do
@moduledoc false
# * `:app` - the app that the docs are being generated for. When linking modules they are
# checked if they are part of the app and based on that the links are relative or absolute.
#
# * `:current_module` - the module that the docs are being generated for. Used to link local
# calls and see if remote calls are in the same app.
#
# * `:module_id` - id of the module being documented (e.g.: `"String"`)
#
# * `:file` - source file location
#
# * `:line` - line number of the beginning of the documentation
#
# * `:id` - a module/function/etc being documented (e.g.: `"String.upcase/2"`)
#
# * `:ext` - the extension (`".html"`, "`.xhtml"`, etc)
#
# * `:siblings` - applications in the same umbrella project as `:app`. When linking modules,
# links to these applications are relative.
#
# * `:extras` - list of extras
#
# * `:skip_undefined_reference_warnings_on` - list of modules to skip the warning on
@enforce_keys [:app, :file]
defstruct [
:app,
:current_module,
:module_id,
:id,
:file,
:line,
extras: [],
ext: ".html",
siblings: [],
skip_undefined_reference_warnings_on: []
]
alias ExDoc.Formatter.HTML
alias ExDoc.Formatter.HTML.Templates, as: T
alias ExDoc.Refs
@hexdocs "https://hexdocs.pm/"
@otpdocs "http://www.erlang.org/doc/man/"
@autoimported_modules [Kernel, Kernel.SpecialForms]
def doc(ast, options \\ []) do
config = struct!(__MODULE__, options)
walk(ast, config)
end
defp walk(list, config) when is_list(list) do
Enum.map(list, &walk(&1, config))
end
defp walk(binary, _) when is_binary(binary) do
binary
end
defp walk({:pre, _, _} = ast, _config) do
ast
end
defp walk({:a, attrs, inner} = ast, config) do
cond do
url = custom_link(attrs, config) ->
{:a, Keyword.put(attrs, :href, url), inner}
url = extra_link(attrs, config) ->
{:a, Keyword.put(attrs, :href, url), inner}
true ->
ast
end
end
defp walk({:code, attrs, [code]} = ast, config) do
if url = url(code, :regular, config) do
code = remove_prefix(code)
{:a, [href: url], [{:code, attrs, [code]}]}
else
ast
end
end
defp walk({tag, attrs, ast}, config) do
{tag, attrs, walk(ast, config)}
end
@ref_regex ~r/^`(.+)`$/
defp custom_link(attrs, config) do
with {:ok, href} <- Keyword.fetch(attrs, :href),
[[_, text]] <- Regex.scan(@ref_regex, href) do
url(text, :custom_link, config)
else
_ -> nil
end
end
defp extra_link(attrs, config) do
with {:ok, href} <- Keyword.fetch(attrs, :href),
uri <- URI.parse(href),
nil <- uri.host,
true <- is_binary(uri.path),
false <- uri.path =~ @ref_regex,
extension when extension in [".md", ".txt", ""] <- Path.extname(uri.path) do
file = Path.basename(uri.path)
if file in config.extras do
without_ext = trim_extension(file, extension)
fragment = (uri.fragment && "#" <> uri.fragment) || ""
HTML.text_to_id(without_ext) <> config.ext <> fragment
else
maybe_warn(nil, config, nil, %{file_path: uri.path, original_text: href})
nil
end
else
_ -> nil
end
end
defp trim_extension(file, ""),
do: file
defp trim_extension(file, extension),
do: String.trim_trailing(file, extension)
@basic_types [
any: 0,
none: 0,
atom: 0,
map: 0,
pid: 0,
port: 0,
reference: 0,
struct: 0,
tuple: 0,
float: 0,
integer: 0,
neg_integer: 0,
non_neg_integer: 0,
pos_integer: 0,
list: 1,
nonempty_list: 1,
maybe_improper_list: 2,
nonempty_improper_list: 2,
nonempty_maybe_improper_list: 2
]
@built_in_types [
term: 0,
arity: 0,
as_boolean: 1,
binary: 0,
bitstring: 0,
boolean: 0,
byte: 0,
char: 0,
charlist: 0,
nonempty_charlist: 0,
fun: 0,
function: 0,
identifier: 0,
iodata: 0,
iolist: 0,
keyword: 0,
keyword: 1,
list: 0,
nonempty_list: 0,
maybe_improper_list: 0,
nonempty_maybe_improper_list: 0,
mfa: 0,
module: 0,
no_return: 0,
node: 0,
number: 0,
struct: 0,
timeout: 0
]
defp url(string = "mix help " <> name, mode, config), do: mix_task(name, string, mode, config)
defp url(string = "mix " <> name, mode, config), do: mix_task(name, string, mode, config)
defp url(string, mode, config) 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, function} ->
local_url(kind, function, arity, config, string)
{:remote, module, function} ->
remote_url(kind, module, function, arity, config, string)
:error ->
nil
end
else
_ ->
nil
end
nil ->
case parse_module(string, mode) do
{:module, module} ->
module_url(module, mode, config, string)
:error ->
nil
end
_ ->
nil
end
end
defp kind("c:" <> rest), do: {:callback, rest}
defp kind("t:" <> rest), do: {:type, rest}
defp kind(rest), do: {:function, rest}
defp remove_prefix("c:" <> rest), do: rest
defp remove_prefix("t:" <> rest), do: rest
defp remove_prefix(rest), do: rest
defp parse_arity(string) do
case Integer.parse(string) do
{arity, ""} -> {:ok, arity}
_ -> :error
end
end
defp parse_module_function(string) do
case string |> String.split(".") |> Enum.reverse() do
[string] ->
with {:function, function} <- parse_function(string) do
{:local, function}
end
["", "", ""] ->
{:local, :..}
["", ""] ->
{:local, :.}
["", "", "" | rest] ->
module_string = rest |> Enum.reverse() |> Enum.join(".")
with {:module, module} <- parse_module(module_string, :custom_link) do
{:remote, module, :..}
end
["", "" | rest] ->
module_string = rest |> Enum.reverse() |> Enum.join(".")
with {:module, module} <- parse_module(module_string, :custom_link) do
{:remote, module, :.}
end
[function_string | rest] ->
module_string = rest |> Enum.reverse() |> Enum.join(".")
with {:module, module} <- parse_module(module_string, :custom_link),
{:function, function} <- parse_function(function_string) do
{:remote, module, function}
end
end
end
defp parse_module(<<first>> <> _ = string, _mode) when first in ?A..?Z do
do_parse_module(string)
end
defp parse_module(<<?:>> <> _ = string, :custom_link) do
do_parse_module(string)
end
defp parse_module(_, _) do
:error
end
defp do_parse_module(string) do
case Code.string_to_quoted(string, warn_on_unnecessary_quotes: false) do
{:ok, module} when is_atom(module) -> {:module, module}
{:ok, {:__aliases__, _, parts}} -> {:module, Module.concat(parts)}
_ -> :error
end
end
defp parse_function(string) do
case Code.string_to_quoted(":" <> string) do
{:ok, function} when is_atom(function) -> {:function, function}
_ -> :error
end
end
defp mix_task(name, string, mode, config) do
{module, url, visibility} =
if name =~ ~r/^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)*$/ do
parts = name |> String.split(".") |> Enum.map(&Macro.camelize/1)
module = Module.concat([Mix, Tasks | parts])
{module, module_url(module, :regular, config, string),
Refs.get_visibility({:module, module})}
else
{nil, nil, :undefined}
end
if is_nil(url) and mode == :custom_link do
maybe_warn({:module, module}, config, visibility, %{mix_task: true, original_text: string})
end
url
end
@doc """
Converts given types/specs `ast` into HTML with links.
"""
def typespec(ast, options) do
config = struct!(__MODULE__, options)
string =
ast
|> Macro.to_string()
|> Code.format_string!(line_length: 80)
|> IO.iodata_to_binary()
|> T.h()
name = typespec_name(ast)
{name, rest} = split_name(string, name)
name <> do_typespec(rest, config)
end
defp typespec_name({:"::", _, [{name, _, _}, _]}), do: Atom.to_string(name)
defp typespec_name({:when, _, [left, _]}), do: typespec_name(left)
defp typespec_name({name, _, _}) when is_atom(name), do: Atom.to_string(name)
# extract out function name so we don't process it. This is to avoid linking it when there's
# a type with the same name
defp split_name(string, name) do
if String.starts_with?(string, name) do
{name, binary_part(string, byte_size(name), byte_size(string) - byte_size(name))}
else
{"", string}
end
end
defp do_typespec(string, config) do
regex = ~r{
( # <call_string>
(?:
( # <module_string>
(?:
\:[a-z][_a-zA-Z0-9]* # Erlang module
)|
(?:
[A-Z][_a-zA-Z0-9]* # Elixir module
(?:\.[A-Z][_a-zA-Z0-9]*)* # Elixir submodule
)
) # </module_string>
\. # Dot operator
)?
([a-z_][_a-zA-Z0-9]*[\?\!]?) # Name <name_string />
) # </call_string>
(\(.*\)) # Arguments <rest />
}x
Regex.replace(regex, string, fn _all, call_string, module_string, name_string, rest ->
module = string_to_module(module_string)
name = String.to_atom(name_string)
arity = count_args(rest, 0, 0)
url =
if module do
remote_url(:type, module, name, arity, config, string)
else
local_url(:type, name, arity, config, string)
end
if url do
~s[<a href="#{url}">#{T.h(call_string)}</a>]
else
call_string
end <> do_typespec(rest, config)
end)
end
defp string_to_module(""), do: nil
defp string_to_module(string) do
if String.starts_with?(string, ":") do
string |> String.trim_leading(":") |> String.to_atom()
else
Module.concat([string])
end
end
defp count_args("()" <> _, 0, 0), do: 0
defp count_args("(" <> rest, counter, acc), do: count_args(rest, counter + 1, acc)
defp count_args("[" <> rest, counter, acc), do: count_args(rest, counter + 1, acc)
defp count_args("{" <> rest, counter, acc), do: count_args(rest, counter + 1, acc)
defp count_args(")" <> _, 1, acc), do: acc + 1
defp count_args(")" <> rest, counter, acc), do: count_args(rest, counter - 1, acc)
defp count_args("]" <> rest, counter, acc), do: count_args(rest, counter - 1, acc)
defp count_args("}" <> rest, counter, acc), do: count_args(rest, counter - 1, acc)
defp count_args("," <> rest, 1, acc), do: count_args(rest, 1, acc + 1)
defp count_args(<<_>> <> rest, counter, acc), do: count_args(rest, counter, acc)
defp count_args("", _counter, acc), do: acc
## Internals
defp module_url(module, mode, config, string) do
if module == config.current_module do
"#content"
else
if Refs.public?({:module, module}) do
app_module_url(tool(module), module, config)
else
if mode == :custom_link do
ref = {:module, module}
maybe_warn(ref, config, Refs.get_visibility(ref), %{original_text: string})
end
nil
end
end
end
defp app_module_url(:ex_doc, module, config) do
ex_doc_app_url(module, config) <> inspect(module) <> config.ext
end
defp app_module_url(:otp, module, _config) do
@otpdocs <> "#{module}.html"
end
defp local_url(:type, name, arity, config, _original_text) when {name, arity} in @basic_types do
ex_doc_app_url(Kernel, config) <> "typespecs" <> config.ext <> "#basic-types"
end
defp local_url(:type, name, arity, config, _original_text)
when {name, arity} in @built_in_types do
ex_doc_app_url(Kernel, config) <> "typespecs" <> config.ext <> "#built-in-types"
end
defp local_url(kind, name, arity, config, original_text) do
module = config.current_module
ref = {kind, module, name, arity}
cond do
Refs.public?(ref) -> fragment(tool(module), kind, name, arity)
kind == :function -> try_autoimported_function(name, arity, config, original_text)
true -> nil
end
end
defp try_autoimported_function(name, arity, config, original_text) do
Enum.find_value(@autoimported_modules, fn module ->
remote_url(:function, module, name, arity, config, original_text, warn?: false)
end)
end
defp remote_url(kind, module, name, arity, config, original_text, opts \\ []) do
warn? = Keyword.get(opts, :warn?, true)
ref = {kind, module, name, arity}
if Refs.public?(ref) do
case tool(module) do
:no_tool ->
nil
tool ->
if module == config.current_module do
fragment(tool, kind, name, arity)
else
app_module_url(tool, module, config) <> fragment(tool, kind, name, arity)
end
end
else
if warn? and Refs.public?({:module, module}) do
maybe_warn(ref, config, Refs.get_visibility(ref), %{original_text: original_text})
end
nil
end
end
defp ex_doc_app_url(module, config) do
app = config.app
case :application.get_application(module) do
{:ok, ^app} -> ""
{:ok, app} -> if app in config.siblings, do: "", else: @hexdocs <> "#{app}/"
_ -> ""
end
end
defp fragment(:ex_doc, kind, name, arity) do
prefix =
case kind do
:function -> ""
:callback -> "c:"
:type -> "t:"
end
"#" <> prefix <> "#{T.enc(Atom.to_string(name))}/#{arity}"
end
defp fragment(:otp, kind, name, arity) do
case kind do
:function -> "##{name}-#{arity}"
:callback -> "#Module:#{name}-#{arity}"
:type -> "#type-#{name}"
end
end
defp tool(module) do
name = Atom.to_string(module)
if name == String.downcase(name) do
case :code.which(module) do
:preloaded ->
:otp
:non_existing ->
:no_tool
path ->
if String.starts_with?(List.to_string(path), List.to_string(:code.lib_dir())) do
:otp
else
:no_tool
end
end
else
:ex_doc
end
end
defp maybe_warn(ref, config, visibility, metadata) do
skipped = config.skip_undefined_reference_warnings_on
file = Path.relative_to(config.file, File.cwd!())
line = config.line
unless Enum.any?([config.id, config.module_id, file], &(&1 in skipped)) do
warn(ref, {file, line}, config.id, visibility, metadata)
end
end
defp warn(message, {file, line}, id) do
warning = IO.ANSI.format([:yellow, "warning: ", :reset])
stacktrace =
" #{file}" <>
if(line, do: ":#{line}", else: "") <>
if(id, do: ": #{id}", else: "")
IO.puts(:stderr, [warning, message, ?\n, stacktrace, ?\n])
end
defp warn(ref, file_line, id, visibility, metadata)
defp warn(
{:module, _module},
{file, line},
id,
visibility,
%{mix_task: true, original_text: original_text}
) do
message =
"documentation references \"#{original_text}\" but it is " <>
format_visibility(visibility, :module)
warn(message, {file, line}, id)
end
defp warn(
{:module, _module},
{file, line},
id,
visibility,
%{original_text: original_text}
) do
message =
"documentation references module \"#{original_text}\" but it is " <>
format_visibility(visibility, :module)
warn(message, {file, line}, id)
end
defp warn(
nil,
{file, line},
id,
_visibility,
%{file_path: _file_path, original_text: original_text}
) do
message = "documentation references file \"#{original_text}\" but it does not exist"
warn(message, {file, line}, id)
end
defp warn(
{kind, _module, _name, _arity},
{file, line},
id,
visibility,
%{original_text: original_text}
) do
message =
"documentation references \"#{original_text}\" but it is " <>
format_visibility(visibility, kind)
warn(message, {file, line}, id)
end
# there is not such a thing as private callback or private module
defp format_visibility(visibility, kind) when kind in [:module, :callback], do: "#{visibility}"
# typep is defined as :hidden, since there is no :private visibility value
# but type defined with @doc false also is the stored the same way.
defp format_visibility(:hidden, :type), do: "hidden or private"
# for the rest, it can either be undefined or private
defp format_visibility(:undefined, _kind), do: "undefined or private"
defp format_visibility(visibility, _kind), do: "#{visibility}"
end