zf

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

retriever.ex (10719B)


      1 defmodule ExDoc.Retriever do
      2   # Functions to extract documentation information from modules.
      3   @moduledoc false
      4 
      5   defmodule Error do
      6     @moduledoc false
      7     defexception [:message]
      8   end
      9 
     10   alias ExDoc.{DocAST, GroupMatcher, Refs, Utils}
     11   alias ExDoc.Retriever.Error
     12 
     13   @doc """
     14   Extract documentation from all modules in the specified directory or directories.
     15   """
     16   @spec docs_from_dir(Path.t() | [Path.t()], ExDoc.Config.t()) :: [ExDoc.ModuleNode.t()]
     17   def docs_from_dir(dir, config) when is_binary(dir) do
     18     files = Path.wildcard(Path.expand("*.beam", dir))
     19 
     20     files
     21     |> Enum.map(&filename_to_module/1)
     22     |> docs_from_modules(config)
     23   end
     24 
     25   def docs_from_dir(dirs, config) when is_list(dirs) do
     26     Enum.flat_map(dirs, &docs_from_dir(&1, config))
     27     |> sort_modules(config)
     28   end
     29 
     30   @doc """
     31   Extract documentation from all modules in the list `modules`
     32   """
     33   @spec docs_from_modules([atom], ExDoc.Config.t()) :: [ExDoc.ModuleNode.t()]
     34   def docs_from_modules(modules, config) when is_list(modules) do
     35     modules
     36     |> Enum.flat_map(&get_module(&1, config))
     37     |> sort_modules(config)
     38   end
     39 
     40   defp sort_modules(modules, config) when is_list(modules) do
     41     Enum.sort_by(modules, fn module ->
     42       {GroupMatcher.group_index(config.groups_for_modules, module.group), module.nested_context,
     43        module.nested_title, module.id}
     44     end)
     45   end
     46 
     47   defp filename_to_module(name) do
     48     name = Path.basename(name, ".beam")
     49     String.to_atom(name)
     50   end
     51 
     52   defp get_module(module, config) do
     53     with {:docs_v1, _, language, _, _, metadata, _} = docs_chunk <- docs_chunk(module),
     54          true <- config.filter_modules.(module, metadata),
     55          {:ok, language} <- ExDoc.Language.get(language, module),
     56          %{} = module_data <- language.module_data(module, docs_chunk, config) do
     57       [generate_node(module, module_data, config)]
     58     else
     59       _ ->
     60         []
     61     end
     62   end
     63 
     64   defp docs_chunk(module) do
     65     result = Code.fetch_docs(module)
     66     Refs.insert_from_chunk(module, result)
     67 
     68     case result do
     69       {:docs_v1, _, _, _, :hidden, _, _} ->
     70         false
     71 
     72       {:docs_v1, _, _, _, _, _, _} = docs ->
     73         case Code.ensure_loaded(module) do
     74           {:module, _} ->
     75             docs
     76 
     77           {:error, reason} ->
     78             IO.warn("skipping module #{inspect(module)}, reason: #{reason}", [])
     79             false
     80         end
     81 
     82       {:error, :chunk_not_found} ->
     83         false
     84 
     85       {:error, :module_not_found} ->
     86         unless Code.ensure_loaded?(module) do
     87           raise Error, "module #{inspect(module)} is not defined/available"
     88         end
     89 
     90       {:error, _} = error ->
     91         raise Error, "error accessing #{inspect(module)}: #{inspect(error)}"
     92 
     93       _ ->
     94         raise Error,
     95               "unknown format in Docs chunk. This likely means you are running on " <>
     96                 "a more recent Elixir version that is not supported by ExDoc. Please update."
     97     end
     98   end
     99 
    100   defp generate_node(module, module_data, config) do
    101     source_url = config.source_url_pattern
    102     source_path = source_path(module, config)
    103     source = %{url: source_url, path: source_path}
    104     {doc_line, moduledoc, metadata} = get_module_docs(module_data, source_path)
    105 
    106     # TODO: The default function groups must be returned by the language
    107     groups_for_functions =
    108       config.groups_for_functions ++ [Callbacks: & &1[:__callback__], Functions: fn _ -> true end]
    109 
    110     function_groups = Enum.map(groups_for_functions, &elem(&1, 0))
    111     function_docs = get_docs(module_data, source, groups_for_functions)
    112     docs = function_docs ++ get_callbacks(module_data, source, groups_for_functions)
    113     types = get_types(module_data, source)
    114 
    115     metadata = Map.put(metadata, :__type__, module_data.type)
    116     group = GroupMatcher.match_module(config.groups_for_modules, module, module_data.id, metadata)
    117     {nested_title, nested_context} = module_data.nesting_info || {nil, nil}
    118 
    119     %ExDoc.ModuleNode{
    120       id: module_data.id,
    121       title: module_data.title,
    122       nested_title: nested_title,
    123       nested_context: nested_context,
    124       group: group,
    125       module: module,
    126       type: module_data.type,
    127       deprecated: metadata[:deprecated],
    128       function_groups: function_groups,
    129       docs: ExDoc.Utils.natural_sort_by(docs, &"#{&1.name}/#{&1.arity}"),
    130       doc: moduledoc,
    131       doc_line: doc_line,
    132       typespecs: ExDoc.Utils.natural_sort_by(types, &"#{&1.name}/#{&1.arity}"),
    133       source_path: source_path,
    134       source_url: source_link(source, module_data.line),
    135       language: module_data.language,
    136       annotations: List.wrap(metadata[:tags])
    137     }
    138   end
    139 
    140   defp doc_ast(format, %{"en" => doc_content}, options) do
    141     DocAST.parse!(doc_content, format, options)
    142   end
    143 
    144   defp doc_ast(_, _, _options) do
    145     nil
    146   end
    147 
    148   # Module Helpers
    149 
    150   defp get_module_docs(module_data, source_path) do
    151     {:docs_v1, anno, _, content_type, moduledoc, metadata, _} = module_data.docs
    152     doc_line = anno_line(anno)
    153     options = [file: source_path, line: doc_line + 1]
    154     {doc_line, doc_ast(content_type, moduledoc, options), metadata}
    155   end
    156 
    157   ## Function helpers
    158 
    159   defp get_docs(module_data, source, groups_for_functions) do
    160     {:docs_v1, _, _, _, _, _, doc_elements} = module_data.docs
    161 
    162     nodes =
    163       Enum.flat_map(doc_elements, fn doc_element ->
    164         case module_data.language.function_data(doc_element, module_data) do
    165           :skip ->
    166             []
    167 
    168           function_data ->
    169             [get_function(doc_element, function_data, source, module_data, groups_for_functions)]
    170         end
    171       end)
    172 
    173     filter_defaults(nodes)
    174   end
    175 
    176   defp get_function(doc_element, function_data, source, module_data, groups_for_functions) do
    177     {:docs_v1, _, _, content_type, _, _, _} = module_data.docs
    178     {{type, name, arity}, anno, signature, doc_content, metadata} = doc_element
    179     doc_line = anno_line(anno)
    180     annotations = annotations_from_metadata(metadata) ++ function_data.extra_annotations
    181     line = function_data.line || doc_line
    182     defaults = get_defaults(name, arity, Map.get(metadata, :defaults, 0))
    183 
    184     doc_ast =
    185       (doc_content && doc_ast(content_type, doc_content, file: source.path, line: doc_line + 1)) ||
    186         function_data.doc_fallback.()
    187 
    188     group = GroupMatcher.match_function(groups_for_functions, metadata)
    189 
    190     %ExDoc.FunctionNode{
    191       id: "#{name}/#{arity}",
    192       name: name,
    193       arity: arity,
    194       deprecated: metadata[:deprecated],
    195       doc: doc_ast,
    196       doc_line: doc_line,
    197       defaults: ExDoc.Utils.natural_sort_by(defaults, fn {name, arity} -> "#{name}/#{arity}" end),
    198       signature: signature(signature),
    199       specs: function_data.specs,
    200       source_path: source.path,
    201       source_url: source_link(source, line),
    202       type: type,
    203       group: group,
    204       annotations: annotations
    205     }
    206   end
    207 
    208   defp get_defaults(_name, _arity, 0), do: []
    209 
    210   defp get_defaults(name, arity, defaults) do
    211     for default <- (arity - defaults)..(arity - 1), do: {name, default}
    212   end
    213 
    214   defp filter_defaults(nodes) do
    215     Enum.map(nodes, &filter_defaults(&1, nodes))
    216   end
    217 
    218   defp filter_defaults(node, nodes) do
    219     update_in(node.defaults, fn defaults ->
    220       Enum.reject(defaults, fn {name, arity} ->
    221         Enum.any?(nodes, &match?(%{name: ^name, arity: ^arity}, &1))
    222       end)
    223     end)
    224   end
    225 
    226   ## Callback helpers
    227 
    228   defp get_callbacks(%{type: :behaviour} = module_data, source, groups_for_functions) do
    229     {:docs_v1, _, _, _, _, _, docs} = module_data.docs
    230 
    231     for {{kind, _, _}, _, _, _, _} = doc <- docs, kind in module_data.callback_types do
    232       get_callback(doc, source, groups_for_functions, module_data)
    233     end
    234   end
    235 
    236   defp get_callbacks(_, _, _), do: []
    237 
    238   defp get_callback(callback, source, groups_for_functions, module_data) do
    239     callback_data = module_data.language.callback_data(callback, module_data)
    240 
    241     {:docs_v1, _, _, content_type, _, _, _} = module_data.docs
    242     {{kind, name, arity}, anno, _signature, doc, metadata} = callback
    243     doc_line = anno_line(anno)
    244 
    245     signature = signature(callback_data.signature)
    246     specs = callback_data.specs
    247     annotations = callback_data.extra_annotations ++ annotations_from_metadata(metadata)
    248     doc_ast = doc_ast(content_type, doc, file: source.path, line: doc_line + 1)
    249 
    250     metadata = Map.put(metadata, :__callback__, true)
    251     group = GroupMatcher.match_function(groups_for_functions, metadata)
    252 
    253     %ExDoc.FunctionNode{
    254       id: "c:#{name}/#{arity}",
    255       name: name,
    256       arity: arity,
    257       deprecated: metadata[:deprecated],
    258       doc: doc_ast,
    259       doc_line: doc_line,
    260       signature: signature,
    261       specs: specs,
    262       source_path: source.path,
    263       source_url: source_link(source, callback_data.line),
    264       type: kind,
    265       annotations: annotations,
    266       group: group
    267     }
    268   end
    269 
    270   ## Typespecs
    271 
    272   defp get_types(module_data, source) do
    273     {:docs_v1, _, _, _, _, _, docs} = module_data.docs
    274 
    275     for {{:type, _, _}, _, _, content, _} = doc <- docs, content != :hidden do
    276       get_type(doc, source, module_data)
    277     end
    278   end
    279 
    280   defp get_type(type_entry, source, module_data) do
    281     {:docs_v1, _, _, content_type, _, _, _} = module_data.docs
    282     {{_, name, arity}, anno, _signature, doc, metadata} = type_entry
    283     doc_line = anno_line(anno)
    284     annotations = annotations_from_metadata(metadata)
    285 
    286     type_data = module_data.language.type_data(type_entry, module_data)
    287     signature = signature(type_data.signature)
    288     annotations = if type_data.type == :opaque, do: ["opaque" | annotations], else: annotations
    289     doc_ast = doc_ast(content_type, doc, file: source.path)
    290 
    291     %ExDoc.TypeNode{
    292       id: "t:#{name}/#{arity}",
    293       name: name,
    294       arity: arity,
    295       type: type_data.type,
    296       spec: type_data.spec,
    297       deprecated: metadata[:deprecated],
    298       doc: doc_ast,
    299       doc_line: doc_line,
    300       signature: signature,
    301       source_path: source.path,
    302       source_url: source_link(source, type_data.line),
    303       annotations: annotations
    304     }
    305   end
    306 
    307   ## General helpers
    308 
    309   defp signature(list) when is_list(list), do: Enum.join(list, " ")
    310 
    311   defp annotations_from_metadata(metadata) do
    312     annotations = []
    313 
    314     annotations =
    315       if since = metadata[:since] do
    316         ["since #{since}" | annotations]
    317       else
    318         annotations
    319       end
    320 
    321     annotations
    322   end
    323 
    324   defp anno_line(line) when is_integer(line), do: abs(line)
    325   defp anno_line(anno), do: anno |> :erl_anno.line() |> abs()
    326 
    327   defp source_link(%{path: _, url: nil}, _line), do: nil
    328 
    329   defp source_link(source, line) do
    330     Utils.source_url_pattern(source.url, source.path, line)
    331   end
    332 
    333   defp source_path(module, _config) do
    334     module.module_info(:compile)[:source]
    335     |> String.Chars.to_string()
    336     |> Path.relative_to(File.cwd!())
    337   end
    338 end