defmodule Mix.Tasks.Recode do @shortdoc "Runs the linter" @moduledoc """ #{@shortdoc}. ```shell > mix recode [options] [inputs] ``` Without a `inputs` argument the `inputs` value from the config is used. The `inputs` argument accepts a wildcard. If `inputs` value is `-`, then the input is read from stdin. Without the option `--config file` the config file `.recode.exs` is used. A default `.recode.exs` can be generated with `mix recode.gen.config`. ## Command line Option * `-a`, `--autocorrect`, `--no-autocorrect` - Activates/deactivates autocrrection. Overwrites the corresponding value in the configuration. * `-c`, `--config` - specifies an alternative config file. * `-d`, `--dry`, `--no-dry` - Activates/deactivates the dry mode. No file is overwritten in dry mode. Overwrites the corresponding value in the configuration. * `-v`, `--verbose`, `--no-verbose` - Activate/deactivates the verbose mode. Overwrites the corresponding value in the configuration. * `-s`, `--silent` - Activates the silent mode. In silent mode, only issues will be printed to the console. Without any issue no output is printed. Overwrites the corresponding value in the configuration. * `-t`, `--task`, specifies the task to use. With this option, the task is used even if it is specified as `active: false` in the configuration. This option can appear multiple times in a call. * `--slowest-tasks` - prints timing information for the N slowest tasks. * `--color` - enables color in the output. Defaults to `true` if ANSI coloring is supported. * `--manifest` - enables reading and writing of the `manifest` file. Defaults to `true` if the `--task` flag is not given. * `--force` - forces a run without reading the `manifest` file. A new manifest file is created. """ use Mix.Task alias Recode.Config alias Recode.Runner alias Rewrite.DotFormatter alias Rewrite.DotFormatterError @opts strict: [ autocorrect: :boolean, color: :boolean, config: :string, debug: :boolean, dry: :boolean, force: :boolean, manifest: :boolean, silent: :boolean, slowest_tasks: :integer, task: :keep, verbose: :boolean ], aliases: [ a: :autocorrect, c: :config, d: :dry, s: :silent, t: :task, v: :verbose ] @task_config_keys [ :active, :autocorrect, :check, :config, :exclude, :exit_code ] @impl Mix.Task @spec run(list()) :: no_return() def run(opts) do {:ok, _apps} = Application.ensure_all_started(:recode) :ok = check_dot_formatter() opts = opts!(opts) opts = opts |> Keyword.get(:config, ".recode.exs") |> read_config!() |> validate_config!() |> validate_tasks!() |> update_task_configs!() |> merge_opts(opts) |> Keyword.put(:cli_opts, cli_tasks(opts)) |> update_verbose() |> update_manifest(opts) |> put(opts, :debug, false) |> put(opts, :force, false) case Runner.run(opts) do {:ok, 0} -> exit(:normal) {:ok, exit_code} -> exit({:shutdown, exit_code}) {:error, :no_sources} -> Mix.raise("No sources found") end end defp merge_opts(config, opts) do Keyword.merge( config, Keyword.take(opts, [ :autocorrect, :color, :dry, :inputs, :manifest, :silent, :slowest_tasks, :verbose ]) ) end defp opts!(opts) do case OptionParser.parse!(opts, @opts) do {opts, []} -> opts {opts, inputs} -> Keyword.put(opts, :inputs, inputs) end end defp cli_tasks(opts) do tasks = Enum.reduce(opts, [], fn {key, value}, acc -> case key do :task -> [value | acc] _else -> acc end end) opts |> Keyword.delete(:task) |> Keyword.put(:tasks, tasks) end defp read_config!(opts) do case Config.read(opts) do {:ok, config} -> config {:error, :not_found} -> Mix.raise("Config file not found. Run `mix recode.gen.config` to create `.recode.exs`.") end end defp validate_config!(config) do case Config.validate(config) do :ok -> config {:error, :out_of_date} -> Mix.raise("The config is out of date. Run `mix recode.update.config` to update.") {:error, :no_tasks} -> Mix.raise("No `:tasks` key found in configuration.") end end defp validate_tasks!(config) do Enum.each(config[:tasks], fn {task, config} -> task |> Code.ensure_loaded() |> validate_task!(task) validate_task_config!(task, config) end) config end defp validate_task_config!(task, config) do keys = Keyword.keys(config) -- @task_config_keys if not Enum.empty?(keys) do config = Enum.reduce(keys, config, fn key, config -> {value, config} = Keyword.pop!(config, key) Keyword.update(config, :config, [{key, value}], fn task_config -> Keyword.put(task_config, key, value) end) end) Mix.raise(""" Invalid config keys #{inspect(keys)} for #{inspect(task)} found. Did you want to create a task-specific configuration: {#{inspect(task)}, #{inspect(config)}} """) end end defp validate_task!({:error, :nofile}, task) do Mix.raise("Recode task #{inspect(task)} not found.") end defp validate_task!({:module, _module}, task) do if Recode.Task not in task.__info__(:attributes)[:behaviour] do Mix.raise("The module #{inspect(task)} does not implement the Recode.Task behaviour.") end end defp update_task_configs!(config) do Keyword.update!(config, :tasks, fn tasks -> do_update_task_configs!(tasks) end) end defp do_update_task_configs!(tasks) do Enum.map(tasks, fn {task, config} -> task_config = Keyword.get(config, :config, []) case task.init(task_config) do {:ok, task_config} -> {task, Keyword.put(config, :config, task_config)} {:error, message} -> Mix.raise("The task #{inspect(task)} has an invalid config:\n#{message}") end end) end defp update_verbose(config) do case config[:dry] do true -> Keyword.put(config, :verbose, true) false -> config end end defp update_manifest(config, opts) do # Updates the manifest configuration based on CLI options and task presence. # The manifest is disabled when specific tasks are provided via --task to # ensure full task execution regardless of the manifest state. manifest? = if Keyword.has_key?(opts, :manifest) do opts[:manifest] else Keyword.get(config, :manifest, true) end opts? = not Keyword.has_key?(opts, :task) Keyword.put(config, :manifest, manifest? && opts?) end defp put(config, opts, key, default) do value = Keyword.get(opts, key, default) Keyword.put(config, key, value) end defp check_dot_formatter do if File.exists?(".formatter.exs") do case DotFormatter.read(ignore_missing_sub_formatters: true) do {:error, reason} -> reason |> DotFormatterError.message() |> Mix.raise() {:ok, _dot_formatter} -> :ok end else :ok end end end