zf

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

erlang.ex (15909B)


      1 defmodule ExDoc.Language.Erlang do
      2   @moduledoc false
      3 
      4   @behaviour ExDoc.Language
      5 
      6   alias ExDoc.{Autolink, Refs}
      7 
      8   @impl true
      9   def module_data(module, docs_chunk, _config) do
     10     # Make sure the module is loaded for future checks
     11     _ = Code.ensure_loaded(module)
     12     id = Atom.to_string(module)
     13     abst_code = get_abstract_code(module)
     14     line = find_module_line(module, abst_code)
     15     type = module_type(module)
     16     optional_callbacks = type == :behaviour && module.behaviour_info(:optional_callbacks)
     17 
     18     %{
     19       module: module,
     20       docs: docs_chunk,
     21       language: __MODULE__,
     22       id: id,
     23       title: id,
     24       type: type,
     25       line: line,
     26       callback_types: [:callback],
     27       nesting_info: nil,
     28       private: %{
     29         abst_code: abst_code,
     30         specs: get_specs(module),
     31         callbacks: get_callbacks(module),
     32         optional_callbacks: optional_callbacks
     33       }
     34     }
     35   end
     36 
     37   @impl true
     38   def function_data(entry, module_data) do
     39     {{kind, name, arity}, _anno, _signature, doc_content, _metadata} = entry
     40 
     41     # TODO: Edoc on Erlang/OTP24.1+ includes private functions in
     42     # the chunk, so we manually yank them out for now.
     43     if kind == :function and doc_content != :hidden and
     44          function_exported?(module_data.module, name, arity) do
     45       function_data(name, arity, doc_content, module_data)
     46     else
     47       :skip
     48     end
     49   end
     50 
     51   defp function_data(name, arity, _doc_content, module_data) do
     52     specs =
     53       case Map.fetch(module_data.private.specs, {name, arity}) do
     54         {:ok, specs} ->
     55           [{:attribute, 0, :spec, {{name, arity}, specs}}]
     56 
     57         :error ->
     58           []
     59       end
     60 
     61     %{
     62       doc_fallback: fn -> nil end,
     63       extra_annotations: [],
     64       line: nil,
     65       specs: specs
     66     }
     67   end
     68 
     69   @impl true
     70   def callback_data(entry, module_data) do
     71     {{_kind, name, arity}, anno, signature, _doc, _metadata} = entry
     72 
     73     extra_annotations =
     74       if {name, arity} in module_data.private.optional_callbacks, do: ["optional"], else: []
     75 
     76     specs =
     77       case Map.fetch(module_data.private.callbacks, {name, arity}) do
     78         {:ok, specs} ->
     79           [{:attribute, 0, :callback, {{name, arity}, specs}}]
     80 
     81         :error ->
     82           []
     83       end
     84 
     85     %{
     86       line: anno_line(anno),
     87       signature: signature,
     88       specs: specs,
     89       extra_annotations: extra_annotations
     90     }
     91   end
     92 
     93   @impl true
     94   def type_data(entry, module_data) do
     95     {{kind, name, arity}, anno, signature, _doc, _metadata} = entry
     96 
     97     case ExDoc.Language.Elixir.type_from_module_data(module_data, name, arity) do
     98       %{} = map ->
     99         %{
    100           type: map.type,
    101           line: map.line,
    102           spec: {:attribute, 0, map.type, map.spec},
    103           signature: signature
    104         }
    105 
    106       nil ->
    107         %{
    108           type: kind,
    109           line: anno_line(anno),
    110           spec: nil,
    111           signature: signature
    112         }
    113     end
    114   end
    115 
    116   @impl true
    117   def autolink_doc(ast, opts) do
    118     config = struct!(Autolink, opts)
    119     walk_doc(ast, config)
    120   end
    121 
    122   @impl true
    123   def autolink_spec(nil, _opts) do
    124     nil
    125   end
    126 
    127   def autolink_spec({:attribute, _, :opaque, ast}, _opts) do
    128     {name, _, args} = ast
    129 
    130     args =
    131       for arg <- args do
    132         {:var, _, name} = arg
    133         Atom.to_string(name)
    134       end
    135       |> Enum.intersperse(", ")
    136 
    137     IO.iodata_to_binary([Atom.to_string(name), "(", args, ")"])
    138   end
    139 
    140   def autolink_spec(ast, opts) do
    141     config = struct!(Autolink, opts)
    142 
    143     {name, quoted} =
    144       case ast do
    145         {:attribute, _, kind, {{name, _arity}, ast}} when kind in [:spec, :callback] ->
    146           {name, Enum.map(ast, &Code.Typespec.spec_to_quoted(name, &1))}
    147 
    148         {:attribute, _, :type, ast} ->
    149           {name, _, _} = ast
    150           {name, Code.Typespec.type_to_quoted(ast)}
    151       end
    152 
    153     formatted = format_spec(ast)
    154     autolink_spec(quoted, name, formatted, config)
    155   end
    156 
    157   @impl true
    158   def highlight_info() do
    159     %{
    160       language_name: "erlang",
    161       lexer: Makeup.Lexers.ErlangLexer,
    162       opts: []
    163     }
    164   end
    165 
    166   @impl true
    167   def format_spec_attribute(%ExDoc.TypeNode{type: type}), do: "-#{type}"
    168   def format_spec_attribute(%ExDoc.FunctionNode{type: :callback}), do: "-callback"
    169   def format_spec_attribute(%ExDoc.FunctionNode{}), do: "-spec"
    170 
    171   ## Shared between Erlang & Elixir
    172 
    173   @doc false
    174   def get_abstract_code(module) do
    175     case :code.get_object_code(module) do
    176       {^module, binary, _file} ->
    177         case :beam_lib.chunks(binary, [:abstract_code]) do
    178           {:ok, {_, [{:abstract_code, {_vsn, abstract_code}}]}} -> abstract_code
    179           _otherwise -> []
    180         end
    181 
    182       :error ->
    183         []
    184     end
    185   end
    186 
    187   @doc false
    188   def find_module_line(module, abst_code) do
    189     Enum.find_value(abst_code, fn
    190       {:attribute, anno, :module, ^module} -> anno_line(anno)
    191       _ -> nil
    192     end)
    193   end
    194 
    195   # Returns a map of {name, arity} => spec.
    196   def get_specs(module) do
    197     case Code.Typespec.fetch_specs(module) do
    198       {:ok, specs} -> Map.new(specs)
    199       :error -> %{}
    200     end
    201   end
    202 
    203   def get_callbacks(module) do
    204     case Code.Typespec.fetch_callbacks(module) do
    205       {:ok, callbacks} -> Map.new(callbacks)
    206       :error -> %{}
    207     end
    208   end
    209 
    210   ## Autolink
    211 
    212   defp walk_doc(list, config) when is_list(list) do
    213     Enum.map(list, &walk_doc(&1, config))
    214   end
    215 
    216   defp walk_doc(binary, _) when is_binary(binary) do
    217     binary
    218   end
    219 
    220   defp walk_doc({:a, attrs, inner, _meta} = ast, config) do
    221     case attrs[:rel] do
    222       "https://erlang.org/doc/link/seeerl" ->
    223         {fragment, url} = extract_fragment(attrs[:href] || "")
    224 
    225         case String.split(url, ":") do
    226           [module] ->
    227             autolink(:module, module, fragment, inner, config)
    228 
    229           [app, module] ->
    230             inner = strip_app(inner, app)
    231             autolink(:module, module, fragment, inner, config)
    232 
    233           _ ->
    234             warn_ref(attrs[:href], config)
    235             inner
    236         end
    237 
    238       "https://erlang.org/doc/link/seemfa" ->
    239         {kind, url} =
    240           case String.split(attrs[:href], "Module:") do
    241             [url] -> {:function, url}
    242             [left, right] -> {:callback, left <> right}
    243           end
    244 
    245         case String.split(url, ":") do
    246           [mfa] ->
    247             autolink(kind, mfa, "", inner, config)
    248 
    249           [app, mfa] ->
    250             inner = strip_app(inner, app)
    251             autolink(kind, mfa, "", inner, config)
    252         end
    253 
    254       "https://erlang.org/doc/link/seetype" ->
    255         case String.split(attrs[:href], ":") do
    256           [type] ->
    257             autolink(:type, type, "", inner, config)
    258 
    259           [app, type] ->
    260             inner = strip_app(inner, app)
    261             autolink(:type, type, "", inner, config)
    262         end
    263 
    264       "https://erlang.org/doc/link/" <> see ->
    265         warn_ref(attrs[:href] <> " (#{see})", config)
    266         inner
    267 
    268       _ ->
    269         ast
    270     end
    271   end
    272 
    273   defp walk_doc({tag, attrs, ast, meta}, config) do
    274     {tag, attrs, walk_doc(ast, config), meta}
    275   end
    276 
    277   defp extract_fragment(url) do
    278     case String.split(url, "#", parts: 2) do
    279       [url] -> {"", url}
    280       [url, fragment] -> {"#" <> fragment, url}
    281     end
    282   end
    283 
    284   defp strip_app([{:code, attrs, [code], meta}], app) do
    285     [{:code, attrs, strip_app(code, app), meta}]
    286   end
    287 
    288   defp strip_app(code, app) when is_binary(code) do
    289     String.trim_leading(code, "//#{app}/")
    290   end
    291 
    292   defp strip_app(other, _app) do
    293     other
    294   end
    295 
    296   defp warn_ref(href, config) do
    297     message = "invalid reference: #{href}"
    298     Autolink.maybe_warn(message, config, nil, %{})
    299   end
    300 
    301   defp autolink(kind, string, fragment, inner, config) do
    302     if url = url(kind, string, config) do
    303       {:a, [href: url <> fragment], inner, %{}}
    304     else
    305       inner
    306     end
    307   end
    308 
    309   defp url(:module, string, config) do
    310     ref = {:module, String.to_atom(string)}
    311     do_url(ref, string, config)
    312   end
    313 
    314   defp url(kind, string, config) do
    315     [module, name, arity] =
    316       case String.split(string, ["#", "/"]) do
    317         [module, name, arity] ->
    318           [module, name, arity]
    319 
    320         # this is what docgen_xml_to_chunk returns
    321         [module, name] when kind == :type ->
    322           # TODO: don't assume 0-arity, instead find first {:type, module, name, arity} ref
    323           # and use that arity.
    324           [module, name, "0"]
    325       end
    326 
    327     name = String.to_atom(name)
    328     arity = String.to_integer(arity)
    329 
    330     original_text =
    331       if kind == :type and arity == 0 do
    332         "#{name}()"
    333       else
    334         "#{name}/#{arity}"
    335       end
    336 
    337     if module == "" do
    338       ref = {kind, config.current_module, name, arity}
    339       visibility = Refs.get_visibility(ref)
    340 
    341       if visibility == :public do
    342         final_url({kind, name, arity}, config)
    343       else
    344         Autolink.maybe_warn(ref, config, visibility, %{original_text: original_text})
    345         nil
    346       end
    347     else
    348       ref = {kind, String.to_atom(module), name, arity}
    349       original_text = "#{module}:#{original_text}"
    350       do_url(ref, original_text, config)
    351     end
    352   end
    353 
    354   defp do_url(ref, original_text, config) do
    355     visibility = Refs.get_visibility(ref)
    356 
    357     # TODO: type with content = %{} in otp xml is marked as :hidden, it should be :public
    358 
    359     if visibility == :public or (visibility == :hidden and elem(ref, 0) == :type) do
    360       final_url(ref, config)
    361     else
    362       Autolink.maybe_warn(ref, config, visibility, %{original_text: original_text})
    363       nil
    364     end
    365   end
    366 
    367   defp final_url({:module, module}, config) do
    368     tool = Autolink.tool(module, config)
    369     Autolink.app_module_url(tool, module, config)
    370   end
    371 
    372   defp final_url({kind, name, arity}, _config) do
    373     fragment(:ex_doc, kind, name, arity)
    374   end
    375 
    376   defp final_url({kind, module, name, arity}, config) do
    377     tool = Autolink.tool(module, config)
    378     module_url = Autolink.app_module_url(tool, module, config)
    379     # TODO: fix me
    380     module_url = String.trim_trailing(module_url, "#content")
    381     module_url <> fragment(tool, kind, name, arity)
    382   end
    383 
    384   defp fragment(:otp, :function, name, arity) do
    385     "##{name}-#{arity}"
    386   end
    387 
    388   defp fragment(:otp, :callback, name, arity) do
    389     "#Module:#{name}-#{arity}"
    390   end
    391 
    392   defp fragment(:otp, :type, name, _arity) do
    393     "#type-#{name}"
    394   end
    395 
    396   defp fragment(:ex_doc, :function, name, arity) do
    397     "##{name}/#{arity}"
    398   end
    399 
    400   defp fragment(:ex_doc, :callback, name, arity) do
    401     "#c:#{name}/#{arity}"
    402   end
    403 
    404   defp fragment(:ex_doc, :type, name, arity) do
    405     "#t:#{name}/#{arity}"
    406   end
    407 
    408   # Traverses quoted and formatted string of the typespec AST, replacing refs with links.
    409   #
    410   # Let's say we have this typespec:
    411   #
    412   #     -spec f(X) -> #{atom() => bar(), integer() => X}.
    413   #
    414   # We traverse the AST and find types and their string representations:
    415   #
    416   #     -spec f(X) -> #{atom() => bar(), integer() => X}.
    417   #                     ^^^^      ^^^    ^^^^^^^
    418   #
    419   #     atom/0    => atom
    420   #     bar/0     => bar
    421   #     integer/0 => integer
    422   #
    423   # We then traverse the formatted string, *in order*, replacing the type strings with links:
    424   #
    425   #     "atom("    => "atom("
    426   #     "bar("     => "<a>bar</a>("
    427   #     "integer(" => "integer("
    428   #
    429   # Finally we end up with:
    430   #
    431   #     -spec f(X) -> #{atom() => <a>bar</a>(), integer() => X}.
    432   #
    433   # All of this hassle is to preserve the original *text layout* of the initial representation,
    434   # all the spaces, newlines, etc.
    435   defp autolink_spec(quoted, name, formatted, config) do
    436     acc =
    437       for quoted <- List.wrap(quoted) do
    438         {_quoted, acc} =
    439           Macro.prewalk(quoted, [], fn
    440             # module.name(args)
    441             {{:., _, [module, name]}, _, args}, acc ->
    442               {{:t, [], args}, [{pp({module, name}), {module, name, length(args)}} | acc]}
    443 
    444             {name, _, _}, acc when name in [:<<>>, :..] ->
    445               {nil, acc}
    446 
    447             # -1
    448             {:-, _, [int]}, acc when is_integer(int) ->
    449               {nil, acc}
    450 
    451             # fun() (spec_to_quoted expands it to (... -> any())
    452             {:->, _, [[{name, _, _}], {:any, _, _}]}, acc when name == :... ->
    453               {nil, acc}
    454 
    455             # #{x :: t()}
    456             {:field_type, _, [name, type]}, acc when is_atom(name) ->
    457               {type, acc}
    458 
    459             {name, _, args} = ast, acc when is_atom(name) and is_list(args) ->
    460               arity = length(args)
    461 
    462               cond do
    463                 name in [:"::", :when, :%{}, :{}, :|, :->, :record] ->
    464                   {ast, acc}
    465 
    466                 # %{required(...) => ..., optional(...) => ...}
    467                 name in [:required, :optional] and arity == 1 ->
    468                   {ast, acc}
    469 
    470                 # name(args)
    471                 true ->
    472                   {ast, [{pp(name), {name, arity}} | acc]}
    473               end
    474 
    475             other, acc ->
    476               {other, acc}
    477           end)
    478 
    479         acc
    480         |> Enum.reverse()
    481         # drop the name of the typespec
    482         |> Enum.drop(1)
    483       end
    484       |> Enum.concat()
    485 
    486     put(acc)
    487 
    488     # Drop and re-add type name (it, the first element in acc, is dropped there too)
    489     #
    490     #     1. foo() :: bar()
    491     #     2.    () :: bar()
    492     #     3.    () :: <a>bar</a>()
    493     #     4. foo() :: <a>bar</a>()
    494     name = pp(name)
    495     formatted = trim_name(formatted, name)
    496     formatted = replace(formatted, acc, config)
    497     name <> formatted
    498   end
    499 
    500   defp trim_name(string, name) do
    501     name_size = byte_size(name)
    502     binary_part(string, name_size, byte_size(string) - name_size)
    503   end
    504 
    505   defp replace(formatted, [], _config) do
    506     formatted
    507   end
    508 
    509   defp replace(formatted, acc, config) do
    510     String.replace(formatted, Enum.map(acc, &"#{elem(&1, 0)}("), fn string ->
    511       string = String.trim_trailing(string, "(")
    512       {other, ref} = pop()
    513 
    514       if string != other do
    515         Autolink.maybe_warn(
    516           "internal inconsistency, please submit bug: #{inspect(string)} != #{inspect(other)}",
    517           config,
    518           nil,
    519           nil
    520         )
    521       end
    522 
    523       url =
    524         case ref do
    525           {name, arity} ->
    526             visibility = Refs.get_visibility({:type, config.current_module, name, arity})
    527 
    528             if visibility in [:public, :hidden] do
    529               final_url({:type, name, arity}, config)
    530             end
    531 
    532           {module, name, arity} ->
    533             ref = {:type, module, name, arity}
    534             visibility = Refs.get_visibility(ref)
    535 
    536             if visibility in [:public, :hidden] do
    537               final_url(ref, config)
    538             else
    539               original_text = "#{string}/#{arity}"
    540               Autolink.maybe_warn(ref, config, visibility, %{original_text: original_text})
    541               nil
    542             end
    543         end
    544 
    545       if url do
    546         ~s|<a href="#{url}">#{string}</a>(|
    547       else
    548         string <> "("
    549       end
    550     end)
    551   end
    552 
    553   defp put(items) do
    554     Process.put({__MODULE__, :stack}, items)
    555   end
    556 
    557   defp pop() do
    558     [head | tail] = Process.get({__MODULE__, :stack})
    559     put(tail)
    560     head
    561   end
    562 
    563   defp pp(name) when is_atom(name) do
    564     :io_lib.format("~p", [name]) |> IO.iodata_to_binary()
    565   end
    566 
    567   defp pp({module, name}) when is_atom(module) and is_atom(name) do
    568     :io_lib.format("~p:~p", [module, name]) |> IO.iodata_to_binary()
    569   end
    570 
    571   defp format_spec(ast) do
    572     {:attribute, _, type, _} = ast
    573 
    574     # `-type ` => 6
    575     offset = byte_size(Atom.to_string(type)) + 2
    576 
    577     options = [linewidth: 98 + offset]
    578     :erl_pp.attribute(ast, options) |> IO.iodata_to_binary() |> trim_offset(offset)
    579   end
    580 
    581   ## Helpers
    582 
    583   defp module_type(module) do
    584     cond do
    585       function_exported?(module, :behaviour_info, 1) ->
    586         :behaviour
    587 
    588       true ->
    589         :module
    590     end
    591   end
    592 
    593   # `-type t() :: atom()` becomes `t() :: atom().`
    594   defp trim_offset(binary, offset) do
    595     binary
    596     |> String.trim()
    597     |> String.split("\n")
    598     |> Enum.map(fn line ->
    599       binary_part(line, offset, byte_size(line) - offset)
    600     end)
    601     |> Enum.join("\n")
    602   end
    603 
    604   defp anno_line(line) when is_integer(line), do: abs(line)
    605   defp anno_line(anno), do: anno |> :erl_anno.line() |> abs()
    606 end