defmodule Doctor.ModuleReport do @moduledoc """ This module exposes a struct which encapsulates all the results for a doctor report. Whether the module has a moduledoc, what the doc coverage is, the number of author defined functions, and so on. """ alias __MODULE__ alias Doctor.ModuleInformation @type t :: %ModuleReport{ doc_coverage: Decimal.t(), spec_coverage: Decimal.t(), file: String.t(), module: module(), functions: integer(), missed_docs: integer(), missed_specs: integer(), has_module_doc: boolean() } defstruct ~w(doc_coverage spec_coverage file module functions missed_docs missed_specs has_module_doc)a @doc """ Given a ModuleInformation struct with the necessary fields completed, build the report. """ def build(%ModuleInformation{} = module_info) do %ModuleReport{ doc_coverage: calculate_doc_coverage(module_info), spec_coverage: calculate_spec_coverage(module_info), file: module_info.file_relative_path, module: generate_module_name(module_info.module), functions: length(module_info.user_defined_functions), missed_docs: calculate_missed_docs(module_info), missed_specs: calculate_missed_specs(module_info), has_module_doc: has_module_doc?(module_info) } end defp generate_module_name(module) do module |> Module.split() |> Enum.join(".") end defp calculate_missed_docs(module_info) do function_arity_list = Enum.map(module_info.user_defined_functions, fn {function, arity, _impl} -> {function, arity} end) Enum.count(module_info.docs, fn doc -> {doc.name, doc.arity} in function_arity_list and doc.doc == :none end) end defp calculate_doc_coverage(module_info) do total = length(module_info.user_defined_functions) missed = calculate_missed_docs(module_info) if total > 0 do (total - missed) |> Decimal.div(total) |> Decimal.mult(100) else nil end end defp calculate_missed_specs(module_info) do function_specs = module_info.specs |> Enum.map(fn spec -> {spec.name, spec.arity} end) Enum.count(module_info.user_defined_functions, fn {function, arity, impl} -> cond do {function, arity} in function_specs -> false is_boolean(impl) and impl -> false is_atom(impl) and Module.concat([impl]) in module_info.behaviours -> false true -> true end end) end defp calculate_spec_coverage(module_info) do total = length(module_info.user_defined_functions) missed = calculate_missed_specs(module_info) if total > 0 do (total - missed) |> Decimal.div(total) |> Decimal.mult(100) else nil end end defp has_module_doc?(module_info) do module_info.module_doc != :none end end