defmodule ExCheck.Check.Compiler do @moduledoc false alias ExCheck.{Config, Project} def compile(tools, opts) do { process_compiler(tools, opts), process_others(tools, opts) } end defp process_compiler(tools, opts) do compiler = List.keyfind(tools, :compiler, 0) || raise("compiler tool definition missing") compiler = prepare(compiler, opts) case compiler do {:pending, _} -> compiler _ -> {:pending, {:compiler, ["mix", "compile"], []}} end end defp process_others(tools, opts) do tools |> List.keydelete(:compiler, 0) |> Enum.sort_by(&get_order/1) |> filter_apps_in_umbrella() |> unwrap_recursive() |> map_recursive_dependents() |> Enum.map(&prepare(&1, opts)) |> Enum.reject(&match?({:disabled, _}, &1)) end defp filter_apps_in_umbrella(tools) do app = Project.config()[:app] if Project.in_umbrella?() do Enum.filter(tools, fn {_, tool_opts} -> enabled_apps = get_in(tool_opts, [:umbrella, :apps]) !enabled_apps || Enum.member?(enabled_apps, app) end) else tools end end defp unwrap_recursive(tools) do Enum.reduce(tools, [], fn tool = {tool_name, tool_opts}, final_tools -> recursive = recursive?(tool_opts) if recursive and Project.umbrella?() do actual_apps_paths = Project.apps_paths() enabled_apps = get_in(tool_opts, [:umbrella, :apps]) apps_paths = if enabled_apps, do: Map.take(actual_apps_paths, enabled_apps), else: actual_apps_paths tool_instances = apps_paths |> Enum.sort_by(&elem(&1, 0)) |> Enum.map(fn {app_name, app_dir} -> final_tool_opts = Keyword.update(tool_opts, :cd, app_dir, &Path.join(app_dir, &1)) {{tool_name, app_name}, final_tool_opts} end) final_tools ++ tool_instances else final_tools ++ [tool] end end) end defp map_recursive_dependents(tools) do recursive_tools = tools |> Enum.filter(&match?({{_, _}, _}, &1)) |> Enum.group_by(fn {{name, _}, _} -> name end) Enum.reduce(recursive_tools, tools, fn recursive_tool, tools -> Enum.map(tools, fn {name, opts} -> opts = map_recursive_dependent(name, opts, recursive_tool) {name, opts} end) end) end defp map_recursive_dependent(name, opts, recursive_tool) do Keyword.update(opts, :deps, [], fn deps -> do_map_recursive_dependent(name, deps, recursive_tool) end) end defp do_map_recursive_dependent(name, deps, {recursive_name, recursive_instances}) do deps |> Enum.map(fn {dep, opts} -> if dep == recursive_name do case name do {_, app} -> {{recursive_name, app}, opts} _ -> Enum.map(recursive_instances, &{elem(&1, 0), opts}) end else {dep, opts} end end) |> List.flatten() end defp recursive?(tool_opts) do case get_in(tool_opts, [:umbrella, :recursive]) do nil -> tool_opts |> Keyword.fetch!(:command) |> command_to_array() |> mix_task_recursive?() recursive -> recursive end end defp command_to_array(cmd) when is_list(cmd), do: cmd defp command_to_array(cmd), do: String.split(cmd, " ") defp mix_task_recursive?(["mix", task | _]) do case Mix.Task.get(task) do nil -> false task_module -> Mix.Task.recursive(task_module) end end defp mix_task_recursive?(_) do true end defp prepare({name, tool_opts}, opts) do cond do disabled?(name, tool_opts, opts) -> {:disabled, name} failed_detection = find_failed_detection(name, tool_opts) -> prepare_failed_detection(name, failed_detection) tool_opts[:cd] && not File.dir?(tool_opts[:cd]) -> {:skipped, name, {:cd, tool_opts[:cd]}} true -> prepare_pending(name, tool_opts, opts) end end defp disabled?({name, _}, tool_opts, opts) do disabled?(name, tool_opts, opts) end defp disabled?(name, tool_opts, opts) do Keyword.get(tool_opts, :enabled, true) == false || (Keyword.has_key?(opts, :only) && !Enum.any?(opts, &(&1 == {:only, name}))) || Enum.any?(opts, fn i -> i == {:except, name} end) end defp find_failed_detection(name, tool_opts) do tool_opts |> Keyword.get(:detect, []) |> Enum.map(&split_detection_opts/1) |> Enum.map(fn {base, opts} -> {prepare_detection_base(base, name, tool_opts), opts} end) |> Enum.find(fn {base, _} -> failed_detection?(base) end) end defp split_detection_opts({:elixir, version}), do: {{:elixir, version}, []} defp split_detection_opts({:package, name, opts}), do: {{:package, name}, opts} defp split_detection_opts({:package, name}), do: {{:package, name}, []} defp split_detection_opts({:file, name, opts}), do: {{:file, name}, opts} defp split_detection_opts({:file, name}), do: {{:file, name}, []} defp prepare_detection_base({:elixir, version}, _, _), do: {:elixir, version} defp prepare_detection_base({:package, name}, {_, app}, _), do: {:package, name, app} defp prepare_detection_base({:package, name}, _, _), do: {:package, name} defp prepare_detection_base({:file, name}, _, tool_opts) do filename = tool_opts |> Keyword.get(:cd, "") |> Path.join(name) |> Path.relative_to_cwd() {:file, filename} end defp failed_detection?({:elixir, version}), do: not Version.match?(System.version(), version) defp failed_detection?({:package, name, app}), do: not Project.has_dep_in_app?(name, app) defp failed_detection?({:package, name}), do: not Project.has_dep?(name) defp failed_detection?({:file, name}), do: not File.exists?(name) defp prepare_failed_detection(name, failed_detection) do {base, opts} = failed_detection case Keyword.get(opts, :else, :skip) do :disable -> {:disabled, name} :skip -> {:skipped, name, base} end end defp prepare_pending(name, tool_opts, opts) do {mode, command} = pick_mode_and_command(tool_opts, opts) command = command |> command_to_array() |> postprocess_cmd(tool_opts) command_opts = tool_opts |> Keyword.take([:cd, :env, :deps]) |> Keyword.put(:mode, mode) |> Keyword.put(:umbrella_parallel, get_in(tool_opts, [:umbrella, :parallel])) {:pending, {name, command, command_opts}} end defp pick_mode_and_command(tool_opts, opts) do cond do opts[:fix] && tool_opts[:fix] -> {:fix, tool_opts[:fix]} opts[:retry] && tool_opts[:retry] -> {:retry, tool_opts[:retry]} true -> {nil, Keyword.fetch!(tool_opts, :command)} end end defp postprocess_cmd(cmd, opts) do if Keyword.get(opts, :enable_ansi, true) do supports_erl_config = Version.match?(System.version(), ">= 1.9.0") enable_ansi(cmd, supports_erl_config) else cmd end end # Elixir commands executed by `mix check` are not run in a TTY and will by default not print ANSI # characters in their output - which means no colors, no bold etc. This makes the tool output # (e.g. assertion diffs from ex_unit) less useful. We explicitly enable ANSI to fix that. defp enable_ansi(["mix" | arg], true), do: ["elixir", "--erl-config", enable_ansi_erl_cfg_path(), "-S", "mix" | arg] defp enable_ansi(["elixir" | arg], true), do: ["elixir", "--erl-config", enable_ansi_erl_cfg_path() | arg] defp enable_ansi(["mix" | arg], false), do: ["elixir", "-e", "Application.put_env(:elixir, :ansi_enabled, true)", "-S", "mix" | arg] defp enable_ansi(cmd, _), do: cmd defp enable_ansi_erl_cfg_path, do: Application.app_dir(:ex_check_ng, ~w[priv enable_ansi enable_ansi.config]) defp get_order({name, opts}), do: [Keyword.get(opts, :order, 0), Config.Default.tool_order(name)] end