lib/tidy.ex

defmodule Tidy do
  @moduledoc """
  Documentation for Tidy.
  """
  alias Tidy.{Config, Inspection, Report}

  def analyze_app(app, config \\ Config.load()) do
    with {:ok, modules} <- :application.get_key(app, :modules) do
      modules
      |> Enum.reject(&(&1 in config.ignore.modules))
      |> Enum.map(&analyze(&1, config))
      |> Enum.reject(&(&1.reference.type == :protocol))
    end
  end

  def analyze(module, config \\ Config.load()) do
    with {:ok, data} <- Inspection.inspect_module(module, config) do
      report = %Report{module: module, reference: data}

      Enum.reduce(config.checks, report, &perform_check/2)
    end
  end

  def errors?(report, opts \\ [])

  def errors?(%Report{errors: errors}, opts) do
    errors.function
    |> Enum.flat_map(&elem(&1, 1))
    |> Kernel.++(errors.module)
    |> filter_check(listify(opts[:check]))
    |> filter_type(listify(opts[:type]))
    |> Enum.count()
  end

  def errors?(reports, opts) when is_list(reports) do
    reports
    |> Enum.map(&errors?(&1, opts))
    |> Enum.sum()
  end

  defp perform_check(check = {mod, _}, report) do
    perform_check(mod.scope, check, report)
  end

  defp perform_check(:module, {check, opts}, report) do
    case check.check(report.reference, opts) do
      :ok ->
        report

      {:error, short, long} ->
        error = %{
          level: opts[:level] || :error,
          short: short,
          long: long,
          type: check
        }

        %{report | errors: Map.update!(report.errors, :module, &[error | &1])}
    end
  end

  defp perform_check(:function, {check, opts}, report) do
    Enum.reduce(report.reference.functions, report, fn function, report ->
      case check.check(function, opts) do
        :ok ->
          report

        {:error, short, long} ->
          error = %{
            level: opts[:level] || :error,
            short: short,
            long: long,
            type: check
          }

          function_errors =
            Map.update(
              report.errors.function,
              {function.name, function.arity},
              [error],
              &[error | &1]
            )

          %{report | errors: Map.put(report.errors, :function, function_errors)}
      end
    end)
  end

  def output(report = %Report{}) do
    IO.puts("""
    ---
    #{report.reference.module}:
    ---
    """)
  end

  def errors(report, opts \\ [])

  def errors(reports, opts) when is_list(reports),
    do: reports |> Enum.map(&errors(&1, opts)) |> IO.iodata_to_binary()

  def errors(%Report{reference: ref, errors: errors}, opts) do
    module =
      case errors.module do
        [] ->
          ""

        errors ->
          errors
          |> filter_check(listify(opts[:check]))
          |> filter_type(listify(opts[:type]))
          |> Enum.map(fn %{short: error} -> "    #{error}" end)
          |> Enum.join("\n")
          |> Kernel.<>("\n")
      end

    function =
      case Enum.to_list(errors.function) do
        [] ->
          ""

        errors ->
          errors
          |> Enum.flat_map(fn {{fun, arity}, fun_errors} ->
            err =
              fun_errors
              |> filter_check(listify(opts[:check]))
              |> filter_type(listify(opts[:type]))
              |> Enum.map(fn %{short: error} -> "      #{error}" end)

            [
              "\n    #{IO.ANSI.cyan()}#{fun}/#{arity}#{IO.ANSI.reset()}:"
              | err
            ]
          end)
          |> Enum.join("\n")
          |> Kernel.<>("\n")
      end

    result = module <> function

    if result != "" do
      [
        [
          IO.ANSI.yellow_background(),
          "\n",
          IO.ANSI.black(),
          "  ",
          String.trim_leading("#{ref.id}", "Elixir."),
          IO.ANSI.reset()
        ],
        "\n",
        result
      ]
      |> IO.iodata_to_binary()
    else
      ""
    end
  end

  defp filter_check(list, []), do: list
  defp filter_check(list, checks), do: Enum.filter(list, &(&1.type in checks))

  defp filter_type(list, []), do: list
  defp filter_type(list, types), do: Enum.filter(list, &(&1.type.category in types))

  defp listify(nil), do: []
  defp listify(list) when is_list(list), do: list
  defp listify(item), do: [item]
end