Current section
Files
Jump to
Current section
Files
lib/module_information.ex
defmodule Doctor.ModuleInformation do @moduledoc """ This module defines a struct which houses all the documentation data for an entire module. """ alias __MODULE__ alias Doctor.{Docs, Specs} @type t :: %ModuleInformation{ module: module(), behaviours: [module()], file_full_path: String.t(), file_relative_path: String.t(), file_ast: list(), docs_version: atom(), module_doc: map(), metadata: map(), docs: [%Docs{}], specs: list(), user_defined_functions: [{atom(), integer(), atom() | boolean()}], struct_type_spec: atom() | boolean(), properties: Keyword.t() } defstruct ~w( module file_full_path file_relative_path file_ast docs_version module_doc metadata docs specs user_defined_functions behaviours struct_type_spec properties )a @doc """ Breaks down the docs format entry returned from Code.fetch_docs(MODULE) """ def build({docs_version, _annotation, _language, _format, module_doc, metadata, docs}, module) do {:ok, module_specs} = Code.Typespec.fetch_specs(module) %ModuleInformation{ module: module, behaviours: get_module_behaviours(module), file_full_path: get_full_file_path(module), file_relative_path: get_relative_file_path(module), file_ast: nil, docs_version: docs_version, module_doc: module_doc, metadata: metadata, docs: Enum.map(docs, &Docs.build/1), specs: Enum.map(module_specs, &Specs.build/1), user_defined_functions: nil, struct_type_spec: contains_struct_type_spec?(module), properties: [ is_exception: is_exception?(module), is_protocol_implementation: is_protocol_implementation?(module) ] } end @doc """ Given the provided module, read the file from which the module was generated and convert the file to an AST. """ def load_file_ast(%ModuleInformation{} = module_info) do ast = module_info.file_full_path |> File.read!() |> Code.string_to_quoted!() %{module_info | file_ast: ast} end @doc """ Checks the provided module for a __struct__ function which is injected into the module whenever you use `defstruct` """ def contains_struct_type_spec?(module) do cond do is_exception?(module) -> :not_struct is_struct?(module) -> {:ok, specs} = Code.Typespec.fetch_types(module) Enum.any?(specs, fn {:type, {:t, _, _}} -> true {:opaque, {:t, _, _}} -> true _ -> false end) true -> :not_struct end end defp is_struct?(module) do function_exported?(module, :__struct__, 0) or function_exported?(module, :__struct__, 1) end defp is_exception?(module) when is_atom(module) do function_exported?(module, :__struct__, 0) and :__exception__ in Map.keys(module.__struct__()) end defp is_protocol_implementation?(module) when is_atom(module) do function_exported?(module, :__impl__, 1) end @doc """ Given a ModuleInformation struct with the AST loaded, fetch all of the author defined functions """ def load_user_defined_functions(%ModuleInformation{} = module_info) do {_ast, %{modules: modules}} = Macro.traverse( module_info.file_ast, %{modules: %{}, stack: []}, &parse_ast_node_for_defmodules/2, &pop_module_stack/2 ) {_ast, %{functions: functions}} = modules |> Map.get(module_info.module) |> Macro.traverse( %{functions: [], last_impl: :none, nesting_level: 0}, &parse_ast_node_for_def/2, &unnest/2 ) %{module_info | user_defined_functions: Enum.uniq(functions)} |> load_using_docs_and_specs(modules) end defp load_using_docs_and_specs(%ModuleInformation{} = module_info, modules) do {_ast, using} = modules |> Map.get(module_info.module) |> Macro.prewalk(%{using: :none}, &parse_ast_for_using/2) acc = %{ last_doc: :none, last_spec: :none, using_docs: [], using_specs: [] } {_ast, extra} = using[:using] |> Macro.prewalk(acc, &parse_ast_using_node/2) %{ module_info | specs: module_info.specs ++ extra.using_specs, docs: module_info.docs ++ extra.using_docs } end defp get_module_behaviours(module) do {_module, bin, _beam_file_path} = :code.get_object_code(module) case :beam_lib.chunks(bin, [:attributes]) do {:ok, {^module, attributes}} -> attributes |> Keyword.get(:attributes, []) |> Keyword.get(:behaviour, []) _ -> [] end end defp get_full_file_path(module) do module.module_info() |> Keyword.get(:compile) |> Keyword.get(:source) |> to_string() end defp get_relative_file_path(module) do module |> get_full_file_path() |> Path.relative_to(File.cwd!()) end defp parse_ast_node_for_def({definition, _defmodule_line, _body} = ast, %{nesting_level: level} = acc) when definition in [:defimpl, :defmodule, :defprotocol] do {ast, Map.put(acc, :nesting_level, level + 1)} end defp parse_ast_node_for_def(ast, %{nesting_level: level} = acc) when level > 1 do {ast, acc} end defp parse_ast_node_for_def({:@, _line_number, [{:doc, _, [false]}]} = ast, acc) do updated_acc = Map.put(acc, :last_impl, false) {ast, updated_acc} end defp parse_ast_node_for_def({:@, _line_number, [{:impl, _, impl_def}]} = ast, acc) do normalized_impl = normalize_impl(impl_def) updated_acc = Map.put(acc, :last_impl, normalized_impl) {ast, updated_acc} end defp parse_ast_node_for_def( {:def, _def_line, [{:when, _line_when, [{function_name, _function_line, args}, _guard]}, _do_block]} = ast, %{last_impl: impl} = acc ) do function_arity = get_function_arity(args) updated_acc = update_acc_for_def(acc, function_name, function_arity, impl) {ast, updated_acc} end defp parse_ast_node_for_def( {:def, _def_line, [{function_name, _function_line, args}, _do_block]} = ast, %{last_impl: impl} = acc ) do function_arity = get_function_arity(args) updated_acc = update_acc_for_def(acc, function_name, function_arity, impl) {ast, updated_acc} end defp parse_ast_node_for_def( {:def, _def_line, [{function_name, _function_line, args}]} = ast, %{last_impl: impl} = acc ) do function_arity = get_function_arity(args) updated_acc = update_acc_for_def(acc, function_name, function_arity, impl) {ast, updated_acc} end defp parse_ast_node_for_def(ast, acc) do {ast, acc} end defp unnest({definition, _defmodule_line, _body} = ast, %{nesting_level: level} = acc) when definition in [:defmodule, :defprotocol] do {ast, Map.put(acc, :nesting_level, level - 1)} end defp unnest(ast, acc) do {ast, acc} end defp update_acc_for_def(acc, function_name, function_arity, last_impl) do impl = case last_impl do :none -> acc[:functions] |> Enum.filter(fn {name, arity, _impl} -> name == function_name and arity == function_arity end) |> Enum.at(0, {function_name, function_arity, :none}) |> Kernel.elem(2) last_impl -> last_impl end acc |> Map.put(:last_impl, :none) |> Map.update(:functions, [], fn functions -> [{function_name, function_arity, impl} | functions] end) end defp normalize_impl([value]) when is_boolean(value) do value end defp normalize_impl([{:__aliases__, _, module}]) do Module.concat(module) end defp normalize_impl(value) do value end defp parse_ast_node_for_defmodules( {definition, _defmodule_line, [{:__aliases__, _line_num, module}, _do_block]} = ast, %{modules: modules, stack: stack} = acc ) when definition in [:defmodule, :defprotocol] do parent = List.first(stack) full_module_name = Module.concat(List.wrap(parent) ++ module) updated_acc = acc |> Map.put(:modules, Map.put(modules, full_module_name, ast)) |> Map.put(:stack, [full_module_name | stack]) {ast, updated_acc} end defp parse_ast_node_for_defmodules(ast, acc) do {ast, acc} end defp pop_module_stack( {definition, _defmodule_line, _body} = ast, %{stack: [_current | rest]} = acc ) when definition in [:defmodule, :defprotocol] do {ast, Map.put(acc, :stack, rest)} end defp pop_module_stack(ast, acc) do {ast, acc} end defp get_function_arity(nil), do: 0 defp get_function_arity(args), do: length(args) defp parse_ast_for_using({:defmacro, _macro_line, [{:__using__, _line, _args}, do_block]} = ast, _acc), do: {ast, %{using: do_block}} defp parse_ast_for_using(ast, acc), do: {ast, acc} defp parse_ast_using_node( {:@, _doc_line, [{:doc, _line, [doc]}]} = ast, acc ), do: {ast, Map.put(acc, :last_doc, doc)} defp parse_ast_using_node( {:@, _spec_line, [{:spec, _line, _spec_info}]} = ast, acc ), do: {ast, Map.put(acc, :last_spec, true)} defp parse_ast_using_node( {:def, _def_line, [{:when, _line_when, [{function_name, _function_line, args}, _guard]}, _do_block]} = ast, acc ) do {ast, update_acc_for_using(function_name, args, acc)} end defp parse_ast_using_node( {:def, _def_line, [{function_name, _function_line, args}, _do_block]} = ast, acc ) do {ast, update_acc_for_using(function_name, args, acc)} end defp parse_ast_using_node( {:def, _def_line, [{function_name, _function_line, args}]} = ast, acc ) do {ast, update_acc_for_using(function_name, args, acc)} end defp parse_ast_using_node(ast, acc), do: {ast, acc} defp update_acc_for_using(function_name, args, acc) do function_arity = get_function_arity(args) function_spec = if acc.last_spec != :none do [%Doctor.Specs{arity: function_arity, name: function_name}] else [] end function_doc = if acc.last_doc != :none do [ %Doctor.Docs{ arity: function_arity, doc: %{"en" => acc.last_doc}, kind: :function, name: function_name } ] else [] end %{ last_doc: :none, last_spec: :none, using_docs: acc.using_docs ++ function_doc, using_specs: acc.using_specs ++ function_spec } endend