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