Packages
ex_doc
0.39.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/autolink.ex
defmodule ExDoc.Autolink do
# Encapsulates all functionality related to autolinking.
@moduledoc false
# * `:apps` - the apps 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.
#
# * `:current_kfa` - the kind, function, arity that the docs are being generated for. Is nil
# if there is no such thing. Used to generate more accurate warnings.
#
# * `:module_id` - id of the module being documented (e.g.: `"String"`)
#
# * `:file` - source file location
#
# * `:line` - line number of the beginning of the documentation
#
# * `:language` - the language call-back module to use
#
# * `:id` - a module/function/etc being documented (e.g.: `"String.upcase/2"`)
#
# * `:ext` - the extension (`".html"`, "`.xhtml"`, etc)
#
# * `:extras` - map of extras
#
# * `:skip_undefined_reference_warnings_on` - function that will be called with
# a module/function/file/etc and return a boolean whether to skip warning on it.
#
# * `:skip_code_autolink_to` - function that will be called with a term and return a boolean
# whether to skip autolinking to it.
#
# * `:filtered_modules` - A list of module nodes that were filtered by the retriever
#
# * `:warnings` - one of:
#
# * `:emit` (default)
#
# * `:raise` (useful for tests)
#
# * `:send` - send back to caller (useful for tests)
alias ExDoc.Refs
defstruct [
:current_module,
:module_id,
:id,
:line,
:language,
file: "nofile",
apps: [],
extras: [],
deps: [],
ext: ".html",
current_kfa: nil,
siblings: [],
skip_undefined_reference_warnings_on: &ExDoc.Config.skip_undefined_reference_warnings_on/1,
skip_code_autolink_to: &ExDoc.Config.skip_code_autolink_to/1,
force_module_prefix: nil,
filtered_modules: [],
warnings: :emit
]
@hexdocs "https://hexdocs.pm/"
@otpappdocs "https://www.erlang.org/doc/apps/"
def app_module_url(tool, module, anchor \\ "#content", config)
def app_module_url(:no_tool, _, _, _), do: nil
def app_module_url(tool, module, anchor, config) do
base_url =
case tool do
:ex_doc -> @hexdocs
:otp -> @otpappdocs
end
path = module |> inspect() |> String.trim_leading(":")
app_url(base_url, module, config, path, config.ext, "#{anchor}")
end
defp string_app_module_url(tool, module, anchor, config) do
if Enum.any?(config.filtered_modules, &(&1.module == module)) do
warn(config, "reference to a filtered module")
nil
else
app_module_url(tool, module, anchor, config)
end
end
@doc false
def ex_doc_app_url(module, config, path, ext, suffix) do
app_url(@hexdocs, module, config, path, ext, suffix)
end
defp app_url(base_url, module, config, path, ext, suffix) do
if app = app(module) do
if app in config.apps do
path <> ext <> suffix
else
config.deps
|> Keyword.get_lazy(app, fn -> base_url <> "#{app}" end)
|> String.trim_trailing("/")
|> Kernel.<>("/" <> path <> ".html" <> suffix)
end
else
path <> ext <> suffix
end
end
defp app(module) do
case :code.which(module) do
:preloaded ->
:erts
maybe_path ->
case :application.get_application(module) do
{:ok, app} ->
app
_ ->
with true <- is_list(maybe_path),
[_, "ebin", app, "lib" | _] <- maybe_path |> Path.split() |> Enum.reverse() do
String.split(app, "-") |> hd() |> String.to_atom()
else
_ -> nil
end
end
end
end
@doc false
def tool(module, config) do
if match?("Elixir." <> _, Atom.to_string(module)) do
:ex_doc
else
app = app(module)
if is_app_otp(app) and app not in config.apps and not Keyword.has_key?(config.deps, app) do
:otp
else
:ex_doc
end
end
end
defp is_app_otp(app) do
maybe_lib_dir_path = :code.lib_dir(app)
is_list(maybe_lib_dir_path) and List.starts_with?(maybe_lib_dir_path, :code.root_dir())
end
def maybe_warn(config, ref, visibility, metadata) do
file = Path.relative_to_cwd(config.file)
unless Enum.any?(
[config.id, config.module_id, file],
config.skip_undefined_reference_warnings_on
) do
warn(config, ref, visibility, metadata)
end
end
defp ref_regex, do: ~r/^`(.+)`$/
def custom_link(attrs, config) do
case Keyword.fetch(attrs, :href) do
{:ok, href} ->
case Regex.scan(ref_regex(), href) do
[[_, custom_link]] ->
custom_link
|> url(:custom_link, config)
|> remove_and_warn_if_invalid(custom_link, config)
[] ->
build_extra_link(href, config)
end
_ ->
nil
end
end
def url(string = "mix help " <> name, mode, config) do
name |> mix_task(string, mode, config) |> maybe_remove_link(mode)
end
def url(string = "mix " <> name, mode, config) do
name |> mix_task(string, mode, config) |> maybe_remove_link(mode)
end
def url(string, mode, config) do
if config.skip_code_autolink_to.(string) do
nil
else
parse_url(string, mode, config)
end
end
defp remove_and_warn_if_invalid(nil, reference, config) do
warn(
config,
~s[documentation references "#{reference}" but it is invalid]
)
:remove_link
end
defp remove_and_warn_if_invalid(result, _, _), do: result
@builtin_ext [".livemd", ".cheatmd", ".md", ".txt", ""]
defp build_extra_link(link, config) do
with %{scheme: nil, host: nil, path: path} = uri <- URI.parse(link),
true <- is_binary(path) and path != "" and not (path =~ ref_regex()),
true <- Path.extname(path) in @builtin_ext do
if file = config.extras[Path.basename(path)] do
append_fragment(file <> config.ext, uri.fragment)
else
maybe_warn(config, nil, nil, %{file_path: path, original_text: link})
nil
end
else
_ -> nil
end
end
defp maybe_remove_link(nil, :custom_link) do
:remove_link
end
defp maybe_remove_link(result, _mode) do
result
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_link, config, string),
Refs.get_visibility({:module, module})}
else
{nil, nil, :undefined}
end
if url in [nil, :remove_link] and mode == :custom_link do
maybe_warn(config, {:module, module}, visibility, %{
mix_task: true,
original_text: string
})
end
url
end
defp module_url(module, anchor \\ nil, mode, config, string) do
ref = {:module, module}
case {mode, Refs.get_visibility(ref)} do
{_link_type, visibility} when visibility in [:public, :limited] ->
string_app_module_url(tool(module, config), module, anchor, config)
{:regular_link, :undefined} ->
nil
{:custom_link, visibility} when visibility in [:hidden, :undefined] ->
maybe_warn(config, ref, visibility, %{original_text: string})
:remove_link
{_link_type, visibility} ->
maybe_warn(config, ref, visibility, %{original_text: string})
nil
end
end
defp extra_url(string, config) do
case String.split(string, ":", parts: 2) do
[app, extra] ->
{extra, anchor} =
case String.split(extra, "#", parts: 2) do
[extra] ->
{extra, ""}
[extra, anchor] ->
{extra, "#" <> anchor}
end
app = String.to_atom(app)
config.deps
|> Keyword.get_lazy(app, fn ->
if Application.ensure_loaded(app) != :ok do
maybe_warn(
config,
"documentation references \"e:#{string}\" but #{app} cannot be found.",
nil,
%{}
)
end
prefix =
cond do
app in config.apps -> ""
is_app_otp(app) -> @otpappdocs
true -> @hexdocs
end
prefix <> "#{app}"
end)
|> String.trim_trailing("/")
|> Kernel.<>("/" <> convert_extra_extension(extra, config) <> anchor)
_ ->
nil
end
end
defp convert_extra_extension(extra, config) do
if Path.extname(extra) in @builtin_ext do
Path.rootname(extra) <> config.ext
else
extra
end
end
defp parse_url(string, mode, config) do
case Regex.run(~r{^(.+)/(\d+)(#.*)?$}, string) do
[_, left, right | maybe_fragment] ->
with {:ok, arity} <- parse_arity(right) do
{kind, rest} = kind(left)
case config.language.parse_module_function(rest) do
{:local, function} ->
kind
|> local_url(function, arity, config, string, mode: mode)
|> maybe_append_nested_fragment(maybe_fragment)
|> maybe_remove_link(mode)
{:remote, module, function} ->
{kind, module, function, arity}
|> remote_url(config, string, mode: mode)
|> maybe_append_nested_fragment(maybe_fragment)
|> maybe_remove_link(mode)
:error ->
nil
end
else
_ ->
nil
end
nil ->
case string do
"m:" <> rest ->
parse_module_with_anchor(rest, config)
"e:" <> rest ->
extra_url(rest, config)
string when mode == :custom_link ->
parse_module_with_anchor(string, config)
string when not config.force_module_prefix ->
case config.language.parse_module(string, mode) do
{:module, module} ->
module_url(module, mode, config, string)
:error ->
nil
end
_ ->
nil
end
_ ->
nil
end
end
defp parse_module_with_anchor(string, config) do
destructure [rest, fragment], String.split(string, "#", parts: 2)
# TODO: rename :custom_link to :strict i.e. we expect ref to be valid
# force custom_link mode because of m: prefix.
case config.language.parse_module(rest, :custom_link) do
{:module, module} ->
module_url(module, fragment && "#" <> fragment, :custom_link, config, rest)
:error ->
nil
end
end
defp parse_arity(string) do
case Integer.parse(string) do
{arity, ""} -> {:ok, arity}
_ -> :error
end
end
def kind("c:" <> rest), do: {:callback, rest}
def kind("t:" <> rest), do: {:type, rest}
## \\ does not work for :custom_url as Earmark strips the \...
def kind("\\" <> rest), do: {:function, rest}
def kind(rest), do: {:function, rest}
def local_url(kind, name, arity, config, original_text, options \\ [])
def local_url(kind, name, arity, config, original_text, options) do
module = config.current_module
ref = {kind, module, name, arity}
mode = Keyword.get(options, :mode, :regular_link)
visibility = Refs.get_visibility(ref)
case {kind, visibility} do
{_kind, :public} ->
fragment(kind, name, arity)
{:function, _visibility} ->
case config.language.try_autoimported_function(name, arity, mode, config, original_text) do
nil ->
if mode == :custom_link do
maybe_warn(config, ref, visibility, %{original_text: original_text})
end
nil
url ->
url
end
{:type, _visibility} ->
case config.language.try_builtin_type(name, arity, mode, config, original_text) do
nil ->
if mode == :custom_link or config.language == ExDoc.Language.Erlang do
maybe_warn(config, ref, visibility, %{original_text: original_text})
end
nil
url ->
url
end
_ ->
maybe_warn(config, ref, visibility, %{original_text: original_text})
nil
end
end
def fragment(kind, nil, arity) do
fragment(kind, "nil", arity)
end
def fragment(kind, name, arity) do
"#" <> prefix(kind) <> "#{encode_fragment_name(name)}/#{arity}"
end
defp encode_fragment_name(name) when is_atom(name) do
encode_fragment_name(Atom.to_string(name))
end
defp encode_fragment_name(name) when is_binary(name) do
URI.encode(name)
end
defp prefix(kind)
defp prefix(:function), do: ""
defp prefix(:callback), do: "c:"
defp prefix(:type), do: "t:"
def remote_url({kind, module, name, arity} = ref, config, original_text, opts \\ []) do
warn? = Keyword.get(opts, :warn?, true)
mode = Keyword.get(opts, :mode, :regular_link)
same_module? = module == config.current_module
case {mode, Refs.get_visibility({:module, module}), Refs.get_visibility(ref)} do
{_mode, _module_visibility, :public} ->
tool = tool(module, config)
if same_module? do
fragment(kind, name, arity)
else
url = string_app_module_url(tool, module, nil, config)
url && url <> fragment(kind, name, arity)
end
{:regular_link, module_visibility, :undefined}
when module_visibility == :public
when module_visibility == :limited and kind != :type ->
if warn? do
maybe_warn(config, ref, :undefined, %{original_text: original_text})
end
nil
{:regular_link, _module_visibility, :undefined}
when not same_module? and
(config.language != ExDoc.Language.Erlang or kind == :function) ->
nil
{:regular_link, :hidden, :hidden}
when not same_module? ->
if warn? do
maybe_warn(config, ref, :hidden, %{
original_text: original_text,
module_visibility: :hidden,
same_module?: false
})
end
nil
{_mode, _module_visibility, visibility} ->
if warn? do
maybe_warn(config, ref, visibility, %{original_text: original_text})
end
nil
end
end
@doc false
def warn(config, message) do
f =
case config.current_kfa do
{:function, f, a} ->
[function: {f, a}]
_ ->
[]
end
stacktrace_info = [file: config.file, line: config.line, module: config.current_module] ++ f
case config.warnings do
:emit ->
ExDoc.Utils.warn(message, stacktrace_info)
:raise ->
ExDoc.Utils.warn(message, stacktrace_info)
raise "fail due to warnings"
:send ->
send(self(), {:warn, message, file: config.file, line: config.line})
end
end
defp warn(config, ref, visibility, metadata)
defp warn(
config,
{:module, _module},
visibility,
%{mix_task: true, original_text: original_text}
) do
message =
"documentation references \"#{original_text}\" but it is " <>
format_visibility(visibility, :module)
warn(config, message)
end
defp warn(
config,
{:module, _module},
visibility,
%{original_text: original_text}
) do
message =
"documentation references module \"#{original_text}\" but it is " <>
format_visibility(visibility, :module)
warn(config, message)
end
defp warn(
config,
nil,
_visibility,
%{file_path: _file_path, original_text: original_text}
) do
message = "documentation references file \"#{original_text}\" but it does not exist"
warn(config, message)
end
defp warn(
config,
{:type, module, _name, _arity},
:hidden,
%{original_text: original_text, module_visibility: :hidden, same_module?: false}
) do
message =
"documentation references type \"#{original_text}\" but the module " <>
"#{inspect(module)} is #{format_visibility(:hidden, :module)}"
warn(config, message)
end
defp warn(
config,
{kind, _module, _name, _arity},
visibility,
%{original_text: original_text}
) do
message =
"documentation references #{kind} \"#{original_text}\" but it is " <>
format_visibility(visibility, kind)
warn(config, message)
end
defp warn(config, message, _, _) when is_binary(message) do
warn(config, message)
end
# there is not such a thing as private callback or private module
def 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.
def format_visibility(:hidden, :type), do: "hidden or private"
# for the rest, it can either be undefined or private
def format_visibility(:undefined, _kind), do: "undefined or private"
def format_visibility(visibility, _kind), do: "#{visibility}"
defp maybe_append_nested_fragment(nil, _), do: nil
defp maybe_append_nested_fragment(url, []), do: url
defp maybe_append_nested_fragment(url, ["#" <> fragment]), do: url <> "-" <> fragment
defp append_fragment(url, nil), do: url
defp append_fragment(url, fragment), do: url <> "#" <> fragment
end