lib/mix/tasks/metacredo.gen.config.ex

defmodule Mix.Tasks.Metacredo.Gen.Config do
  @shortdoc "Generate a default .metacredo.exs configuration file"
  @moduledoc """
  Generates a `.metacredo.exs` configuration file in the current directory.

  The generated file lists every available MetaCredo check grouped by category.
  To disable a check, move its entry to the `disabled` list or set its second
  element to `false`:

      {MetaCredo.Check.Security.SQLInjection, false}

  ## Usage

      $ mix metacredo.gen.config

  ## Options

    * `--force` - Overwrite an existing `.metacredo.exs`
  """

  use Mix.Task

  @impl Mix.Task
  def run(argv) do
    force? = "--force" in argv
    path = MetaCredo.Config.default_config_path()

    if File.exists?(path) and not force? do
      Mix.shell().info("#{path} already exists. Use --force to overwrite.")
    else
      Mix.Task.run("app.start", [])
      File.write!(path, build_config())
      Mix.shell().info("Created #{path}")
    end
  end

  # -- Private --

  defp build_config do
    checks_section = build_checks_section()

    """
    # Generated by `mix metacredo.gen.config`.
    # Move any check to the `disabled` list or set its second element to `false`
    # to turn it off.  Add `{ModuleName, [param: value]}` to customise params.
    %{
      configs: [
        %{
          name: "default",
          files: %{
            included: ["lib/", "src/", "web/"],
            excluded: [
              ~r"/_build/",
              ~r"/deps/",
              ~r"/node_modules/"
            ]
          },
          checks: %{
            enabled: [
    #{checks_section}
            ],
            disabled: []
          }
        }
      ]
    }
    """
  end

  defp build_checks_section do
    discover_checks()
    |> Enum.group_by(& &1.category())
    |> Enum.sort_by(fn {cat, _} -> to_string(cat) end)
    |> Enum.map_join("\n", fn {category, checks} ->
      header = "              # -- #{format_category(category)} --"

      lines =
        checks
        |> Enum.sort_by(&inspect/1)
        |> Enum.map_join(",\n", fn mod ->
          "              {#{inspect(mod)}, []}"
        end)

      header <> "\n" <> lines
    end)
  end

  defp discover_checks do
    case :application.get_key(:metacredo, :modules) do
      {:ok, modules} ->
        Enum.filter(modules, fn mod ->
          String.starts_with?(to_string(mod), "Elixir.MetaCredo.Check.") and
            function_exported?(mod, :run, 2) and
            function_exported?(mod, :category, 0)
        end)

      :undefined ->
        []
    end
  end

  defp format_category(category) do
    category
    |> to_string()
    |> String.replace("_", " ")
    |> String.split()
    |> Enum.map_join(" ", &String.capitalize/1)
  end
end