defmodule Doctor.Reporters.ModuleExplain do @moduledoc """ This module produces a report for a single project module. This is useful when you need to figure out exactly why a particular module failed validation. The only validations that are taken into account during this report are single module validations. In other words, the only thing that is checked are things that pertain to a single module like: - `min_module_doc_coverage` - `min_module_spec_coverage` - `moduledoc_required` - `struct_type_spec_required` """ alias Doctor.{Config, Docs, Specs} alias Doctor.{ModuleInformation, ModuleReport} alias Doctor.Reporters.OutputUtils @doc """ Generate the output for a single module report """ def generate_report(%ModuleInformation{} = module_information, %Config{} = config) do module_report = ModuleReport.build(module_information) user_defined_functions = module_information.user_defined_functions module_docs = module_information.docs module_specs = module_information.specs # Get max function name length # 13 is picked as the starting acc as that is the length of "Function Name" # which is the column header max_length = Enum.reduce(user_defined_functions, 13, fn {function, _arity, _impl}, acc -> length = function |> Atom.to_string() |> String.length() if length > acc, do: length, else: acc end) |> Kernel.+(5) # Print table header generate_header(max_length) OutputUtils.print_divider(max_length + 11) # Print per function information Enum.each(user_defined_functions, fn {function, arity, impl} -> function_name = function |> Atom.to_string() |> Kernel.<>("/#{arity}") |> OutputUtils.gen_fixed_width_string(max_length) has_doc = function |> has_doc(arity, impl, module_docs) |> OutputUtils.print_pass_or_fail() |> OutputUtils.gen_fixed_width_string(6) has_spec = function |> has_spec(arity, impl, module_specs) |> OutputUtils.print_pass_or_fail() |> OutputUtils.gen_fixed_width_string(6) Mix.shell().info("#{function_name}#{has_doc}#{has_spec}") end) # Print module summary info Mix.shell().info("\nModule Results:") print_doc_coverage(module_report, config) print_spec_coverage(module_report, config) print_module_doc(module_report, config) print_struct_spec(module_report, config) # Determine whether the module passed or failed doc_coverage = module_report.doc_coverage |> Decimal.round(1) |> Decimal.to_float() spec_coverage = module_report.spec_coverage |> Decimal.round(1) |> Decimal.to_float() ((config.struct_type_spec_required and module_report.has_struct_type_spec == :not_struct) or module_report.has_struct_type_spec) and (config.moduledoc_required and module_report.has_module_doc) and doc_coverage >= config.min_module_doc_coverage and spec_coverage >= config.min_module_spec_coverage end defp print_struct_spec(%ModuleReport{} = module_report, %Config{} = config) do if config.struct_type_spec_required and module_report.has_struct_type_spec do OutputUtils.print_success( " Has Struct Spec: #{OutputUtils.print_pass_or_fail(module_report.has_struct_type_spec)}" ) else OutputUtils.print_error( " Has Struct Spec: #{OutputUtils.print_pass_or_fail(module_report.has_struct_type_spec)} --> Your config has a 'struct_type_spec_required' value of true" ) end end defp print_module_doc(%ModuleReport{} = module_report, %Config{} = config) do if config.moduledoc_required and module_report.has_module_doc do OutputUtils.print_success(" Has Module Doc: #{OutputUtils.print_pass_or_fail(module_report.has_module_doc)}") else OutputUtils.print_error( " Has Module Doc: #{OutputUtils.print_pass_or_fail(module_report.has_module_doc)} --> Your config has a 'moduledoc_required' value of true" ) end end defp print_doc_coverage(%ModuleReport{} = module_report, %Config{} = config) do doc_coverage = module_report.doc_coverage |> Decimal.round(1) |> Decimal.to_float() if doc_coverage >= config.min_module_doc_coverage do OutputUtils.print_success(" Doc Coverage: #{doc_coverage}%") else OutputUtils.print_error( " Doc Coverage: #{doc_coverage}% --> Your config has a 'min_module_doc_coverage' value of #{ config.min_module_doc_coverage }" ) end end defp print_spec_coverage(%ModuleReport{} = module_report, %Config{} = config) do spec_coverage = module_report.spec_coverage |> Decimal.round(1) |> Decimal.to_float() if spec_coverage >= config.min_module_spec_coverage do OutputUtils.print_success(" Spec Coverage: #{spec_coverage}%") else OutputUtils.print_error( " Spec Coverage: #{spec_coverage}% --> Your config has a 'min_module_spec_coverage' value of #{ config.min_module_spec_coverage }" ) end end defp generate_header(function_name_length) do output_line = OutputUtils.generate_table_line([ {"Function", function_name_length}, {"@doc", 6}, {"@spec", 7} ]) Mix.shell().info("\n#{output_line}") end defp has_doc(function, arity, :none, module_docs) do Enum.any?(module_docs, fn %Docs{arity: ^arity, name: ^function, doc: doc} when doc != :none -> true _ -> false end) end defp has_doc(_, _, _, _) do true end defp has_spec(function, arity, :none, module_specs) do Enum.any?(module_specs, fn %Specs{arity: ^arity, name: ^function} -> true _ -> false end) end defp has_spec(_, _, _, _) do true end end