Repository: hrzndhrn/recode
Pull request #75 (2023-10-07 08:04:23)

Run tasks async


Patch:
diff --git a/.dialyzer_ignore.exs b/.dialyzer_ignore.exs
index 9ead283..1c9e005 100644
--- a/.dialyzer_ignore.exs
+++ b/.dialyzer_ignore.exs
@@ -1,4 +1,5 @@
 [
-  ~r|lib/recode/cli_formatter.ex:.*:no_return|
+  ~r|lib/recode/cli_formatter.ex:.*:no_return|,
+  ~r|lib/recode/cli_formatter.ex:.*:unused_fun|
 ]
 
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 11c2f13..c9c3467 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,8 @@
 ## 0.7.0 - dev
 
 + Refactor formatter.
++ Run recode tasks async.
++ Remove `Recode.StopWatch`
 
 ## 0.6.5 - 2023/10/09
 
diff --git a/examples/my_code/.recode.exs b/examples/my_code/.recode.exs
index 1803c4d..ac67c23 100644
--- a/examples/my_code/.recode.exs
+++ b/examples/my_code/.recode.exs
@@ -1,5 +1,5 @@
 [
-  version: "0.6.0",
+  version: "0.7.0",
   # Can also be set/reset with `--autocorrect`/`--no-autocorrect`.
   autocorrect: true,
   # With "--dry" no changes will be written to the files.
@@ -17,17 +17,17 @@
     # a deactivated task can be forced by calling `mix recode --task ModuleName`.
     {Recode.Task.AliasExpansion, []},
     {Recode.Task.AliasOrder, []},
-    {Recode.Task.Dbg, autocorrect: false},
-    {Recode.Task.EnforceLineLength, active: true, exclude: "mix.exs"},
+    {Recode.Task.Dbg, [autocorrect: false]},
+    {Recode.Task.EnforceLineLength, [active: true, exclude: "mix.exs"]},
     {Recode.Task.FilterCount, []},
-    {Recode.Task.IOInspect, autocorrect: false},
+    {Recode.Task.IOInspect, [autocorrect: false]},
     {Recode.Task.Nesting, []},
     {Recode.Task.PipeFunOne, []},
     {Recode.Task.SinglePipe, []},
-    {Recode.Task.Specs, exclude: "test/**/*.{ex,exs}", config: [only: :visible]},
-    {Recode.Task.TagFIXME, exit_code: 2},
-    {Recode.Task.TagTODO, exit_code: 4},
+    {Recode.Task.Specs, [exclude: "test/**/*.{ex,exs}", config: [only: :visible]]},
+    {Recode.Task.TagFIXME, [exit_code: 2]},
+    {Recode.Task.TagTODO, [exit_code: 4]},
     {Recode.Task.TestFileExt, []},
-    {Recode.Task.UnusedVariable, active: false}
+    {Recode.Task.UnusedVariable, [active: false]}
   ]
 ]
diff --git a/examples/my_code/mix.exs b/examples/my_code/mix.exs
index 85eaec3..4c7efc1 100644
--- a/examples/my_code/mix.exs
+++ b/examples/my_code/mix.exs
@@ -14,7 +14,8 @@ defmodule MyCode.MixProject do
 
   def application do
     [
-      extra_applications: [:logger]
+      extra_applications: [:logger],
+      mod: {Recode.Application, []}
     ]
   end
 
