Skip to main content

lib/ex_check/config/loader.ex

defmodule ExCheck.Config.Loader do
  @moduledoc false

  alias ExCheck.Config.Default, as: DefaultConfig
  alias ExCheck.Project

  @config_filename ".check.exs"
  @option_list ~w[parallel skipped fix retry format output]a

  def load(opts) do
    config_file =
      opts
      |> Keyword.get(:file)
      |> List.wrap()

    user_dir_config = config_filename(System.user_home())
    project_root_config = config_filename(Project.get_mix_root_dir())

    files = config_file ++ user_dir_config ++ project_root_config

    default_config = normalize_config(DefaultConfig.get())
    config = load_from_files(files, default_config)
    tools = Keyword.fetch!(config, :tools)
    opts = Keyword.take(config, @option_list)

    {tools, opts}
  end

  defp config_filename(nil), do: []

  defp config_filename(directory) do
    directory
    |> Path.join(@config_filename)
    |> Path.expand()
    |> List.wrap()
  end

  # sobelow_skip ["RCE.CodeModule"]
  defp load_from_files(files, default_config) do
    files
    |> Enum.filter(&File.exists?/1)
    |> Enum.reduce(default_config, fn next_config_filename, config ->
      {next_config, _} = Code.eval_file(next_config_filename)
      next_config = normalize_config(next_config)

      merge_config(config, next_config)
    end)
  end

  defp normalize_config(config) do
    Keyword.update(config, :tools, [], fn tools ->
      Enum.map(tools, fn tool ->
        {name, opts} = normalize_tool(tool)
        opts = normalize_tool_opts(opts)

        {name, opts}
      end)
    end)
  end

  defp normalize_tool({name, opts = [{_, _} | _]}), do: {name, opts}
  defp normalize_tool({name, enabled}) when is_boolean(enabled), do: {name, enabled: enabled}
  defp normalize_tool({name, command}) when is_binary(command), do: {name, command: command}
  defp normalize_tool({name, command = [arg | _]}) when is_binary(arg), do: {name, command: command}

  defp normalize_tool({name, command, opts = [{_, _} | _]}) when is_binary(command) do
    {name, Keyword.put(opts, :command, command)}
  end

  defp normalize_tool({name, command = [arg | _], opts = [{_, _} | _]}) when is_binary(arg) do
    {name, Keyword.put(opts, :command, command)}
  end

  defp normalize_tool_opts(opts) do
    Keyword.update(opts, :deps, [], fn deps ->
      Enum.map(deps, fn
        dep = {_, opts} when is_list(opts) -> dep
        name -> {name, []}
      end)
    end)
  end

  defp merge_config(config, next_config) do
    config_opts = Keyword.take(config, @option_list)
    next_config_opts = Keyword.take(next_config, @option_list)
    merged_opts = Keyword.merge(config_opts, next_config_opts)

    config_tools = Keyword.fetch!(config, :tools)
    next_config_tools = Keyword.get(next_config, :tools, [])

    merged_tools =
      Enum.reduce(next_config_tools, config_tools, fn next_tool, tools ->
        next_tool_name = elem(next_tool, 0)
        tool = List.keyfind(tools, next_tool_name, 0)
        merged_tool = merge_tool(tool, next_tool)

        List.keystore(tools, next_tool_name, 0, merged_tool)
      end)

    Keyword.put(merged_opts, :tools, merged_tools)
  end

  defp merge_tool(tool, next_tool)
  defp merge_tool(nil, next_tool), do: next_tool
  defp merge_tool({name, opts}, {name, next_opts}), do: {name, merge_tool_opts(opts, next_opts)}

  defp merge_tool_opts(opts, next_opts) do
    opts
    |> Keyword.merge(next_opts)
    |> Keyword.put(:env, Map.merge(opts[:env] || %{}, next_opts[:env] || %{}))
    |> Keyword.put(:umbrella, Keyword.merge(opts[:umbrella] || [], next_opts[:umbrella] || []))
  end
end