defmodule Mix.Tasks.Doctor do @moduledoc """ Doctor is a command line utility that can be used to ensure that your project documentation remains healthy. For more in depth documentation on Doctor or to file bug/feature requests, please check out https://github.com/akoutmos/doctor. The `mix doctor` command supports the following CLI flags (all of these options and more are also configurable from your `.doctor.exs` file). The following CLI flags are supported: ``` --config_file Provide a relative or absolute path to a `.doctor.exs` file to use during the execution of the mix command. --full When generating a Doctor report of your project, use the Doctor.Reporters.Full reporter. --short When generating a Doctor report of your project, use the Doctor.Reporters.Short reporter. --summary When generating a Doctor report of your project, use the Doctor.Reporters.Summary reporter. --raise If any of your modules fails Doctor validation, then raise an error and return a non-zero exit status. --failed If set, only the failed modules will be reported. Works with --full and --short options. --umbrella By default, in an umbrella project, each app will be evaluated independently against the specified thresholds in your .doctor.exs file. This flag changes that behavior by aggregating the results of all your umbrella apps, and then comparing those results to the configured thresholds. ``` """ use Mix.Task alias Doctor.{CLI, Config} alias Doctor.Reporters.{Full, Short, Summary} @shortdoc "Documentation coverage report" @recursive true @umbrella_accumulator Doctor.Umbrella @impl true def run(args) do cli_arg_opts = parse_cli_args(args) config_file_opts = load_config_file(cli_arg_opts) # Aggregate all of the various options sources # Precedence order is: # default < config file < cli args config = config_file_opts |> Map.merge(cli_arg_opts) |> Config.new() if config.umbrella do run_umbrella(config) else run_default(config) end end defp run_umbrella(config) do module_report_list = CLI.generate_module_report_list(config) acc_pid = case Process.whereis(@umbrella_accumulator) do nil -> init_umbrella_acc(config) pid -> pid end Agent.update(acc_pid, fn acc -> acc ++ module_report_list end) :ok end defp run_default(config) do result = config |> CLI.generate_module_report_list() |> CLI.process_module_report_list(config) unless result do System.at_exit(fn _ -> exit({:shutdown, 1}) end) if config.raise do Mix.raise("Doctor validation has failed and raised an error") end end :ok end defp init_umbrella_acc(config) do {:ok, pid} = Agent.start_link(fn -> [] end, name: @umbrella_accumulator) System.at_exit(fn _ -> module_report_list = Agent.get(pid, & &1) Agent.stop(pid) result = CLI.process_module_report_list(module_report_list, config) unless result do if config.raise do Mix.raise("Doctor validation has failed and raised an error") end exit({:shutdown, 1}) end end) pid end defp load_config_file(%{config_file_path: file_path} = _cli_args) do full_path = Path.expand(file_path) if File.exists?(full_path) do Mix.shell().info("Doctor file found. Loading configuration.") {config, _bindings} = Code.eval_file(full_path) config else Mix.shell().error("Doctor file not found at path \"#{full_path}\". Using defaults.") %{} end end defp load_config_file(_) do # If we are performing this operation on an umbrella app then look to # the project root for the config file file = if Mix.Task.recursing?() do Path.join(["..", "..", Config.config_file()]) else Config.config_file() end if File.exists?(file) do Mix.shell().info("Doctor file found. Loading configuration.") {config, _bindings} = Code.eval_file(file) config else Mix.shell().info("Doctor file not found. Using defaults.") %{} end end defp parse_cli_args(args) do {parsed_args, _args, _invalid} = OptionParser.parse(args, strict: [ full: :boolean, short: :boolean, summary: :boolean, raise: :boolean, failed: :boolean, umbrella: :boolean, config_file: :string ] ) parsed_args |> Enum.reduce(%{}, fn {:full, true}, acc -> Map.merge(acc, %{reporter: Full}) {:short, true}, acc -> Map.merge(acc, %{reporter: Short}) {:summary, true}, acc -> Map.merge(acc, %{reporter: Summary}) {:raise, true}, acc -> Map.merge(acc, %{raise: true}) {:failed, true}, acc -> Map.merge(acc, %{failed: true}) {:umbrella, true}, acc -> Map.merge(acc, %{umbrella: true}) {:config_file, file_path}, acc -> Map.merge(acc, %{config_file_path: file_path}) _unexpected_arg, acc -> acc end) end end