zf

zenflows testing
git clone https://s.sonu.ch/~srfsh/zf.git
Log | Files | Refs | Submodules | README | LICENSE

execution.ex (23765B)


      1 defmodule Credo.Execution do
      2   @moduledoc """
      3   Every run of Credo is configured via an `Credo.Execution` struct, which is created and
      4   manipulated via the `Credo.Execution` module.
      5   """
      6 
      7   @doc """
      8   The `Credo.Execution` struct is created and manipulated via the `Credo.Execution` module.
      9   """
     10   defstruct argv: [],
     11             cli_options: nil,
     12             # TODO: these initial switches should also be %Credo.CLI.Switch{} struct
     13             cli_switches: [
     14               debug: :boolean,
     15               color: :boolean,
     16               config_name: :string,
     17               config_file: :string,
     18               working_dir: :string
     19             ],
     20             cli_aliases: [C: :config_name, D: :debug],
     21             cli_switch_plugin_param_converters: [],
     22 
     23             # config
     24             files: nil,
     25             color: true,
     26             debug: false,
     27             checks: nil,
     28             requires: [],
     29             plugins: [],
     30             parse_timeout: 5000,
     31             strict: false,
     32 
     33             # options, set by the command line
     34             format: nil,
     35             help: false,
     36             verbose: false,
     37             version: false,
     38 
     39             # options, that are kept here for legacy reasons
     40             all: false,
     41             crash_on_error: true,
     42             enable_disabled_checks: nil,
     43             ignore_checks_tags: [],
     44             ignore_checks: nil,
     45             max_concurrent_check_runs: nil,
     46             min_priority: 0,
     47             mute_exit_status: false,
     48             only_checks_tags: [],
     49             only_checks: nil,
     50             read_from_stdin: false,
     51 
     52             # state, which is accessed and changed over the course of Credo's execution
     53             pipeline_map: %{},
     54             commands: %{},
     55             config_files: [],
     56             current_task: nil,
     57             parent_task: nil,
     58             initializing_plugin: nil,
     59             halted: false,
     60             config_files_pid: nil,
     61             source_files_pid: nil,
     62             issues_pid: nil,
     63             timing_pid: nil,
     64             skipped_checks: nil,
     65             assigns: %{},
     66             results: %{},
     67             config_comment_map: %{}
     68 
     69   @typedoc false
     70   @type t :: %__MODULE__{}
     71 
     72   @execution_pipeline_key __MODULE__
     73   @execution_pipeline_key_backwards_compatibility_map %{
     74     Credo.CLI.Command.Diff.DiffCommand => "diff",
     75     Credo.CLI.Command.List.ListCommand => "list",
     76     Credo.CLI.Command.Suggest.SuggestCommand => "suggest",
     77     Credo.CLI.Command.Info.InfoCommand => "info"
     78   }
     79   @execution_pipeline [
     80     __pre__: [
     81       Credo.Execution.Task.AppendDefaultConfig,
     82       Credo.Execution.Task.AppendExtraConfig,
     83       {Credo.Execution.Task.ParseOptions, parser_mode: :preliminary},
     84       Credo.Execution.Task.ConvertCLIOptionsToConfig,
     85       Credo.Execution.Task.InitializePlugins
     86     ],
     87     parse_cli_options: [{Credo.Execution.Task.ParseOptions, parser_mode: :preliminary}],
     88     initialize_plugins: [
     89       # This is where plugins can "put" their hooks using `Credo.Plugin.append_task/3`
     90       # to initialize themselves based on the params given in the config as well as
     91       # in their own command line switches.
     92       #
     93       # Example:
     94       #
     95       #     defmodule CredoDemoPlugin do
     96       #       import Credo.Plugin
     97       #
     98       #       def init(exec) do
     99       #         append_task(exec, :initialize_plugins, CredoDemoPlugin.SetDiffAsDefaultCommand)
    100       #       end
    101       #     end
    102     ],
    103     determine_command: [Credo.Execution.Task.DetermineCommand],
    104     set_default_command: [Credo.Execution.Task.SetDefaultCommand],
    105     initialize_command: [Credo.Execution.Task.InitializeCommand],
    106     parse_cli_options_final: [{Credo.Execution.Task.ParseOptions, parser_mode: :strict}],
    107     validate_cli_options: [Credo.Execution.Task.ValidateOptions],
    108     convert_cli_options_to_config: [Credo.Execution.Task.ConvertCLIOptionsToConfig],
    109     resolve_config: [Credo.Execution.Task.UseColors, Credo.Execution.Task.RequireRequires],
    110     validate_config: [Credo.Execution.Task.ValidateConfig],
    111     run_command: [Credo.Execution.Task.RunCommand],
    112     halt_execution: [Credo.Execution.Task.AssignExitStatusForIssues]
    113   ]
    114 
    115   alias Credo.Execution.ExecutionConfigFiles
    116   alias Credo.Execution.ExecutionIssues
    117   alias Credo.Execution.ExecutionSourceFiles
    118   alias Credo.Execution.ExecutionTiming
    119 
    120   @doc "Builds an Execution struct for the the given `argv`."
    121   def build(argv \\ []) when is_list(argv) do
    122     max_concurrent_check_runs = System.schedulers_online()
    123 
    124     %__MODULE__{argv: argv, max_concurrent_check_runs: max_concurrent_check_runs}
    125     |> put_pipeline(@execution_pipeline_key, @execution_pipeline)
    126     |> put_builtin_command("categories", Credo.CLI.Command.Categories.CategoriesCommand)
    127     |> put_builtin_command("diff", Credo.CLI.Command.Diff.DiffCommand)
    128     |> put_builtin_command("explain", Credo.CLI.Command.Explain.ExplainCommand)
    129     |> put_builtin_command("gen.check", Credo.CLI.Command.GenCheck)
    130     |> put_builtin_command("gen.config", Credo.CLI.Command.GenConfig)
    131     |> put_builtin_command("help", Credo.CLI.Command.Help)
    132     |> put_builtin_command("info", Credo.CLI.Command.Info.InfoCommand)
    133     |> put_builtin_command("list", Credo.CLI.Command.List.ListCommand)
    134     |> put_builtin_command("suggest", Credo.CLI.Command.Suggest.SuggestCommand)
    135     |> put_builtin_command("version", Credo.CLI.Command.Version)
    136     |> start_servers()
    137   end
    138 
    139   @doc false
    140   def build(%__MODULE__{} = previous_exec, files_that_changed) when is_list(files_that_changed) do
    141     previous_exec.argv
    142     |> build()
    143     |> put_rerun(previous_exec, files_that_changed)
    144   end
    145 
    146   def build(argv, files_that_changed) when is_list(files_that_changed) do
    147     build(argv)
    148   end
    149 
    150   @doc false
    151   defp start_servers(%__MODULE__{} = exec) do
    152     exec
    153     |> ExecutionConfigFiles.start_server()
    154     |> ExecutionIssues.start_server()
    155     |> ExecutionSourceFiles.start_server()
    156     |> ExecutionTiming.start_server()
    157   end
    158 
    159   @doc """
    160   Returns the checks that should be run for a given `exec` struct.
    161 
    162   Takes all checks from the `checks:` field of the exec, matches those against
    163   any patterns to include or exclude certain checks given via the command line.
    164   """
    165   def checks(exec)
    166 
    167   def checks(%__MODULE__{checks: nil}) do
    168     {[], [], []}
    169   end
    170 
    171   def checks(%__MODULE__{
    172         checks: %{enabled: checks},
    173         only_checks: only_checks,
    174         only_checks_tags: only_checks_tags,
    175         ignore_checks: ignore_checks,
    176         ignore_checks_tags: ignore_checks_tags
    177       }) do
    178     only_matching =
    179       checks |> filter_only_checks_by_tags(only_checks_tags) |> filter_only_checks(only_checks)
    180 
    181     ignore_matching_by_name = filter_ignore_checks(checks, ignore_checks)
    182     ignore_matching_by_tags = filter_ignore_checks_by_tags(checks, ignore_checks_tags)
    183     ignore_matching = ignore_matching_by_name ++ ignore_matching_by_tags
    184 
    185     result = only_matching -- ignore_matching
    186 
    187     {result, only_matching, ignore_matching}
    188   end
    189 
    190   defp filter_only_checks(checks, nil), do: checks
    191   defp filter_only_checks(checks, []), do: checks
    192   defp filter_only_checks(checks, patterns), do: filter_checks(checks, patterns)
    193 
    194   defp filter_ignore_checks(_checks, nil), do: []
    195   defp filter_ignore_checks(_checks, []), do: []
    196   defp filter_ignore_checks(checks, patterns), do: filter_checks(checks, patterns)
    197 
    198   defp filter_checks(checks, patterns) do
    199     regexes =
    200       patterns
    201       |> List.wrap()
    202       |> to_match_regexes
    203 
    204     Enum.filter(checks, &match_regex(&1, regexes, true))
    205   end
    206 
    207   defp match_regex(_tuple, [], default_for_empty), do: default_for_empty
    208 
    209   defp match_regex(tuple, regexes, _default_for_empty) do
    210     check_name =
    211       tuple
    212       |> Tuple.to_list()
    213       |> List.first()
    214       |> to_string
    215 
    216     Enum.any?(regexes, &Regex.run(&1, check_name))
    217   end
    218 
    219   defp to_match_regexes(list) do
    220     Enum.map(list, fn match_check ->
    221       {:ok, match_pattern} = Regex.compile(match_check, "i")
    222       match_pattern
    223     end)
    224   end
    225 
    226   defp filter_only_checks_by_tags(checks, nil), do: checks
    227   defp filter_only_checks_by_tags(checks, []), do: checks
    228   defp filter_only_checks_by_tags(checks, tags), do: filter_checks_by_tags(checks, tags)
    229 
    230   defp filter_ignore_checks_by_tags(_checks, nil), do: []
    231   defp filter_ignore_checks_by_tags(_checks, []), do: []
    232   defp filter_ignore_checks_by_tags(checks, tags), do: filter_checks_by_tags(checks, tags)
    233 
    234   defp filter_checks_by_tags(_checks, nil), do: []
    235   defp filter_checks_by_tags(_checks, []), do: []
    236 
    237   defp filter_checks_by_tags(checks, tags) do
    238     tags = Enum.map(tags, &String.to_atom/1)
    239 
    240     Enum.filter(checks, &match_tags(&1, tags, true))
    241   end
    242 
    243   defp match_tags(_tuple, [], default_for_empty), do: default_for_empty
    244 
    245   defp match_tags({check, params}, tags, _default_for_empty) do
    246     tags_for_check = tags_for_check(check, params)
    247 
    248     Enum.any?(tags, &Enum.member?(tags_for_check, &1))
    249   end
    250 
    251   @doc """
    252   Returns the tags for a given `check` and its `params`.
    253   """
    254   def tags_for_check(check, params)
    255 
    256   def tags_for_check(check, nil), do: check.tags
    257   def tags_for_check(check, []), do: check.tags
    258 
    259   def tags_for_check(check, params) when is_list(params) do
    260     params
    261     |> Credo.Check.Params.tags(check)
    262     |> Enum.flat_map(fn
    263       :__initial__ -> check.tags
    264       tag -> [tag]
    265     end)
    266   end
    267 
    268   @doc """
    269   Sets the exec values which `strict` implies (if applicable).
    270   """
    271   def set_strict(exec)
    272 
    273   def set_strict(%__MODULE__{strict: true} = exec) do
    274     %__MODULE__{exec | all: true, min_priority: -99}
    275   end
    276 
    277   def set_strict(%__MODULE__{strict: false} = exec) do
    278     %__MODULE__{exec | min_priority: 0}
    279   end
    280 
    281   def set_strict(exec), do: exec
    282 
    283   @doc false
    284   @deprecated "Use `Execution.working_dir/1` instead"
    285   def get_path(exec) do
    286     exec.cli_options.path
    287   end
    288 
    289   @doc false
    290   def working_dir(exec) do
    291     Path.expand(exec.cli_options.path)
    292   end
    293 
    294   # Commands
    295 
    296   @doc """
    297   Returns the name of the command, which should be run by the given execution.
    298 
    299       Credo.Execution.get_command_name(exec)
    300       # => "suggest"
    301   """
    302   def get_command_name(exec) do
    303     exec.cli_options.command
    304   end
    305 
    306   @doc """
    307   Returns all valid command names.
    308 
    309       Credo.Execution.get_valid_command_names(exec)
    310       # => ["categories", "diff", "explain", "gen.check", "gen.config", "help", "info",
    311       #     "list", "suggest", "version"]
    312   """
    313   def get_valid_command_names(exec) do
    314     Map.keys(exec.commands)
    315   end
    316 
    317   @doc """
    318   Returns the `Credo.CLI.Command` module for the given `name`.
    319 
    320       Credo.Execution.get_command(exec, "explain")
    321       # => Credo.CLI.Command.Explain.ExplainCommand
    322   """
    323   def get_command(exec, name) do
    324     Map.get(exec.commands, name) ||
    325       raise ~s'Command not found: "#{inspect(name)}"\n\nRegistered commands: #{inspect(exec.commands, pretty: true)}'
    326   end
    327 
    328   @doc false
    329   def put_command(exec, _plugin_mod, name, command_mod) do
    330     commands = Map.put(exec.commands, name, command_mod)
    331 
    332     %__MODULE__{exec | commands: commands}
    333     |> command_mod.init()
    334   end
    335 
    336   @doc false
    337   def set_initializing_plugin(%__MODULE__{initializing_plugin: nil} = exec, plugin_mod) do
    338     %__MODULE__{exec | initializing_plugin: plugin_mod}
    339   end
    340 
    341   def set_initializing_plugin(exec, nil) do
    342     %__MODULE__{exec | initializing_plugin: nil}
    343   end
    344 
    345   def set_initializing_plugin(%__MODULE__{initializing_plugin: mod1}, mod2) do
    346     raise "Attempting to initialize plugin #{inspect(mod2)}, " <>
    347             "while already initializing plugin #{mod1}"
    348   end
    349 
    350   # Plugin params
    351 
    352   @doc """
    353   Returns the `Credo.Plugin` module's param value.
    354 
    355       Credo.Execution.get_command(exec, CredoDemoPlugin, "foo")
    356       # => nil
    357 
    358       Credo.Execution.get_command(exec, CredoDemoPlugin, "foo", 42)
    359       # => 42
    360   """
    361   def get_plugin_param(exec, plugin_mod, param_name) do
    362     exec.plugins[plugin_mod][param_name]
    363   end
    364 
    365   @doc false
    366   def put_plugin_param(exec, plugin_mod, param_name, param_value) do
    367     plugins =
    368       Keyword.update(exec.plugins, plugin_mod, [], fn list ->
    369         Keyword.update(list, param_name, param_value, fn _ -> param_value end)
    370       end)
    371 
    372     %__MODULE__{exec | plugins: plugins}
    373   end
    374 
    375   # CLI switches
    376 
    377   @doc """
    378   Returns the value for the given `switch_name`.
    379 
    380       Credo.Execution.get_given_cli_switch(exec, "foo")
    381       # => "bar"
    382   """
    383   def get_given_cli_switch(exec, switch_name) do
    384     if Map.has_key?(exec.cli_options.switches, switch_name) do
    385       {:ok, exec.cli_options.switches[switch_name]}
    386     else
    387       :error
    388     end
    389   end
    390 
    391   @doc false
    392   def put_cli_switch(exec, _plugin_mod, name, type) do
    393     %__MODULE__{exec | cli_switches: exec.cli_switches ++ [{name, type}]}
    394   end
    395 
    396   @doc false
    397   def put_cli_switch_alias(exec, _plugin_mod, _name, nil), do: exec
    398 
    399   def put_cli_switch_alias(exec, _plugin_mod, name, alias_name) do
    400     %__MODULE__{exec | cli_aliases: exec.cli_aliases ++ [{alias_name, name}]}
    401   end
    402 
    403   @doc false
    404   def put_cli_switch_plugin_param_converter(exec, plugin_mod, cli_switch_name, plugin_param_name) do
    405     converter_tuple = {cli_switch_name, plugin_mod, plugin_param_name}
    406 
    407     %__MODULE__{
    408       exec
    409       | cli_switch_plugin_param_converters:
    410           exec.cli_switch_plugin_param_converters ++ [converter_tuple]
    411     }
    412   end
    413 
    414   # Assigns
    415 
    416   @doc """
    417   Returns the assign with the given `name` for the given `exec` struct (or return the given `default` value).
    418 
    419       Credo.Execution.get_assign(exec, "foo")
    420       # => nil
    421 
    422       Credo.Execution.get_assign(exec, "foo", 42)
    423       # => 42
    424   """
    425   def get_assign(exec, name_or_list, default \\ nil)
    426 
    427   def get_assign(exec, path, default) when is_list(path) do
    428     case get_in(exec.assigns, path) do
    429       nil -> default
    430       value -> value
    431     end
    432   end
    433 
    434   def get_assign(exec, name, default) do
    435     Map.get(exec.assigns, name, default)
    436   end
    437 
    438   @doc """
    439   Puts the given `value` with the given `name` as assign into the given `exec` struct and returns the struct.
    440 
    441       Credo.Execution.put_assign(exec, "foo", 42)
    442       # => %Credo.Execution{...}
    443   """
    444   def put_assign(exec, name_or_list, value)
    445 
    446   def put_assign(exec, path, value) when is_list(path) do
    447     %__MODULE__{exec | assigns: do_put_nested_assign(exec.assigns, path, value)}
    448   end
    449 
    450   def put_assign(exec, name, value) do
    451     %__MODULE__{exec | assigns: Map.put(exec.assigns, name, value)}
    452   end
    453 
    454   defp do_put_nested_assign(map, [last_key], value) do
    455     Map.put(map, last_key, value)
    456   end
    457 
    458   defp do_put_nested_assign(map, [next_key | rest], value) do
    459     new_map =
    460       map
    461       |> Map.get(next_key, %{})
    462       |> do_put_nested_assign(rest, value)
    463 
    464     Map.put(map, next_key, new_map)
    465   end
    466 
    467   # Config Files
    468 
    469   @doc false
    470   def get_config_files(exec) do
    471     Credo.Execution.ExecutionConfigFiles.get(exec)
    472   end
    473 
    474   @doc false
    475   def append_config_file(exec, {_, _, _} = config_file) do
    476     config_files = get_config_files(exec) ++ [config_file]
    477 
    478     ExecutionConfigFiles.put(exec, config_files)
    479 
    480     exec
    481   end
    482 
    483   # Source Files
    484 
    485   @doc """
    486   Returns all source files for the given `exec` struct.
    487 
    488       Credo.Execution.get_source_files(exec)
    489       # => [%SourceFile<lib/my_project.ex>,
    490       #     %SourceFile<lib/credo/my_project/foo.ex>]
    491   """
    492   def get_source_files(exec) do
    493     Credo.Execution.ExecutionSourceFiles.get(exec)
    494   end
    495 
    496   @doc false
    497   def put_source_files(exec, source_files) do
    498     ExecutionSourceFiles.put(exec, source_files)
    499 
    500     exec
    501   end
    502 
    503   # Issues
    504 
    505   @doc """
    506   Returns all issues for the given `exec` struct.
    507   """
    508   def get_issues(exec) do
    509     exec
    510     |> ExecutionIssues.to_map()
    511     |> Map.values()
    512     |> List.flatten()
    513   end
    514 
    515   @doc """
    516   Returns all issues grouped by filename for the given `exec` struct.
    517   """
    518   def get_issues_grouped_by_filename(exec) do
    519     ExecutionIssues.to_map(exec)
    520   end
    521 
    522   @doc """
    523   Returns all issues for the given `exec` struct that relate to the given `filename`.
    524   """
    525   def get_issues(exec, filename) do
    526     exec
    527     |> ExecutionIssues.to_map()
    528     |> Map.get(filename, [])
    529   end
    530 
    531   @doc """
    532   Sets the issues for the given `exec` struct, overwriting any existing issues.
    533   """
    534   def put_issues(exec, issues) do
    535     ExecutionIssues.set(exec, issues)
    536 
    537     exec
    538   end
    539 
    540   @doc false
    541   @deprecated "Use put_issues/2 instead"
    542   def set_issues(exec, issues) do
    543     put_issues(exec, issues)
    544   end
    545 
    546   # Results
    547 
    548   @doc """
    549   Returns the result with the given `name` for the given `exec` struct (or return the given `default` value).
    550 
    551       Credo.Execution.get_result(exec, "foo")
    552       # => nil
    553 
    554       Credo.Execution.get_result(exec, "foo", 42)
    555       # => 42
    556   """
    557   def get_result(exec, name, default \\ nil) do
    558     Map.get(exec.results, name, default)
    559   end
    560 
    561   @doc """
    562   Puts the given `value` with the given `name` as result into the given `exec` struct.
    563 
    564       Credo.Execution.put_result(exec, "foo", 42)
    565       # => %Credo.Execution{...}
    566   """
    567   def put_result(exec, name, value) do
    568     %__MODULE__{exec | results: Map.put(exec.results, name, value)}
    569   end
    570 
    571   @doc false
    572   def put_exit_status(exec, exit_status) do
    573     put_assign(exec, "credo.exit_status", exit_status)
    574   end
    575 
    576   @doc false
    577   def get_exit_status(exec) do
    578     get_assign(exec, "credo.exit_status", 0)
    579   end
    580 
    581   # Halt
    582 
    583   @doc """
    584   Halts further execution of the pipeline meaning all subsequent steps are skipped.
    585 
    586   The `error` callback is called for the current Task.
    587 
    588       defmodule FooTask do
    589         use Credo.Execution.Task
    590 
    591         def call(exec) do
    592           Execution.halt(exec)
    593         end
    594 
    595         def error(exec) do
    596           IO.puts("Execution has been halted!")
    597 
    598           exec
    599         end
    600       end
    601   """
    602   def halt(exec) do
    603     %__MODULE__{exec | halted: true}
    604   end
    605 
    606   @doc """
    607   Halts further execution of the pipeline using the given `halt_message`.
    608 
    609   The `error` callback is called for the current Task.
    610   If the callback is not implemented, Credo outputs the given `halt_message`.
    611 
    612       defmodule FooTask do
    613         use Credo.Execution.Task
    614 
    615         def call(exec) do
    616           Execution.halt(exec, "Execution has been halted!")
    617         end
    618       end
    619   """
    620   def halt(exec, halt_message) do
    621     %__MODULE__{exec | halted: true}
    622     |> put_halt_message(halt_message)
    623   end
    624 
    625   @doc false
    626   def get_halt_message(exec) do
    627     get_assign(exec, "credo.halt_message")
    628   end
    629 
    630   @doc false
    631   def put_halt_message(exec, halt_message) do
    632     put_assign(exec, "credo.halt_message", halt_message)
    633   end
    634 
    635   # Task tracking
    636 
    637   @doc false
    638   def set_parent_and_current_task(exec, parent_task, current_task) do
    639     %__MODULE__{exec | parent_task: parent_task, current_task: current_task}
    640   end
    641 
    642   # Running tasks
    643 
    644   @doc false
    645   def run(exec) do
    646     run_pipeline(exec, __MODULE__)
    647   end
    648 
    649   # Pipelines
    650 
    651   @doc false
    652   defp get_pipeline(exec, pipeline_key) do
    653     case exec.pipeline_map[get_pipeline_key(exec, pipeline_key)] do
    654       nil -> raise "Could not find execution pipeline for '#{pipeline_key}'"
    655       pipeline -> pipeline
    656     end
    657   end
    658 
    659   @doc false
    660   defp get_pipeline_key(exec, pipeline_key) do
    661     case exec.pipeline_map[pipeline_key] do
    662       nil -> @execution_pipeline_key_backwards_compatibility_map[pipeline_key]
    663       _ -> pipeline_key
    664     end
    665   end
    666 
    667   @doc """
    668   Puts a given `pipeline` in `exec` under `pipeline_key`.
    669 
    670   A pipeline is a keyword list of named groups. Each named group is a list of `Credo.Execution.Task` modules:
    671 
    672       Execution.put_pipeline(exec, :my_pipeline_key,
    673         load_things: [ MyProject.LoadThings ],
    674         run_analysis: [ MyProject.Run ],
    675         print_results: [ MyProject.PrintResults ]
    676       )
    677 
    678   A named group can also be a list of two-element tuples, consisting of a `Credo.Execution.Task` module and a
    679   keyword list of options, which are passed to the Task module's `call/2` function:
    680 
    681       Execution.put_pipeline(exec, :my_pipeline_key,
    682         load_things: [ {MyProject.LoadThings, []} ],
    683         run_analysis: [ {MyProject.Run, [foo: "bar"]} ],
    684         print_results: [ {MyProject.PrintResults, []} ]
    685       )
    686   """
    687   def put_pipeline(exec, pipeline_key, pipeline) do
    688     new_pipelines = Map.put(exec.pipeline_map, pipeline_key, pipeline)
    689 
    690     %__MODULE__{exec | pipeline_map: new_pipelines}
    691   end
    692 
    693   @doc """
    694   Runs the pipeline with the given `pipeline_key` and returns the result `Credo.Execution` struct.
    695 
    696       Execution.run_pipeline(exec, :my_pipeline_key)
    697       # => %Credo.Execution{...}
    698   """
    699   def run_pipeline(%__MODULE__{} = initial_exec, pipeline_key)
    700       when is_atom(pipeline_key) and not is_nil(pipeline_key) do
    701     initial_pipeline = get_pipeline(initial_exec, pipeline_key)
    702 
    703     Enum.reduce(initial_pipeline, initial_exec, fn {group_name, _list}, exec_inside_pipeline ->
    704       outer_pipeline = get_pipeline(exec_inside_pipeline, pipeline_key)
    705 
    706       task_group = outer_pipeline[group_name]
    707 
    708       Enum.reduce(task_group, exec_inside_pipeline, fn
    709         {task_mod, opts}, exec_inside_task_group ->
    710           Credo.Execution.Task.run(task_mod, exec_inside_task_group, opts)
    711 
    712         task_mod, exec_inside_task_group when is_atom(task_mod) ->
    713           Credo.Execution.Task.run(task_mod, exec_inside_task_group, [])
    714       end)
    715     end)
    716   end
    717 
    718   @doc false
    719   def prepend_task(exec, plugin_mod, pipeline_key, group_name, task_tuple)
    720 
    721   def prepend_task(exec, plugin_mod, nil, group_name, task_tuple) do
    722     prepend_task(exec, plugin_mod, @execution_pipeline_key, group_name, task_tuple)
    723   end
    724 
    725   def prepend_task(exec, plugin_mod, pipeline_key, group_name, task_mod) when is_atom(task_mod) do
    726     prepend_task(exec, plugin_mod, pipeline_key, group_name, {task_mod, []})
    727   end
    728 
    729   def prepend_task(exec, _plugin_mod, pipeline_key, group_name, task_tuple) do
    730     pipeline =
    731       exec
    732       |> get_pipeline(pipeline_key)
    733       |> Enum.map(fn
    734         {^group_name, list} -> {group_name, [task_tuple] ++ list}
    735         value -> value
    736       end)
    737 
    738     put_pipeline(exec, get_pipeline_key(exec, pipeline_key), pipeline)
    739   end
    740 
    741   @doc false
    742   def append_task(exec, plugin_mod, pipeline_key, group_name, task_tuple)
    743 
    744   def append_task(exec, plugin_mod, nil, group_name, task_tuple) do
    745     append_task(exec, plugin_mod, __MODULE__, group_name, task_tuple)
    746   end
    747 
    748   def append_task(exec, plugin_mod, pipeline_key, group_name, task_mod) when is_atom(task_mod) do
    749     append_task(exec, plugin_mod, pipeline_key, group_name, {task_mod, []})
    750   end
    751 
    752   def append_task(exec, _plugin_mod, pipeline_key, group_name, task_tuple) do
    753     pipeline =
    754       exec
    755       |> get_pipeline(pipeline_key)
    756       |> Enum.map(fn
    757         {^group_name, list} -> {group_name, list ++ [task_tuple]}
    758         value -> value
    759       end)
    760 
    761     put_pipeline(exec, get_pipeline_key(exec, pipeline_key), pipeline)
    762   end
    763 
    764   @doc false
    765   defp put_builtin_command(exec, name, command_mod) do
    766     exec
    767     |> command_mod.init()
    768     |> put_command(Credo, name, command_mod)
    769   end
    770 
    771   @doc ~S"""
    772   Ensures that the given `value` is a `%Credo.Execution{}` struct, raises an error otherwise.
    773 
    774   Example:
    775 
    776       exec
    777       |> mod.init()
    778       |> Credo.Execution.ensure_execution_struct("#{mod}.init/1")
    779   """
    780   def ensure_execution_struct(value, fun_name)
    781 
    782   def ensure_execution_struct(%__MODULE__{} = exec, _fun_name), do: exec
    783 
    784   def ensure_execution_struct(value, fun_name) do
    785     raise("Expected #{fun_name} to return %Credo.Execution{}, got: #{inspect(value)}")
    786   end
    787 
    788   @doc false
    789   def get_rerun(exec) do
    790     case get_assign(exec, "credo.rerun.previous_execution") do
    791       nil -> :notfound
    792       previous_exec -> {previous_exec, get_assign(exec, "credo.rerun.files_that_changed")}
    793     end
    794   end
    795 
    796   defp put_rerun(exec, previous_exec, files_that_changed) do
    797     exec
    798     |> put_assign("credo.rerun.previous_execution", previous_exec)
    799     |> put_assign(
    800       "credo.rerun.files_that_changed",
    801       Enum.map(files_that_changed, fn filename ->
    802         filename
    803         |> Path.expand()
    804         |> Path.relative_to_cwd()
    805       end)
    806     )
    807   end
    808 end