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