zf

zenflows testing
git clone https://s.sonu.ch/~srfsh/zf.git
Log | Files | Refs | Submodules | README | LICENSE

elixir.ex (25001B)


      1 defmodule ExDoc.Language.Elixir do
      2   @moduledoc false
      3 
      4   @behaviour ExDoc.Language
      5 
      6   alias ExDoc.Autolink
      7   alias ExDoc.Refs
      8   alias ExDoc.Language.Erlang
      9 
     10   @impl true
     11   def module_data(module, docs_chunk, config) do
     12     {type, skip} = module_type_and_skip(module)
     13 
     14     if skip do
     15       :skip
     16     else
     17       title = module_title(module, type)
     18       abst_code = Erlang.get_abstract_code(module)
     19       line = Erlang.find_module_line(module, abst_code)
     20       optional_callbacks = type == :behaviour && module.behaviour_info(:optional_callbacks)
     21 
     22       %{
     23         module: module,
     24         docs: docs_chunk,
     25         language: __MODULE__,
     26         id: inspect(module),
     27         title: title,
     28         type: type,
     29         line: line,
     30         callback_types: [:callback, :macrocallback],
     31         nesting_info: nesting_info(title, config.nest_modules_by_prefix),
     32         private: %{
     33           abst_code: abst_code,
     34           specs: Erlang.get_specs(module),
     35           callbacks: Erlang.get_callbacks(module),
     36           impls: get_impls(module),
     37           optional_callbacks: optional_callbacks
     38         }
     39       }
     40     end
     41   end
     42 
     43   @impl true
     44   def function_data(entry, module_data) do
     45     {{kind, name, arity}, _anno, _signature, doc_content, metadata} = entry
     46 
     47     if doc?(entry, module_data.type) do
     48       function_data(kind, name, arity, doc_content, metadata, module_data)
     49     else
     50       :skip
     51     end
     52   end
     53 
     54   def function_data(kind, name, arity, _doc_content, metadata, module_data) do
     55     extra_annotations =
     56       case {kind, name, arity} do
     57         {:macro, _, _} -> ["macro"]
     58         {_, :__struct__, 0} -> ["struct"]
     59         _ -> []
     60       end
     61 
     62     actual_def = actual_def(name, arity, kind)
     63 
     64     %{
     65       doc_fallback: fn ->
     66         impl = Map.fetch(module_data.private.impls, actual_def)
     67 
     68         callback_doc_ast(name, arity, impl) ||
     69           delegate_doc_ast(metadata[:delegate_to])
     70       end,
     71       extra_annotations: extra_annotations,
     72       line: find_function_line(module_data, actual_def),
     73       specs: specs(kind, name, actual_def, module_data)
     74     }
     75   end
     76 
     77   # We are only interested in functions and macros for now
     78   defp doc?({{kind, _, _}, _, _, _, _}, _) when kind not in [:function, :macro] do
     79     false
     80   end
     81 
     82   # Skip impl_for and impl_for! for protocols
     83   defp doc?({{_, name, _}, _, _, _, _}, :protocol) when name in [:impl_for, :impl_for!] do
     84     false
     85   end
     86 
     87   # If content is a map, then it is ok.
     88   defp doc?({_, _, _, %{}, _}, _) do
     89     true
     90   end
     91 
     92   # If it is none, then we need to look at underscore.
     93   # TODO: We can remove this on Elixir v1.13 as all underscored are hidden.
     94   defp doc?({{_, name, _}, _, _, :none, _}, _type) do
     95     hd(Atom.to_charlist(name)) != ?_
     96   end
     97 
     98   # Everything else is hidden.
     99   defp doc?({_, _, _, _, _}, _) do
    100     false
    101   end
    102 
    103   @impl true
    104   def callback_data(entry, module_data) do
    105     {{kind, name, arity}, anno, _signature, _doc, _metadata} = entry
    106     actual_def = actual_def(name, arity, kind)
    107 
    108     extra_annotations =
    109       if actual_def in module_data.private.optional_callbacks, do: ["optional"], else: []
    110 
    111     specs =
    112       case module_data.private.callbacks do
    113         %{^actual_def => specs} when kind == :macrocallback ->
    114           Enum.map(specs, &remove_callback_term/1)
    115 
    116         %{^actual_def => specs} ->
    117           specs
    118 
    119         %{} ->
    120           []
    121       end
    122 
    123     line =
    124       if specs != [] do
    125         {:type, anno, _, _} = hd(specs)
    126         anno_line(anno)
    127       else
    128         anno_line(anno)
    129       end
    130 
    131     quoted = Enum.map(specs, &Code.Typespec.spec_to_quoted(name, &1))
    132     signature = [get_typespec_signature(hd(quoted), arity)]
    133 
    134     %{
    135       line: line,
    136       signature: signature,
    137       specs: quoted,
    138       extra_annotations: extra_annotations
    139     }
    140   end
    141 
    142   defp remove_callback_term({:type, num, :bounded_fun, [lhs, rhs]}) do
    143     {:type, num, :bounded_fun, [remove_callback_term(lhs), rhs]}
    144   end
    145 
    146   defp remove_callback_term({:type, num, :fun, [{:type, num, :product, [_ | rest_args]} | rest]}) do
    147     {:type, num, :fun, [{:type, num, :product, rest_args} | rest]}
    148   end
    149 
    150   @impl true
    151   def type_data(entry, module_data) do
    152     {{_kind, name, arity}, _anno, _signature, _doc, _metadata} = entry
    153 
    154     %{type: type, spec: spec, line: line} = type_from_module_data(module_data, name, arity)
    155     quoted = spec |> Code.Typespec.type_to_quoted() |> process_type_ast(type)
    156     signature = [get_typespec_signature(quoted, arity)]
    157 
    158     %{
    159       type: type,
    160       line: line,
    161       spec: quoted,
    162       signature: signature
    163     }
    164   end
    165 
    166   @doc false
    167   def type_from_module_data(module_data, name, arity) do
    168     Enum.find_value(module_data.private.abst_code, fn
    169       {:attribute, anno, type, {^name, _, args} = spec} ->
    170         if type in [:opaque, :type] and length(args) == arity do
    171           %{
    172             type: type,
    173             spec: spec,
    174             line: anno_line(anno)
    175           }
    176         end
    177 
    178       _ ->
    179         nil
    180     end)
    181   end
    182 
    183   @impl true
    184   def autolink_doc(ast, opts) do
    185     config = struct!(Autolink, opts)
    186     walk_doc(ast, config)
    187   end
    188 
    189   @impl true
    190   def autolink_spec(ast, opts) do
    191     config = struct!(Autolink, opts)
    192 
    193     # TODO: re-use ExDoc.Language.Erlang.autolink_spec/2
    194 
    195     string =
    196       ast
    197       |> Macro.to_string()
    198       |> safe_format_string!()
    199       |> ExDoc.Utils.h()
    200 
    201     name = typespec_name(ast)
    202     {name, rest} = split_name(string, name)
    203 
    204     name <> do_typespec(rest, config)
    205   end
    206 
    207   @impl true
    208   def highlight_info() do
    209     %{
    210       language_name: "elixir",
    211       lexer: Makeup.Lexers.ElixirLexer,
    212       opts: []
    213     }
    214   end
    215 
    216   @impl true
    217   def format_spec_attribute(%ExDoc.TypeNode{type: type}), do: "@#{type}"
    218   def format_spec_attribute(%ExDoc.FunctionNode{type: :callback}), do: "@callback"
    219   def format_spec_attribute(%ExDoc.FunctionNode{type: :macrocallback}), do: "@macrocallback"
    220   def format_spec_attribute(%ExDoc.FunctionNode{}), do: "@spec"
    221 
    222   ## Module Helpers
    223 
    224   defp nesting_info(title, prefixes) do
    225     prefixes
    226     |> Enum.find(&String.starts_with?(title, &1 <> "."))
    227     |> case do
    228       nil -> nil
    229       prefix -> {"." <> String.trim_leading(title, prefix <> "."), prefix}
    230     end
    231   end
    232 
    233   defp module_type_and_skip(module) do
    234     cond do
    235       function_exported?(module, :__struct__, 0) and
    236           match?(%{__exception__: true}, module.__struct__) ->
    237         {:exception, false}
    238 
    239       function_exported?(module, :__protocol__, 1) ->
    240         {:protocol, false}
    241 
    242       function_exported?(module, :__impl__, 1) ->
    243         {:impl, true}
    244 
    245       match?("Elixir.Mix.Tasks." <> _, Atom.to_string(module)) ->
    246         {:task, false}
    247 
    248       function_exported?(module, :behaviour_info, 1) ->
    249         {:behaviour, false}
    250 
    251       true ->
    252         {:module, false}
    253     end
    254   end
    255 
    256   defp module_title(module, :task), do: "mix " <> task_name(module)
    257   defp module_title(module, _), do: inspect(module)
    258 
    259   defp task_name(module) do
    260     "Elixir.Mix.Tasks." <> name = Atom.to_string(module)
    261     name |> String.split(".") |> Enum.map_join(".", &Macro.underscore/1)
    262   end
    263 
    264   def get_impls(module) do
    265     for behaviour <- behaviours_implemented_by(module),
    266         {callback, _} <- Erlang.get_callbacks(behaviour),
    267         do: {callback, behaviour},
    268         into: %{}
    269   end
    270 
    271   defp behaviours_implemented_by(module) do
    272     for {:behaviour, list} <- module.module_info(:attributes),
    273         behaviour <- list,
    274         do: behaviour
    275   end
    276 
    277   ## Helpers
    278 
    279   defp specs(kind, name, actual_def, module_data) do
    280     specs =
    281       module_data.private.specs
    282       |> Map.get(actual_def, [])
    283       |> Enum.map(&Code.Typespec.spec_to_quoted(name, &1))
    284 
    285     if kind == :macro do
    286       Enum.map(specs, &remove_first_macro_arg/1)
    287     else
    288       specs
    289     end
    290   end
    291 
    292   defp actual_def(name, arity, :macrocallback) do
    293     {String.to_atom("MACRO-" <> to_string(name)), arity + 1}
    294   end
    295 
    296   defp actual_def(name, arity, :macro) do
    297     {String.to_atom("MACRO-" <> to_string(name)), arity + 1}
    298   end
    299 
    300   defp actual_def(name, arity, _), do: {name, arity}
    301 
    302   defp remove_first_macro_arg({:"::", info, [{name, info2, [_term_arg | rest_args]}, return]}) do
    303     {:"::", info, [{name, info2, rest_args}, return]}
    304   end
    305 
    306   defp remove_first_macro_arg({:when, meta, [lhs, rhs]}) do
    307     {:when, meta, [remove_first_macro_arg(lhs), rhs]}
    308   end
    309 
    310   defp delegate_doc_ast({m, f, a}) do
    311     [
    312       {:p, [], ["See ", {:code, [class: "inline"], [Exception.format_mfa(m, f, a)], %{}}, "."],
    313        %{}}
    314     ]
    315   end
    316 
    317   defp delegate_doc_ast(nil) do
    318     nil
    319   end
    320 
    321   defp callback_doc_ast(name, arity, {:ok, behaviour}) do
    322     [
    323       {:p, [],
    324        [
    325          "Callback implementation for ",
    326          {:code, [class: "inline"], ["c:#{inspect(behaviour)}.#{name}/#{arity}"], %{}},
    327          "."
    328        ], %{}}
    329     ]
    330   end
    331 
    332   defp callback_doc_ast(_, _, _) do
    333     nil
    334   end
    335 
    336   defp find_function_line(module_data, {name, arity}) do
    337     Enum.find_value(module_data.private.abst_code, fn
    338       {:function, anno, ^name, ^arity, _} -> anno_line(anno)
    339       _ -> nil
    340     end)
    341   end
    342 
    343   defp anno_line(line) when is_integer(line), do: abs(line)
    344   defp anno_line(anno), do: anno |> :erl_anno.line() |> abs()
    345 
    346   defp get_typespec_signature({:when, _, [{:"::", _, [{name, meta, args}, _]}, _]}, arity) do
    347     Macro.to_string({name, meta, strip_types(args, arity)})
    348   end
    349 
    350   defp get_typespec_signature({:"::", _, [{name, meta, args}, _]}, arity) do
    351     Macro.to_string({name, meta, strip_types(args, arity)})
    352   end
    353 
    354   defp get_typespec_signature({name, meta, args}, arity) do
    355     Macro.to_string({name, meta, strip_types(args, arity)})
    356   end
    357 
    358   defp strip_types(args, arity) do
    359     args
    360     |> Enum.take(-arity)
    361     |> Enum.with_index(1)
    362     |> Enum.map(fn
    363       {{:"::", _, [left, _]}, position} -> to_var(left, position)
    364       {{:|, _, _}, position} -> to_var({}, position)
    365       {left, position} -> to_var(left, position)
    366     end)
    367     |> Macro.prewalk(fn node -> Macro.update_meta(node, &Keyword.delete(&1, :line)) end)
    368   end
    369 
    370   defp to_var({:%, meta, [name, _]}, _), do: {:%, meta, [name, {:%{}, meta, []}]}
    371   defp to_var({:%{}, _, _}, _), do: {:map, [], nil}
    372   defp to_var({name, meta, _}, _) when is_atom(name), do: {name, meta, nil}
    373 
    374   defp to_var({{:., meta, [_module, name]}, _, _args}, _) when is_atom(name),
    375     do: {name, meta, nil}
    376 
    377   defp to_var([{:->, _, _} | _], _), do: {:function, [], nil}
    378   defp to_var({:<<>>, _, _}, _), do: {:binary, [], nil}
    379   defp to_var({:{}, _, _}, _), do: {:tuple, [], nil}
    380   defp to_var({_, _}, _), do: {:tuple, [], nil}
    381   defp to_var(integer, _) when is_integer(integer), do: {:integer, [], nil}
    382   defp to_var(float, _) when is_integer(float), do: {:float, [], nil}
    383   defp to_var(list, _) when is_list(list), do: {:list, [], nil}
    384   defp to_var(atom, _) when is_atom(atom), do: {:atom, [], nil}
    385   defp to_var(_, position), do: {:"arg#{position}", [], nil}
    386 
    387   # Cut off the body of an opaque type while leaving it on a normal type.
    388   defp process_type_ast({:"::", _, [d | _]}, :opaque), do: d
    389   defp process_type_ast(ast, _), do: ast
    390 
    391   ## Autolinking
    392 
    393   @autoimported_modules [Kernel, Kernel.SpecialForms]
    394 
    395   defp walk_doc(list, config) when is_list(list) do
    396     Enum.map(list, &walk_doc(&1, config))
    397   end
    398 
    399   defp walk_doc(binary, _) when is_binary(binary) do
    400     binary
    401   end
    402 
    403   defp walk_doc({:pre, _, _, _} = ast, _config) do
    404     ast
    405   end
    406 
    407   defp walk_doc({:a, attrs, inner, meta} = ast, config) do
    408     case custom_link(attrs, config) do
    409       :remove_link ->
    410         remove_link(ast)
    411 
    412       nil ->
    413         ast
    414 
    415       url ->
    416         {:a, Keyword.put(attrs, :href, url), inner, meta}
    417     end
    418   end
    419 
    420   defp walk_doc({:code, attrs, [code], meta} = ast, config) do
    421     if url = url(code, :regular_link, config) do
    422       code = remove_prefix(code)
    423       {:a, [href: url], [{:code, attrs, [code], meta}], %{}}
    424     else
    425       ast
    426     end
    427   end
    428 
    429   defp walk_doc({tag, attrs, ast, meta}, config) do
    430     {tag, attrs, walk_doc(ast, config), meta}
    431   end
    432 
    433   defp remove_link({:a, _attrs, inner, _meta}),
    434     do: inner
    435 
    436   @ref_regex ~r/^`(.+)`$/
    437 
    438   defp custom_link(attrs, config) do
    439     case Keyword.fetch(attrs, :href) do
    440       {:ok, href} ->
    441         case Regex.scan(@ref_regex, href) do
    442           [[_, custom_link]] -> url(custom_link, :custom_link, config)
    443           [] -> build_extra_link(href, config)
    444         end
    445 
    446       _ ->
    447         nil
    448     end
    449   end
    450 
    451   defp build_extra_link(link, config) do
    452     with %{scheme: nil, host: nil, path: path} = uri <- URI.parse(link),
    453          true <- is_binary(path) and path != "" and not (path =~ @ref_regex),
    454          true <- Path.extname(path) in [".livemd", ".md", ".txt", ""] do
    455       if file = config.extras[Path.basename(path)] do
    456         fragment = (uri.fragment && "#" <> uri.fragment) || ""
    457         file <> config.ext <> fragment
    458       else
    459         Autolink.maybe_warn(nil, config, nil, %{file_path: path, original_text: link})
    460         nil
    461       end
    462     else
    463       _ -> nil
    464     end
    465   end
    466 
    467   @basic_types [
    468     any: 0,
    469     none: 0,
    470     atom: 0,
    471     map: 0,
    472     pid: 0,
    473     port: 0,
    474     reference: 0,
    475     struct: 0,
    476     tuple: 0,
    477     float: 0,
    478     integer: 0,
    479     neg_integer: 0,
    480     non_neg_integer: 0,
    481     pos_integer: 0,
    482     list: 1,
    483     nonempty_list: 1,
    484     maybe_improper_list: 2,
    485     nonempty_improper_list: 2,
    486     nonempty_maybe_improper_list: 2
    487   ]
    488 
    489   @built_in_types [
    490     term: 0,
    491     arity: 0,
    492     as_boolean: 1,
    493     binary: 0,
    494     bitstring: 0,
    495     boolean: 0,
    496     byte: 0,
    497     char: 0,
    498     charlist: 0,
    499     nonempty_charlist: 0,
    500     fun: 0,
    501     function: 0,
    502     identifier: 0,
    503     iodata: 0,
    504     iolist: 0,
    505     keyword: 0,
    506     keyword: 1,
    507     list: 0,
    508     nonempty_list: 0,
    509     maybe_improper_list: 0,
    510     nonempty_maybe_improper_list: 0,
    511     mfa: 0,
    512     module: 0,
    513     no_return: 0,
    514     node: 0,
    515     number: 0,
    516     struct: 0,
    517     timeout: 0
    518   ]
    519 
    520   defp url(string = "mix help " <> name, mode, config), do: mix_task(name, string, mode, config)
    521   defp url(string = "mix " <> name, mode, config), do: mix_task(name, string, mode, config)
    522 
    523   defp url(string, mode, config) do
    524     case Regex.run(~r{^(.+)/(\d+)$}, string) do
    525       [_, left, right] ->
    526         with {:ok, arity} <- parse_arity(right) do
    527           {kind, rest} = kind(left)
    528 
    529           case parse_module_function(rest) do
    530             {:local, function} ->
    531               local_url(kind, function, arity, config, string, mode: mode)
    532 
    533             {:remote, module, function} ->
    534               remote_url({kind, module, function, arity}, config, string, mode: mode)
    535 
    536             :error ->
    537               nil
    538           end
    539         else
    540           _ ->
    541             nil
    542         end
    543 
    544       nil ->
    545         case parse_module(string, mode) do
    546           {:module, module} ->
    547             module_url(module, mode, config, string)
    548 
    549           :error ->
    550             nil
    551         end
    552 
    553       _ ->
    554         nil
    555     end
    556   end
    557 
    558   defp kind("c:" <> rest), do: {:callback, rest}
    559   defp kind("t:" <> rest), do: {:type, rest}
    560   defp kind(rest), do: {:function, rest}
    561 
    562   defp remove_prefix("c:" <> rest), do: rest
    563   defp remove_prefix("t:" <> rest), do: rest
    564   defp remove_prefix(rest), do: rest
    565 
    566   defp parse_arity(string) do
    567     case Integer.parse(string) do
    568       {arity, ""} -> {:ok, arity}
    569       _ -> :error
    570     end
    571   end
    572 
    573   defp parse_module_function(string) do
    574     case string |> String.split(".") |> Enum.reverse() do
    575       [string] ->
    576         with {:function, function} <- parse_function(string) do
    577           {:local, function}
    578         end
    579 
    580       ["", "", ""] ->
    581         {:local, :..}
    582 
    583       ["//", "", ""] ->
    584         {:local, :"..//"}
    585 
    586       ["", ""] ->
    587         {:local, :.}
    588 
    589       ["", "", "" | rest] ->
    590         module_string = rest |> Enum.reverse() |> Enum.join(".")
    591 
    592         with {:module, module} <- parse_module(module_string, :custom_link) do
    593           {:remote, module, :..}
    594         end
    595 
    596       ["", "" | rest] ->
    597         module_string = rest |> Enum.reverse() |> Enum.join(".")
    598 
    599         with {:module, module} <- parse_module(module_string, :custom_link) do
    600           {:remote, module, :.}
    601         end
    602 
    603       [function_string | rest] ->
    604         module_string = rest |> Enum.reverse() |> Enum.join(".")
    605 
    606         with {:module, module} <- parse_module(module_string, :custom_link),
    607              {:function, function} <- parse_function(function_string) do
    608           {:remote, module, function}
    609         end
    610     end
    611   end
    612 
    613   defp parse_module(<<first>> <> _ = string, _mode) when first in ?A..?Z do
    614     do_parse_module(string)
    615   end
    616 
    617   defp parse_module(<<?:>> <> _ = string, :custom_link) do
    618     do_parse_module(string)
    619   end
    620 
    621   defp parse_module(_, _) do
    622     :error
    623   end
    624 
    625   defp do_parse_module(string) do
    626     case Code.string_to_quoted(string, warn_on_unnecessary_quotes: false) do
    627       {:ok, module} when is_atom(module) ->
    628         {:module, module}
    629 
    630       {:ok, {:__aliases__, _, parts}} ->
    631         if Enum.all?(parts, &is_atom/1) do
    632           {:module, Module.concat(parts)}
    633         else
    634           :error
    635         end
    636 
    637       _ ->
    638         :error
    639     end
    640   end
    641 
    642   # There are special forms that are forbidden by the tokenizer
    643   defp parse_function("__aliases__"), do: {:function, :__aliases__}
    644   defp parse_function("__block__"), do: {:function, :__block__}
    645   defp parse_function("%"), do: {:function, :%}
    646 
    647   defp parse_function(string) do
    648     case Code.string_to_quoted("& #{string}/0") do
    649       {:ok, {:&, _, [{:/, _, [{function, _, _}, 0]}]}} when is_atom(function) ->
    650         {:function, function}
    651 
    652       _ ->
    653         :error
    654     end
    655   end
    656 
    657   defp mix_task(name, string, mode, config) do
    658     {module, url, visibility} =
    659       if name =~ ~r/^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)*$/ do
    660         parts = name |> String.split(".") |> Enum.map(&Macro.camelize/1)
    661         module = Module.concat([Mix, Tasks | parts])
    662 
    663         {module, module_url(module, :regular_link, config, string),
    664          Refs.get_visibility({:module, module})}
    665       else
    666         {nil, nil, :undefined}
    667       end
    668 
    669     if url in [nil, :remove_link] and mode == :custom_link do
    670       Autolink.maybe_warn({:module, module}, config, visibility, %{
    671         mix_task: true,
    672         original_text: string
    673       })
    674     end
    675 
    676     url
    677   end
    678 
    679   defp safe_format_string!(string) do
    680     try do
    681       string
    682       |> Code.format_string!(line_length: 80)
    683       |> IO.iodata_to_binary()
    684     rescue
    685       _ -> string
    686     end
    687   end
    688 
    689   defp typespec_name({:"::", _, [{name, _, _}, _]}), do: Atom.to_string(name)
    690   defp typespec_name({:when, _, [left, _]}), do: typespec_name(left)
    691   defp typespec_name({name, _, _}) when is_atom(name), do: Atom.to_string(name)
    692 
    693   # extract out function name so we don't process it. This is to avoid linking it when there's
    694   # a type with the same name
    695   defp split_name(string, name) do
    696     if String.starts_with?(string, name) do
    697       {name, binary_part(string, byte_size(name), byte_size(string) - byte_size(name))}
    698     else
    699       {"", string}
    700     end
    701   end
    702 
    703   defp do_typespec(string, config) do
    704     regex = ~r{
    705         (                                             # <call_string>
    706           (?:
    707             (                                         # <module_string>
    708               (?:
    709                 \:[a-z][_a-zA-Z0-9]*                  # Erlang module
    710               )|
    711               (?:
    712                 [A-Z][_a-zA-Z0-9]*                    # Elixir module
    713                 (?:\.[A-Z][_a-zA-Z0-9]*)*             # Elixir submodule
    714               )
    715             )                                         # </module_string>
    716             \.                                        # Dot operator
    717           )?
    718           ([a-z_][_a-zA-Z0-9]*[\?\!]?)                # Name <name_string />
    719         )                                             # </call_string>
    720         (\(.*\))                                      # Arguments <rest />
    721       }x
    722 
    723     Regex.replace(regex, string, fn _all, call_string, module_string, name_string, rest ->
    724       module = string_to_module(module_string)
    725       name = String.to_atom(name_string)
    726       arity = count_args(rest, 0, 0)
    727       original_text = call_string <> "()"
    728 
    729       url =
    730         if module do
    731           remote_url({:type, module, name, arity}, config, original_text)
    732         else
    733           local_url(:type, name, arity, config, original_text)
    734         end
    735 
    736       if url do
    737         ~s[<a href="#{url}">#{ExDoc.Utils.h(call_string)}</a>]
    738       else
    739         call_string
    740       end <> do_typespec(rest, config)
    741     end)
    742   end
    743 
    744   defp string_to_module(""), do: nil
    745 
    746   defp string_to_module(string) do
    747     if String.starts_with?(string, ":") do
    748       string |> String.trim_leading(":") |> String.to_atom()
    749     else
    750       Module.concat([string])
    751     end
    752   end
    753 
    754   defp count_args("()" <> _, 0, 0), do: 0
    755   defp count_args("(" <> rest, counter, acc), do: count_args(rest, counter + 1, acc)
    756   defp count_args("[" <> rest, counter, acc), do: count_args(rest, counter + 1, acc)
    757   defp count_args("{" <> rest, counter, acc), do: count_args(rest, counter + 1, acc)
    758   defp count_args(")" <> _, 1, acc), do: acc + 1
    759   defp count_args(")" <> rest, counter, acc), do: count_args(rest, counter - 1, acc)
    760   defp count_args("]" <> rest, counter, acc), do: count_args(rest, counter - 1, acc)
    761   defp count_args("}" <> rest, counter, acc), do: count_args(rest, counter - 1, acc)
    762   defp count_args("," <> rest, 1, acc), do: count_args(rest, 1, acc + 1)
    763   defp count_args(<<_>> <> rest, counter, acc), do: count_args(rest, counter, acc)
    764   defp count_args("", _counter, acc), do: acc
    765 
    766   ## Internals
    767 
    768   defp module_url(module, mode, config, string) do
    769     ref = {:module, module}
    770 
    771     case {mode, Refs.get_visibility(ref)} do
    772       {_link_type, visibility} when visibility in [:public, :limited] ->
    773         Autolink.app_module_url(Autolink.tool(module, config), module, config)
    774 
    775       {:regular_link, :undefined} ->
    776         nil
    777 
    778       {:custom_link, visibility} when visibility in [:hidden, :undefined] ->
    779         Autolink.maybe_warn(ref, config, visibility, %{original_text: string})
    780         :remove_link
    781 
    782       {_link_type, visibility} ->
    783         Autolink.maybe_warn(ref, config, visibility, %{original_text: string})
    784         nil
    785     end
    786   end
    787 
    788   defp local_url(kind, name, arity, config, original_text, options \\ [])
    789 
    790   defp local_url(:type, name, arity, config, _original_text, _options)
    791        when {name, arity} in @basic_types do
    792     Autolink.ex_doc_app_url(Kernel, config, "typespecs", config.ext, "#basic-types")
    793   end
    794 
    795   defp local_url(:type, name, arity, config, _original_text, _options)
    796        when {name, arity} in @built_in_types do
    797     Autolink.ex_doc_app_url(Kernel, config, "typespecs", config.ext, "#built-in-types")
    798   end
    799 
    800   defp local_url(kind, name, arity, config, original_text, options) do
    801     module = config.current_module
    802     ref = {kind, module, name, arity}
    803     mode = Keyword.get(options, :mode, :regular_link)
    804     visibility = Refs.get_visibility(ref)
    805 
    806     case {kind, visibility} do
    807       {_kind, :public} ->
    808         fragment(Autolink.tool(module, config), kind, name, arity)
    809 
    810       {:function, _visibility} ->
    811         try_autoimported_function(name, arity, mode, config, original_text)
    812 
    813       {:type, :hidden} ->
    814         nil
    815 
    816       {:type, _} ->
    817         nil
    818 
    819       _ ->
    820         Autolink.maybe_warn(ref, config, visibility, %{original_text: original_text})
    821         nil
    822     end
    823   end
    824 
    825   defp try_autoimported_function(name, arity, mode, config, original_text) do
    826     Enum.find_value(@autoimported_modules, fn module ->
    827       remote_url({:function, module, name, arity}, config, original_text,
    828         warn?: false,
    829         mode: mode
    830       )
    831     end)
    832   end
    833 
    834   defp remote_url({kind, module, name, arity} = ref, config, original_text, opts \\ []) do
    835     warn? = Keyword.get(opts, :warn?, true)
    836     mode = Keyword.get(opts, :mode, :regular_link)
    837     same_module? = module == config.current_module
    838 
    839     case {mode, Refs.get_visibility({:module, module}), Refs.get_visibility(ref)} do
    840       {_mode, _module_visibility, :public} ->
    841         tool = Autolink.tool(module, config)
    842 
    843         if same_module? do
    844           fragment(tool, kind, name, arity)
    845         else
    846           Autolink.app_module_url(tool, module, config) <> fragment(tool, kind, name, arity)
    847         end
    848 
    849       {:regular_link, module_visibility, :undefined}
    850       when module_visibility == :public
    851       when module_visibility == :limited and kind != :type ->
    852         if warn?,
    853           do: Autolink.maybe_warn(ref, config, :undefined, %{original_text: original_text})
    854 
    855         nil
    856 
    857       {:regular_link, _module_visibility, :undefined} when not same_module? ->
    858         nil
    859 
    860       {_mode, _module_visibility, visibility} ->
    861         if warn?,
    862           do: Autolink.maybe_warn(ref, config, visibility, %{original_text: original_text})
    863 
    864         nil
    865     end
    866   end
    867 
    868   defp prefix(kind)
    869   defp prefix(:function), do: ""
    870   defp prefix(:callback), do: "c:"
    871   defp prefix(:type), do: "t:"
    872 
    873   defp fragment(:ex_doc, kind, name, arity) do
    874     "#" <> prefix(kind) <> "#{URI.encode(Atom.to_string(name))}/#{arity}"
    875   end
    876 
    877   defp fragment(_, kind, name, arity) do
    878     case kind do
    879       :function -> "##{name}-#{arity}"
    880       :callback -> "#Module:#{name}-#{arity}"
    881       :type -> "#type-#{name}"
    882     end
    883   end
    884 end