diff --git a/lib/mix/tasks/recode.ex b/lib/mix/tasks/recode.ex
index 4211e0d..b73bb32 100644
--- a/lib/mix/tasks/recode.ex
+++ b/lib/mix/tasks/recode.ex
@@ -33,10 +33,11 @@ defmodule Mix.Tasks.Recode do
     * `-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.
   """
 
   use Mix.Task
-  use Recode.StopWatch
 
   alias Recode.Config
   alias Recode.Runner
@@ -47,7 +48,8 @@ defmodule Mix.Tasks.Recode do
           debug: :boolean,
           dry: :boolean,
           task: :keep,
-          verbose: :boolean
+          verbose: :boolean,
+          slowest_tasks: :integer
         ],
         aliases: [
           a: :autocorrect,
@@ -62,8 +64,6 @@ defmodule Mix.Tasks.Recode do
   def run(opts) do
     {:ok, _apps} = Application.ensure_all_started(:recode)
 
-    _stop_watch = StopWatch.init(start: :recode)
-
     opts = opts!(opts)
 
     opts =
@@ -73,7 +73,9 @@ defmodule Mix.Tasks.Recode do
       |> validate_config!()
       |> validate_tasks!()
       |> update_task_configs!()
-      |> Keyword.merge(Keyword.take(opts, [:verbose, :autocorrect, :dry, :inputs]))
+      |> Keyword.merge(
+        Keyword.take(opts, [:verbose, :autocorrect, :dry, :inputs, :slowest_tasks])
+      )
       |> Keyword.put(:cli_opts, acc_tasks(opts))
       |> update(:verbose)
       |> put_debug(opts)
diff --git a/lib/mix/tasks/recode.update.config.ex b/lib/mix/tasks/recode.update.config.ex
index f6e5332..a043e6b 100644
--- a/lib/mix/tasks/recode.update.config.ex
+++ b/lib/mix/tasks/recode.update.config.ex
@@ -16,6 +16,7 @@ defmodule Mix.Tasks.Recode.Update.Config do
   use Mix.Task
 
   @config_filename ".recode.exs"
+  @deprecated_configs [:formatter]
 
   @doc false
   def run([]), do: do_run(false)
@@ -28,6 +29,7 @@ defmodule Mix.Tasks.Recode.Update.Config do
         @config_filename
         |> Code.eval_file()
         |> elem(0)
+        |> delete(@deprecated_configs)
         |> Recode.Config.merge()
 
       Mix.Generator.create_file(@config_filename, Recode.Config.to_string(config), force: force)
@@ -35,4 +37,8 @@ defmodule Mix.Tasks.Recode.Update.Config do
       Mix.raise(~s|config file #{@config_filename} not found, run "mix recode.gen.config"|)
     end
   end
+
+  defp delete(config, deprecated_configs) do
+    Enum.reduce(deprecated_configs, config, fn key, config -> Keyword.delete(config, key) end)
+  end
 end
diff --git a/lib/recode/application.ex b/lib/recode/application.ex
new file mode 100644
index 0000000..c13ce6d
--- /dev/null
+++ b/lib/recode/application.ex
@@ -0,0 +1,14 @@
+defmodule Recode.Application do
+  @moduledoc false
+
+  use Application
+
+  @impl true
+  def start(_type, _args) do
+    children = [{Task.Supervisor, name: Recode.TaskSupervisor, max_restarts: 10}]
+
+    opts = [strategy: :one_for_one, name: Recode.Supervisor]
+
+    Supervisor.start_link(children, opts)
+  end
+end
diff --git a/lib/recode/cli_formatter.ex b/lib/recode/cli_formatter.ex
index 1cc75da..58d5bfb 100644
--- a/lib/recode/cli_formatter.ex
+++ b/lib/recode/cli_formatter.ex
@@ -17,10 +17,10 @@ defmodule Recode.CLIFormatter do
         :ok
 
       1 ->
-        puts([:info, "Found 1 file"])
+        puts([:info, "Read 1 file"])
 
       count ->
-        puts([:info, "Found #{count} files"])
+        puts([:info, "Read #{count} files in #{format_time(time)}s"])
     end
 
     {:noreply, config}
@@ -28,7 +28,7 @@ defmodule Recode.CLIFormatter do
 
   def handle_cast({:finished, %Rewrite{} = project, time}, config) when is_integer(time) do
     unless Enum.empty?(project) do
-      puts([:info, "Finished in #{format_time(time)}"])
+      puts([:info, "Finished in #{format_time(time)}s."])
     end
 
     {:noreply, config}
@@ -42,9 +42,9 @@ defmodule Recode.CLIFormatter do
     {:noreply, config}
   end
 
-  def handle_cast({:task_finished, %Source{} = source, task}, config) when is_atom(task) do
+  def handle_cast({:task_finished, %Source{} = source, task, time}, config) when is_atom(task) do
     if config[:debug] do
-      puts([:aqua, "Finished #{task} with #{source.path}."])
+      puts([:aqua, "Finished #{task} with #{source.path} [#{time}μs]."])
     else
       cond do
         issue?(source, task) -> write([:warn, "!"])
@@ -53,65 +53,76 @@ defmodule Recode.CLIFormatter do
       end
     end
 
+    config =
+      Keyword.update(config, :times, [{task, source.path, time}], fn times ->
+        [{task, source.path, time} | times]
+      end)
+
     {:noreply, config}
   end
 
-  def handle_cast({:tasks_finished, %Rewrite{} = project}, config) do
+  def handle_cast({:tasks_finished, %Rewrite{} = project, time}, config) do
     unless Enum.empty?(project) do
       puts(["\n"])
-      format_results(project, config)
+      stats = format_results(project, config)
+      :ok = format_tasks_stats(config, time)
+      :ok = format_slowest_tasks(config[:slowest_tasks], config)
+      :ok = format_stats(project, stats)
+      :ok = format_ok(stats)
     end
 
     {:noreply, config}
   end
 
+  defp format_tasks_stats(config, time) do
+    executions = length(Keyword.get(config, :times, []))
+
+    puts([:info, "Executed #{executions} tasks in #{format_time(time)}s."])
+  end
+
   defp format_results(project, config) do
     verbose = Keyword.fetch!(config, :verbose)
 
     stats = %{extname_count: %{}, issues: 0, updated: 0, created: 0, moved: 0}
 
-    stats =
-      project
-      |> Rewrite.sources()
-      |> Enum.reduce(stats, fn source, stats ->
-        opts = [
-          issues?: Source.has_issues?(source, :all),
-          code_updated?: Source.updated?(source, :content),
-          path_updated?: Source.updated?(source, :path),
-          created?: Source.from?(source, :string)
-        ]
-
-        format_result(source, verbose, opts)
-
-        stats
-        |> stats_count(:issues, opts[:issues?], Enum.count(source.issues))
-        |> stats_count(:updated, opts[:code_updated?])
-        |> stats_count(:moved, opts[:path_updated?])
-        |> stats_count(:created, opts[:created?])
-        |> Map.update!(:extname_count, fn extname_count ->
-          extname = Path.extname(source.path)
-          Map.update(extname_count, extname, 1, fn count -> count + 1 end)
-        end)
-      end)
-
     project
-    |> Enum.count()
-    |> format_stats(stats)
+    |> Rewrite.sources()
+    |> Enum.reduce(stats, fn source, stats ->
+      opts = [
+        issues?: Source.has_issues?(source, :all),
+        code_updated?: Source.updated?(source, :content),
+        path_updated?: Source.updated?(source, :path),
+        created?: Source.from?(source, :string)
+      ]
+
+      format_result(source, verbose, opts)
+
+      stats
+      |> stats_count(:issues, opts[:issues?], Enum.count(source.issues))
+      |> stats_count(:updated, opts[:code_updated?])
+      |> stats_count(:moved, opts[:path_updated?])
+      |> stats_count(:created, opts[:created?])
+      |> Map.update!(:extname_count, fn extname_count ->
+        extname = Path.extname(source.path)
+        Map.update(extname_count, extname, 1, fn count -> count + 1 end)
+      end)
+    end)
   end
 
-  defp format_stats(file_count, stats) do
+  defp format_stats(project, stats) do
     filte_stats =
       stats
       |> Map.get(:extname_count)
       |> Enum.map_join(", ", fn {extname, count} -> "#{extname}: #{count}" end)
 
-    puts([:info, "Files: #{file_count} ", "(#{filte_stats})"])
+    puts([:info, "Files: #{Enum.count(project)} ", "(#{filte_stats})"])
 
     stats
     |> format_stat(:created, :info, ["Created # file", "Created # files"])
     |> format_stat(:moved, :info, ["Moved # file", "Moved # files"])
     |> format_stat(:updated, :info, ["Updated # file", "Updated # files"])
-    |> format_ok()
+
+    :ok
   end
 
   defp format_ok(%{issues: issues}) do
@@ -253,11 +264,11 @@ defmodule Recode.CLIFormatter do
          _actual,
          true
        ) do
-    [:warn, "Execution of the #{inspect(meta[:task])} task failed with error:\n#{message}\n"]
+    [:error, "Execution of the #{inspect(meta[:task])} task failed with error:\n#{message}\n"]
   end
 
   defp format_issue(%{reporter: Recode.Runner, meta: meta}, _version, _actual, false) do
-    [:warn, "Execution of the #{inspect(meta[:task])} task failed.\n"]
+    [:error, "Execution of the #{inspect(meta[:task])} task failed.\n"]
   end
 
   defp format_issue(issue, version, actual, _verbose) do
@@ -280,6 +291,31 @@ defmodule Recode.CLIFormatter do
     Enum.concat(warn, message)
   end
 
+  defp format_slowest_tasks(nil, _config), do: :ok
+
+  defp format_slowest_tasks(amount, config) do
+    slowest_tasks =
+      config
+      |> Keyword.get(:times, [])
+      |> Enum.reduce(%{}, fn {task, _path, time}, acc ->
+        Map.update(acc, task, {1, time}, fn {calls, duration} -> {calls + 1, duration + time} end)
+      end)
+      |> Enum.into([], fn {task, {calls, time}} -> {task, calls, round(time / calls)} end)
+      |> Enum.sort_by(&elem(&1, 2), :desc)
+      |> Enum.take(amount)
+      |> Enum.with_index(1)
+      |> Enum.map(fn {{task, calls, time}, index} ->
+        """
+        #{index} - \
+        task: #{inspect(task)}, \
+        calls: #{calls}, \
+        avg: #{format_time(time, :millisecond)}ms
+        """
+      end)
+
+    puts([:info, "\nSlowest tasks:\n", slowest_tasks])
+  end
+
   defp changed_by(%Source{history: history}) do
     by = Enum.map(history, fn {_key, by, _value} -> module(by) end)
 
diff --git a/lib/recode/config.ex b/lib/recode/config.ex
index 63e2628..bfb4e41 100644
--- a/lib/recode/config.ex
+++ b/lib/recode/config.ex
@@ -9,11 +9,11 @@ defmodule Recode.Config do
 
   @config_filename ".recode.exs"
 
-  @config_version "0.6.5"
+  @config_version "0.7.0"
 
   # The minimum version of the config to run recode. This version marks the last
   # breaking change for handle the config.
-  @config_min_version "0.6.0"
+  @config_min_version "0.7.0"
 
   @config_keys [:version, :autocorrect, :dry, :verbose, :inputs, :formatters, :tasks]
 
@@ -70,7 +70,7 @@ defmodule Recode.Config do
       verbose: <%= @config[:verbose] %>,
       # Can be overwritten by calling `mix recode "lib/**/*.ex"`.
       inputs: <%= inspect @config[:inputs] %>,
-      formatter: <%= inspect @config[:formatter] %>,
+      formatters: <%= inspect @config[:formatters] %>,
       tasks: [
         # Tasks could be added by a tuple of the tasks module name and an options
         # keyword list. A task can be deactivated by `active: false`. The execution of
diff --git a/lib/recode/formatter.ex b/lib/recode/formatter.ex
index 38a5c19..f7aad3a 100644
--- a/lib/recode/formatter.ex
+++ b/lib/recode/formatter.ex
@@ -13,10 +13,10 @@ defmodule Recode.Formatter do
   * `{:task_started, %Rewrite.Source{} = source, task}` -
     a task has started.
 
-  * `{:task_finished, %Rewrite.Source{} = source, task}` .
+  * `{:task_finished, %Rewrite.Source{} = source, task, time}` .
     a task has finished.
 
-  * `{:tasks_finished, %Rewrite{} = project}` -
+  * `{:tasks_finished, %Rewrite{} = project, time}` -
     all tasks are finished.
 
   * `{:finished, %Rewrite{} = project, time}` -
@@ -29,7 +29,7 @@ defmodule Recode.Formatter do
   """
 
   @doc """
-  Formats the given `microseconds` to a string.
+  Formats the given `microseconds` to a string representing the time in seconds.
 
   ## Examples
 
@@ -44,8 +44,33 @@ defmodule Recode.Formatter do
       iex> Recode.Formatter.format_time(12345678)
       "12.3"
   """
-  @spec format_time(microseconds :: integer) :: String.t()
-  def format_time(microseconds) do
+  @spec format_time(integer) :: String.t()
+  def format_time(microseconds), do: format_time(microseconds, :second)
+
+  @doc """
+  Formats the given `microseconds` to a string representing the time in the
+  given `time_unit`.
+
+  ## Examples
+
+      iex> Recode.Formatter.format_time(1234, :second)
+      "0.00"
+      iex> Recode.Formatter.format_time(12345, :second)
+      "0.01"
+      iex> Recode.Formatter.format_time(123456, :second)
+      "0.1"
+      iex> Recode.Formatter.format_time(1234567, :second)
+      "1.2"
+      iex> Recode.Formatter.format_time(12345678, :second)
+      "12.3"
+
+      iex> Recode.Formatter.format_time(1234, :millisecond)
+      "1.234"
+      iex> Recode.Formatter.format_time(12345, :millisecond)
+      "12.345"
+  """
+  @spec format_time(integer, time_unit :: :second | :millisecond) :: String.t()
+  def format_time(microseconds, :second) do
     time = div(microseconds, 10_000)
 
     if time < 10 do
@@ -55,4 +80,8 @@ defmodule Recode.Formatter do
       "#{div(time, 10)}.#{rem(time, 10)}"
     end
   end
+
+  def format_time(microseconds, :millisecond) do
+    "#{div(microseconds, 1_000)}.#{rem(microseconds, 1_000)}"
+  end
 end
diff --git a/lib/recode/io.ex b/lib/recode/io.ex
index f31cdf8..b05ea0b 100644
--- a/lib/recode/io.ex
+++ b/lib/recode/io.ex
@@ -25,7 +25,8 @@ defmodule Recode.IO do
     debug: :aqua,
     ok: :green,
     updated: :green,
-    correctable: :cyan
+    correctable: :cyan,
+    error: :red
   }
 
   @type chardata :: String.t() | maybe_improper_list(char | chardata, String.t() | [])
diff --git a/lib/recode/runner/impl.ex b/lib/recode/runner/impl.ex
index 46a4512..b23a8d9 100644
--- a/lib/recode/runner/impl.ex
+++ b/lib/recode/runner/impl.ex
@@ -1,17 +1,16 @@
 defmodule Recode.Runner.Impl do
   @moduledoc false
 
-  use Recode.StopWatch
-
   @behaviour Recode.Runner
 
   alias Recode.EventManager
   alias Recode.Issue
-  alias Recode.Task
   alias Rewrite.Source
 
   @impl true
   def run(config) when is_list(config) do
+    start_recode = time()
+
     tasks = tasks(config)
 
     config =
@@ -22,15 +21,17 @@ defmodule Recode.Runner.Impl do
     project =
       config
       |> project()
-      |> notify(:prepared, config, StopWatch.time(:recode))
+      |> notify(:prepared, config, time(start_recode))
+
+    start_tasks = time()
 
     tasks
     |> update_opts(config)
     |> correctors_first()
     |> run_tasks(project, config)
-    |> notify(:tasks_finished, config)
+    |> notify(:tasks_finished, config, time(start_tasks))
     |> tap(fn project -> write(project, config) end)
-    |> notify(:finished, config, StopWatch.time(:recode))
+    |> notify(:finished, config, time(start_recode))
     |> tap(fn _project -> stop_event_manager(config) end)
     |> then(fn project ->
       case Enum.empty?(project) do
@@ -50,6 +51,7 @@ defmodule Recode.Runner.Impl do
 
     tasks
     |> update_opts(config)
+    |> correctors_first()
     |> Enum.reduce(source, fn {module, opts}, source ->
       case exclude?(module, source, config) do
         true -> source
@@ -90,23 +92,57 @@ defmodule Recode.Runner.Impl do
   end
 
   defp run_tasks(tasks, project, config) do
-    Enum.reduce(tasks, project, fn {module, opts}, project ->
-      Rewrite.map!(project, fn source ->
-        run_task(source, config, module, opts)
-      end)
-    end)
+    sources = sources(project)
+    runner = runner(tasks, config)
+
+    sources =
+      Recode.TaskSupervisor
+      |> Task.Supervisor.async_stream(sources, runner, zip_input_on_exit: true)
+      |> Stream.map(&result/1)
+      |> Enum.into(%{})
+
+    %{project | sources: sources}
+  end
+
+  defp sources(%Rewrite{sources: sources}) do
+    Stream.map(sources, fn {_path, source} -> source end)
+  end
+
+  defp result({:ok, source}), do: {source.path, source}
+
+  defp result({:exit, {source, {error, stacktrace}}}) when is_exception(error) do
+    source =
+      Source.add_issue(
+        source,
+        Issue.new(
+          Recode.Runner,
+          task: Recode.Runner,
+          error: error,
+          message: Exception.format(:error, error, stacktrace)
+        )
+      )
+
+    {source.path, source}
+  end
+
+  defp runner(tasks, config) do
+    fn source ->
+      Enum.reduce(tasks, source, fn task, source -> run_task(task, source, config) end)
+    end
   end
 
-  defp run_task(source, config, module, opts) do
-    case exclude?(module, source, config) do
+  defp run_task({task_module, task_config}, source, config) do
+    case exclude?(task_module, source, config) do
       true ->
         source
 
       false ->
+        start = time()
+
         source
-        |> notify(:task_started, config, module)
-        |> module.run(opts)
-        |> notify(:task_finished, config, module)
+        |> notify(:task_started, config, task_module)
+        |> task_module.run(task_config)
+        |> notify(:task_finished, config, {task_module, time(start)})
     end
   rescue
     error ->
@@ -114,7 +150,7 @@ defmodule Recode.Runner.Impl do
         source,
         Issue.new(
           Recode.Runner,
-          task: module,
+          task: task_module,
           error: error,
           message: Exception.format(:error, error, __STACKTRACE__)
         )
@@ -133,11 +169,15 @@ defmodule Recode.Runner.Impl do
     |> Keyword.get(key, default)
   end
 
-  defp notify(data, event, config) when is_atom(event) do
-    notify(data, {event, data}, config)
+  defp notify(data, event, config, {meta, time}) when is_atom(event) do
+    do_notify(data, {event, data, meta, time}, config)
+  end
+
+  defp notify(data, event, config, meta) when is_atom(event) do
+    do_notify(data, {event, data, meta}, config)
   end
 
-  defp notify(data, event, config) do
+  defp do_notify(data, event, config) do
     config
     |> event_manager()
     |> EventManager.notify(event)
@@ -145,10 +185,6 @@ defmodule Recode.Runner.Impl do
     data
   end
 
-  defp notify(data, event, config, time) when is_atom(event) do
-    notify(data, {event, data, time}, config)
-  end
-
   defp event_manager(config), do: Keyword.fetch!(config, :event_manager)
 
   defp project(config) do
@@ -207,14 +243,14 @@ defmodule Recode.Runner.Impl do
 
   defp tasks_autocorrect(tasks, autocorrect) do
     case autocorrect do
-      false -> Enum.filter(tasks, fn {task, _opts} -> Task.checker?(task) end)
+      false -> Enum.filter(tasks, fn {task, _opts} -> Recode.Task.checker?(task) end)
       true -> tasks
     end
   end
 
   defp tasks_check(tasks, false) do
     Enum.reject(tasks, fn {task, _opts} ->
-      Task.checker?(task) && !Task.corrector?(task)
+      Recode.Task.checker?(task) && !Recode.Task.corrector?(task)
     end)
   end
 
@@ -227,7 +263,7 @@ defmodule Recode.Runner.Impl do
   defp correctors_first(tasks) do
     groups =
       Enum.group_by(tasks, fn {task, opts} ->
-        Task.corrector?(task) && Keyword.get(opts, :autocorrect)
+        Recode.Task.corrector?(task) && Keyword.get(opts, :autocorrect)
       end)
 
     Enum.concat(Map.get(groups, true, []), Map.get(groups, false, []))
@@ -293,4 +329,10 @@ defmodule Recode.Runner.Impl do
   defp eof(""), do: ""
 
   defp eof(string), do: String.trim_trailing(string, "\n") <> "\n"
+
+  defp time, do: System.monotonic_time()
+
+  defp time(time) do
+    System.convert_time_unit(System.monotonic_time() - time, :native, :microsecond)
+  end
 end
diff --git a/lib/recode/stop_watch.ex b/lib/recode/stop_watch.ex
deleted file mode 100644
index 6fded91..0000000
--- a/lib/recode/stop_watch.ex
+++ /dev/null
@@ -1,60 +0,0 @@
-defmodule Recode.StopWatch do
-  @moduledoc false
-
-  @name :__stop_watch__
-
-  defmacro __using__(_opts) do
-    quote do
-      require Recode.StopWatch
-      alias Recode.StopWatch
-    end
-  end
-
-  def init(opts \\ []) do
-    name = Keyword.get(opts, :name, @name)
-    ets = :ets.new(Keyword.get(opts, :name, @name), [:set, :public, :named_table])
-
-    if Keyword.has_key?(opts, :start) do
-      _put(name, opts[:start], :start, System.monotonic_time())
-    end
-
-    ets
-  rescue
-    ArgumentError ->
-      reraise ArgumentError, [message: "StopWatch already exisits."], __STACKTRACE__
-  end
-
-  def _put(name, key, op, timestamp) do
-    with false <- :ets.insert_new(name, {{key, op}, timestamp}) do
-      raise ArgumentError,
-            "StopWatch has already received the #{inspect(op)} operation for #{inspect(key)}."
-    end
-  end
-
-  def _time(name \\ @name, key, timestamp) do
-    case :ets.match(name, {{key, :_}, :"$1"}) do
-      [[timestamp1], [timestamp2]] -> abs(timestamp1 - timestamp2) |> convert()
-      [[timestamp1]] -> convert(timestamp - timestamp1)
-    end
-  end
-
-  defp convert(time), do: System.convert_time_unit(time, :native, :microsecond)
-
-  defmacro start(name \\ @name, key) do
-    quote bind_quoted: [name: name, key: key] do
-      Recode.StopWatch._put(name, key, :start, System.monotonic_time())
-    end
-  end
-
-  defmacro stop(name \\ @name, key) do
-    quote bind_quoted: [name: name, key: key] do
-      Recode.StopWatch._put(name, key, :stop, System.monotonic_time())
-    end
-  end
-
-  defmacro time(name \\ @name, key) do
-    quote bind_quoted: [name: name, key: key] do
-      Recode.StopWatch._time(name, key, System.monotonic_time())
-    end
-  end
-end
diff --git a/lib/recode/task/tags.ex b/lib/recode/task/tags.ex
index f51dbe8..e7ffae8 100644
--- a/lib/recode/task/tags.ex
+++ b/lib/recode/task/tags.ex
@@ -73,6 +73,7 @@ defmodule Recode.Task.Tags do
   defp check_tags(zipper, issues, _opts), do: {zipper, issues}
 
   defp doc?([{:__block__, _meta, [text]}]), do: text != false
+  defp doc?(_ast), do: false
 
   defp doc([{:__block__, meta, [text]}]) do
     doc = Keyword.put(meta, :text, text)
diff --git a/mix.exs b/mix.exs
index 868e1ce..b12ec6b 100644
--- a/mix.exs
+++ b/mix.exs
@@ -1,7 +1,7 @@
 defmodule Recode.MixProject do
   use Mix.Project
 
-  @version "0.6.5"
+  @version "0.7.0"
   @source_url "https://github.com/hrzndhrn/recode"
 
   def project do
@@ -26,7 +26,8 @@ defmodule Recode.MixProject do
 
   def application do
     [
-      extra_applications: [:logger, :mix, :ex_unit, :crypto, :iex, :eex]
+      extra_applications: [:logger, :mix, :ex_unit, :crypto, :iex, :eex],
+      mod: {Recode.Application, []}
     ]
   end
 


Test patch:
diff --git a/test/fixtures/config.exs b/test/fixtures/config.exs
index 7efdf52..ad76f73 100644
--- a/test/fixtures/config.exs
+++ b/test/fixtures/config.exs
@@ -1,5 +1,5 @@
 [
-  version: "0.6.0",
+  version: "0.7.0",
   # Can also be set/reset with `--autocorrect`/`--no-autocorrect`.
   autocorrect: true,
   # With "--dry" no changes will be written to the files.
diff --git a/test/recode/cli_formatter_test.exs b/test/recode/cli_formatter_test.exs
index 529194c..9d114a7 100644
--- a/test/recode/cli_formatter_test.exs
+++ b/test/recode/cli_formatter_test.exs
@@ -22,10 +22,11 @@ defmodule Recode.CLIFormatterTest do
 
     output =
       capture_io(fn ->
-        format({:tasks_finished, project})
+        format({:tasks_finished, project, 10_000})
       end)
 
     assert output |> strip_esc_seq() |> String.trim_leading() == """
+           Executed 0 tasks in 0.01s.
            Files: 1 (.ex: 1)
             Everything ok
 
@@ -48,7 +49,11 @@ defmodule Recode.CLIFormatterTest do
 
     output =
       capture_io(fn ->
-        format({:tasks_finished, project})
+        format({:tasks_finished, project, 10_000},
+          times: [{TaTask, 1}, {DaTask, 2}],
+          corrector_tasks_finished: 20_000,
+          checker_tasks_finished: 12_345
+        )
       end)
 
     assert output |> strip_esc_seq() |> String.trim_leading() == """
@@ -61,6 +66,7 @@ defmodule Recode.CLIFormatterTest do
            3 3   |end
            4 4   |
 
+           Executed 2 tasks in 0.01s.
            Files: 1 (.ex: 1)
            Updated 1 file
             Everything ok
@@ -89,7 +95,7 @@ defmodule Recode.CLIFormatterTest do
 
     output =
       capture_io(fn ->
-        format({:tasks_finished, project})
+        format({:tasks_finished, project, 10_000})
       end)
 
     assert output |> strip_esc_seq() |> String.trim_leading() == """
@@ -111,6 +117,7 @@ defmodule Recode.CLIFormatterTest do
            3 3   |end
            4 4   |
 
+           Executed 0 tasks in 0.01s.
            Files: 2 (.ex: 2)
            Updated 2 files
             Everything ok
@@ -135,7 +142,7 @@ defmodule Recode.CLIFormatterTest do
 
     output =
       capture_io(fn ->
-        format({:tasks_finished, project})
+        format({:tasks_finished, project, 10_000})
       end)
 
     assert output |> strip_esc_seq() |> String.trim_leading() == """
@@ -143,6 +150,7 @@ defmodule Recode.CLIFormatterTest do
            Updates: 2
            Changed by: test, test
 
+           Executed 0 tasks in 0.01s.
            Files: 1 (.ex: 1)
            Updated 1 file
             Everything ok
@@ -180,7 +188,7 @@ defmodule Recode.CLIFormatterTest do
 
     output =
       capture_io(fn ->
-        format({:tasks_finished, project})
+        format({:tasks_finished, project, 10_000})
       end)
 
     output = strip_esc_seq(output)
@@ -204,7 +212,7 @@ defmodule Recode.CLIFormatterTest do
 
     output =
       capture_io(fn ->
-        format({:tasks_finished, project})
+        format({:tasks_finished, project, 10_000})
       end)
 
     assert output |> strip_esc_seq() |> String.trim_leading() == """
@@ -213,6 +221,7 @@ defmodule Recode.CLIFormatterTest do
            Changed by: test
            Moved from: foo.ex
 
+           Executed 0 tasks in 0.01s.
            Files: 1 (.ex: 1)
            Moved 1 file
             Everything ok
@@ -232,13 +241,14 @@ defmodule Recode.CLIFormatterTest do
 
     output =
       capture_io(fn ->
-        format({:tasks_finished, project})
+        format({:tasks_finished, project, 10_000})
       end)
 
     assert output |> strip_esc_seq() |> String.trim_leading() == """
            File: foo.ex
            New file
 
+           Executed 0 tasks in 0.01s.
            Files: 1 (.ex: 1)
            Created 1 file
             Everything ok
@@ -258,13 +268,14 @@ defmodule Recode.CLIFormatterTest do
 
     output =
       capture_io(fn ->
-        format({:tasks_finished, project})
+        format({:tasks_finished, project, 10_000})
       end)
 
     assert output |> strip_esc_seq() |> String.trim_leading() == """
            File: foo.ex
            New file, created by Test
 
+           Executed 0 tasks in 0.01s.
            Files: 1 (.ex: 1)
            Created 1 file
             Everything ok
@@ -291,7 +302,7 @@ defmodule Recode.CLIFormatterTest do
 
     output =
       capture_io(fn ->
-        format({:tasks_finished, project})
+        format({:tasks_finished, project, 10_000})
       end)
 
     output = strip_esc_seq(output)
@@ -301,6 +312,7 @@ defmodule Recode.CLIFormatterTest do
            [foo 1/2] do not do this
            [bar 2/3] no no no
 
+           Executed 0 tasks in 0.01s.
            Files: 1 (.ex: 1)
             Found 2 issues
            """
@@ -326,7 +338,7 @@ defmodule Recode.CLIFormatterTest do
 
     output =
       capture_io(fn ->
-        format({:tasks_finished, project})
+        format({:tasks_finished, project, 10_000})
       end)
 
     output = strip_esc_seq(output)
@@ -337,6 +349,7 @@ defmodule Recode.CLIFormatterTest do
            [foo 2/3] no
            [foo 3/1] no
 
+           Executed 0 tasks in 0.01s.
            Files: 1 (.ex: 1)
             Found 3 issues
            """
@@ -362,7 +375,7 @@ defmodule Recode.CLIFormatterTest do
 
     output =
       capture_io(fn ->
-        format({:tasks_finished, project})
+        format({:tasks_finished, project, 10_000})
       end)
 
     output = strip_esc_seq(output)
@@ -373,6 +386,7 @@ defmodule Recode.CLIFormatterTest do
            [foo 2/2] no
            [foo 2/3] no
 
+           Executed 0 tasks in 0.01s.
            Files: 1 (.ex: 1)
             Found 3 issues
            """
@@ -398,7 +412,7 @@ defmodule Recode.CLIFormatterTest do
 
     output =
       capture_io(fn ->
-        format({:tasks_finished, project})
+        format({:tasks_finished, project, 10_000})
       end)
 
     output = strip_esc_seq(output)
@@ -415,6 +429,7 @@ defmodule Recode.CLIFormatterTest do
            Version 1/2 [foo 1/2] do not do this
            Version 1/2 [bar 2/3] no no no
 
+           Executed 0 tasks in 0.01s.
            Files: 1 (.ex: 1)
            Updated 1 file
             Found 2 issues
@@ -439,7 +454,7 @@ defmodule Recode.CLIFormatterTest do
 
     output =
       capture_io(fn ->
-        format({:tasks_finished, project})
+        format({:tasks_finished, project, 10_000})
       end)
 
     output = strip_esc_seq(output)
@@ -449,6 +464,7 @@ defmodule Recode.CLIFormatterTest do
            Execution of the Test task failed with error:
            Error Message
 
+           Executed 0 tasks in 0.01s.
            Files: 1 (.ex: 1)
             Found 1 issue
 
@@ -471,7 +487,7 @@ defmodule Recode.CLIFormatterTest do
         format({:prepared, project, 9999})
       end)
 
-    assert strip_esc_seq(output) == "Found 1 file\n"
+    assert strip_esc_seq(output) == "Read 1 file\n"
   end
 
   test "formats when tasks ready for an empty project" do
@@ -479,7 +495,7 @@ defmodule Recode.CLIFormatterTest do
 
     output =
       capture_io(fn ->
-        assert format({:tasks_finished, project})
+        assert format({:tasks_finished, project, 10_000})
       end)
 
     assert output == ""
@@ -498,10 +514,11 @@ defmodule Recode.CLIFormatterTest do
 
     output =
       capture_io(fn ->
-        assert format({:tasks_finished, project})
+        assert format({:tasks_finished, project, 10_000})
       end)
 
     assert output |> strip_esc_seq() |> String.trim_leading() == """
+           Executed 0 tasks in 0.01s.
            Files: 1 (.ex: 1)
             Everything ok
 
@@ -553,7 +570,7 @@ defmodule Recode.CLIFormatterTest do
 
     output =
       capture_io(fn ->
-        format({:task_finished, source, TaTask})
+        format({:task_finished, source, TaTask, 1})
       end)
 
     assert strip_esc_seq(output) == "."
@@ -573,7 +590,7 @@ defmodule Recode.CLIFormatterTest do
 
     output =
       capture_io(fn ->
-        format({:task_finished, source, TaTask})
+        format({:task_finished, source, TaTask, 1})
       end)
 
     assert strip_esc_seq(output) == "!"
@@ -593,7 +610,7 @@ defmodule Recode.CLIFormatterTest do
 
     output =
       capture_io(fn ->
-        format({:task_finished, source, TaTask})
+        format({:task_finished, source, TaTask, 1})
       end)
 
     assert strip_esc_seq(output) == "!"
@@ -610,10 +627,11 @@ defmodule Recode.CLIFormatterTest do
 
     output =
       capture_io(fn ->
-        format({:task_finished, source, TaTask}, debug: true)
+        format({:task_finished, source, TaTask, 10_000}, debug: true)
       end)
 
-    assert strip_esc_seq(output) == "Finished Elixir.TaTask with test/formatter_test.ex.\n"
+    assert strip_esc_seq(output) ==
+             "Finished Elixir.TaTask with test/formatter_test.ex [10000μs].\n"
   end
 
   defp format(message, opts \\ []) do
diff --git a/test/recode/runner/impl_test.exs b/test/recode/runner/impl_test.exs
index 1f0c9bf..a3559ca 100644
--- a/test/recode/runner/impl_test.exs
+++ b/test/recode/runner/impl_test.exs
@@ -5,10 +5,10 @@ defmodule Recode.Runner.ImplTest do
   import Mox
 
   alias Recode.Runner.Impl, as: Runner
-  alias Recode.StopWatch
   alias Recode.Task.AliasOrder
   alias Recode.Task.SinglePipe
   alias Recode.TaskMock
+  alias Rewrite.Source
 
   @task_config __recode_task_config__: [checker: true, corrector: true]
 
@@ -20,8 +20,6 @@ defmodule Recode.Runner.ImplTest do
 
     config = "config.exs" |> Code.eval_file() |> elem(0)
 
-    StopWatch.init(start: :recode)
-
     on_exit(fn ->
       File.cd(cwd)
     end)
@@ -108,7 +106,7 @@ defmodule Recode.Runner.ImplTest do
 
     test "runs task with the right config", %{config: config} do
       TaskMock
-      |> expect(:run, fn source, config ->
+      |> expect(:run, 2, fn source, config ->
         assert config == [autocorrect: true]
         source
       end)
@@ -116,14 +114,17 @@ defmodule Recode.Runner.ImplTest do
 
       config = Keyword.put(config, :tasks, [{TaskMock, []}])
 
-      capture_io(fn ->
-        assert Runner.run(config)
-      end)
+      output =
+        capture_io(fn ->
+          assert Runner.run(config)
+        end)
+
+      assert output =~ "Everything ok"
     end
 
     test "runs task with the right aditional config", %{config: config} do
       TaskMock
-      |> expect(:run, fn source, config ->
+      |> expect(:run, 2, fn source, config ->
         assert config == [autocorrect: true, foo: :bar]
         source
       end)
@@ -138,16 +139,18 @@ defmodule Recode.Runner.ImplTest do
 
     test "runs task with input from stdin", %{config: config} do
       config = Keyword.merge(config, inputs: "-", tasks: [{TaskMock, []}])
-      code = ":foo |> bar()"
+      code = ":foo |> bar()\n"
 
       TaskMock
       |> expect(:run, fn source, _config ->
-        assert source.code == code
-        source
+        assert source.content == code
+        Source.update(source, :content, "bar(:foo)")
       end)
       |> expect(:__attributes__, fn -> @task_config end)
 
-      capture_io(code, fn -> assert Runner.run(config) end)
+      capture_io(code, fn ->
+        assert Runner.run(code, config) == "bar(:foo)\n"
+      end)
     end
 
     test "does not run task with active: false", %{config: config} do
diff --git a/test/recode/stop_watch_test.exs b/test/recode/stop_watch_test.exs
deleted file mode 100644
index d0c78de..0000000
--- a/test/recode/stop_watch_test.exs
+++ /dev/null
@@ -1,68 +0,0 @@
-defmodule Recode.StopWatchTest do
-  use ExUnit.Case
-
-  alias Recode.StopWatch
-
-  doctest Recode.StopWatch
-
-  setup do
-    [name: String.to_atom("sw-#{System.unique_integer()}")]
-  end
-
-  describe "init/1" do
-    test "raises an ArgumentError when already initalized", %{name: name} do
-      message = "StopWatch already exisits."
-
-      assert_raise(ArgumentError, message, fn ->
-        StopWatch.init(name: name)
-        StopWatch.init(name: name)
-      end)
-    end
-  end
-
-  describe "start/2" do
-    test "raises an ArgumentError when already started", %{name: name} do
-      message = "StopWatch has already received the :start operation for :foo."
-
-      assert_raise(ArgumentError, message, fn ->
-        StopWatch.init(name: name)
-        StopWatch.start(name, :foo)
-        StopWatch.start(name, :foo)
-      end)
-    end
-  end
-
-  describe "stop/2" do
-    test "raises an ArgumentError when already stoped", %{name: name} do
-      message = "StopWatch has already received the :stop operation for :foo."
-
-      assert_raise(ArgumentError, message, fn ->
-        StopWatch.init(name: name)
-        StopWatch.start(name, :foo)
-        StopWatch.stop(name, :foo)
-        StopWatch.stop(name, :foo)
-      end)
-    end
-  end
-
-  describe "time/2" do
-    test "returns time for a running stop watch", %{name: name} do
-      StopWatch.init(name: name)
-      StopWatch.start(name, :foo)
-      time_1 = StopWatch.time(name, :foo)
-      time_2 = StopWatch.time(name, :foo)
-
-      assert time_1 < time_2
-    end
-
-    test "returns time for a stoped stop watch", %{name: name} do
-      StopWatch.init(name: name)
-      StopWatch.start(name, "foo")
-      StopWatch.stop(name, "foo")
-      time_1 = StopWatch.time(name, "foo")
-      time_2 = StopWatch.time(name, "foo")
-
-      assert time_1 == time_2
-    end
-  end
-end