Repository: hrzndhrn/recode
Pull request #64 (2023-08-17 18:01:41)

Recode/tasks/tag

coveralls: ## Pull Request Test Coverage Report for [Build 9b65fb36da9634c51bf790cf0a19a2b7c3b5d2d9-PR-64](https://coveralls.io/builds/62058951)

* **79** of **95**   **(83.16%)**  changed or added relevant lines in **12** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage decreased (**-0.6%**) to **88.863%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [lib/recode/stop_watch.ex](https://coveralls.io/builds/62058951/source?filename=lib%2Frecode%2Fstop_watch.ex#L42) | 6 | 7 | 85.71%
| [lib/recode/context.ex](https://coveralls.io/builds/62058951/source?filename=lib%2Frecode%2Fcontext.ex#L638) | 0 | 6 | 0.0%
| [lib/mix/tasks/recode.ex](https://coveralls.io/builds/62058951/source?filename=lib%2Fmix%2Ftasks%2Frecode.ex#L93) | 18 | 27 | 66.67%
<!-- | **Total:** | **79** | **95** | **83.16%** | -->


|  Totals | [![Coverage Status](https://coveralls.io/builds/62058951/badge)](https://coveralls.io/builds/62058951) |
| :-- | --: |
| Change from base [Build 5e9651f046ea661ababe8adcb58431f04806c943](https://coveralls.io/builds/61683358): |  -0.6% |
| Covered Lines: | 750 |
| Relevant Lines: | 844 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ea3cafd..58a3d0b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,10 @@
 + Use switch `--task` multiple times.
 + Add mix task `recode.help`.
 + Add mix task `recode.update.config`.
++ Add some minor fixes for `Recode.Task.AliasOrder`.
++ Fix file count output.
++ Add callback `init/1` to `Recode.Task`.
++ Add validation of `task` and `config` in `Mix.Tasks.Recode`
 
 ## 0.5.2 - 2023/07/17
 
diff --git a/README.md b/README.md
index b5aef42..4e8d1a2 100644
--- a/README.md
+++ b/README.md
@@ -12,14 +12,30 @@ This library is still under development, breaking changes are expected.
 The same is true for `sourceror` and most of `recode`'s functionality is based
 on `sourceror`.
 
-For now, `recode` corrects only a few things:
+`recode` can correct and check the following things:
 
-* `AliasExpansion` expands multi aliases
-* `AliasOrder` orders aliases alphabetically
-* `Format` runs the Elixir formatter
-* `PipeFunOne` adds `()` to one arity functions in pipes
-* `SinglePipe` corrects single pipes
-* `TestFileExt` renames test file extensions to `*.exs`
+```sh
+> mix recode.help
+
+Design tasks:
+TagFIXME          # Checker   - Checks if there are FIXME tags in the sources.
+TagTODO           # Checker   - Checks if there are TODO tags in the sources.
+Readability tasks:
+AliasExpansion    # Corrector - Exapnds multi aliases to separate aliases.
+AliasOrder        # Corrector - Checks if aliases are sorted alphabetically.
+EnforceLineLength # Corrector - Forces expressions to one line.
+Format            # Corrector - Does the same as `mix format`.
+PipeFunOne        # Corrector - Add parentheses to one-arity functions.
+SinglePipe        # Corrector - Pipes should only be used when piping data through multiple calls.
+Specs             # Checker   - Checks for specs.
+Refactor tasks:
+FilterCount       # Corrector - Checks calls like Enum.filter(...) |> Enum.count().
+Nesting           # Checker   - Checks code nesting depth in functions and macros.
+Warning tasks:
+Dbg               # Corrector - There should be no calls to dbg.
+TestFileExt       # Corrector - Checks the file extension of test files.
+UnusedVariable    # Corrector - Checks if unused variables occur.
+```
 
 It is also possible to run `recode` in a none-autocorrect mode to just lint your
 code.
@@ -58,7 +74,7 @@ Documentation can be found at [https://hexdocs.pm/recode](https://hexdocs.pm/rec
 
 ## Usage
 
-To start with Foo a configuration file is needed.
+To start with `recode` a configuration file is needed.
 
 ```sh
 mix recode.gen.config
@@ -67,40 +83,51 @@ mix recode.gen.config
 This mix task generates the config file `.recode.exs`.
 
 ```elixir
-alias Recode.Task
-
 [
-  version: "0.4.0",
-  # Can also be set/reset with "--autocorrect"/"--no-autocorrect".
+  version: "0.6.0",
+  # Can also be set/reset with `--autocorrect`/`--no-autocorrect`.
   autocorrect: true,
   # With "--dry" no changes will be written to the files.
-  # Can also be set/reset with "--dry"/"--no-dry".
+  # Can also be set/reset with `--dry`/`--no-dry`.
   # If dry is true then verbose is also active.
   dry: false,
-  # Can also be set/reset with "--verbose"/"--no-verbose".
+  # Can also be set/reset with `--verbose`/`--no-verbose`.
   verbose: false,
   # Can be overwriten by calling `mix recode "lib/**/*.ex"`.
-  inputs: ["{config,lib,test}/**/*.{ex,exs}"],
+  inputs: ["{mix,.formatter}.exs", "{apps,config,lib,test}/**/*.{ex,exs}"],
   formatter: {Recode.Formatter, []},
   tasks: [
     # Tasks could be added by a tuple of the tasks module name and an options
     # keyword list. A task can be deactived by `active: false`. The execution of
     # a deactivated task can be forced by calling `mix recode --task ModuleName`.
-    {Task.AliasExpansion, []},
-    {Task.AliasOrder, []},
-    {Task.EnforceLineLength, active: false},
-    {Task.PipeFunOne, []},
-    {Task.SinglePipe, []},
-    {Task.Specs, exclude: "test/**/*.{ex,exs}", config: [only: :visible]},
-    {Task.TestFileExt, []},
-    {Task.UnusedVariable, active: false}
+    {Recode.Task.AliasExpansion, []},
+    {Recode.Task.AliasOrder, []},
+    {Recode.Task.Dbg, [autocorrect: false]},
+    {Recode.Task.EnforceLineLength, [active: false]},
+    {Recode.Task.FilterCount, []},
+    {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.TestFileExt, []},
+    {Recode.Task.UnusedVariable, [active: false]}
   ]
 ]
 ```
 
+If a configuration file already exists, you can use the mix task
+
+```sh
+mix recode.update.config
+```
+
+to update the configuration file.
+
 ### `mix recode`
 
-This mix tasks runs the linter with autocorrection. The switch `--dry` prevents
+This mix task runs the linter with autocorrection. The switch `--dry` prevents
 the update of the files and shows all changes in the console.
 
 ```
@@ -257,7 +284,21 @@ You can also run Recode together with `mix format` by adding
 ]
 ```
 
-Note that Elixir >= 1.13 is required.
+### `mix recode.help`
+
+The task `recode.help` prints all available recode-task with a short description.
+
+The task prints the documentation for a given recode-task.
+
+```sh
+> mix recode.help Dbg
+                                Recode.Task.Dbg
+
+Calls to dbg/2 should only appear in debug sessions.
+
+This task rewrites the code when mix recode runs with autocorrect: true.
+```
+
 
 ## Differences to Credo
 
diff --git a/examples/my_code/.recode.exs b/examples/my_code/.recode.exs
index ce2fc25..8816821 100644
--- a/examples/my_code/.recode.exs
+++ b/examples/my_code/.recode.exs
@@ -20,10 +20,12 @@
     {Recode.Task.Dbg, [autocorrect: false]},
     {Recode.Task.EnforceLineLength, [active: false]},
     {Recode.Task.FilterCount, []},
-    {Recode.Task.Nesting, [config: [max_depth: 2]]},
+    {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.TestFileExt, []},
     {Recode.Task.UnusedVariable, [active: false]}
   ]
diff --git a/examples/my_code/lib/my_code/raise_task.ex b/examples/my_code/lib/my_code/raise_task.ex
index 04666f9..40ea0f8 100644
--- a/examples/my_code/lib/my_code/raise_task.ex
+++ b/examples/my_code/lib/my_code/raise_task.ex
@@ -1,7 +1,7 @@
 defmodule MyCode.RaiseTask do
   @moduledoc false
 
-  use Recode.Task, correct: true, check: true
+  use Recode.Task, corrector: true
 
   @impl Recode.Task
   def run(_source, _opts) do
diff --git a/examples/my_code/lib/my_code/tags.ex b/examples/my_code/lib/my_code/tags.ex
new file mode 100644
index 0000000..569e0c0
--- /dev/null
+++ b/examples/my_code/lib/my_code/tags.ex
@@ -0,0 +1,8 @@
+defmodule MyCode.Tags do
+  @moduledoc """
+  TODO: add docs
+  """
+
+  # FIXME: add more functions
+  def fun, do: :foo
+end
diff --git a/lib/mix/tasks/recode.ex b/lib/mix/tasks/recode.ex
index 13ac5dd..94a3140 100644
--- a/lib/mix/tasks/recode.ex
+++ b/lib/mix/tasks/recode.ex
@@ -35,44 +35,68 @@ defmodule Mix.Tasks.Recode do
   """
 
   use Mix.Task
+  use Recode.StopWatch
+
+  import Recode.IO
 
   alias Recode.Config
   alias Recode.Runner
 
   @opts strict: [
           autocorrect: :boolean,
-          dry: :boolean,
-          verbose: :boolean,
           config: :string,
-          task: :keep
+          dry: :boolean,
+          task: :keep,
+          verbose: :boolean
+        ],
+        aliases: [
+          a: :autocorrect,
+          c: :config,
+          d: :dry,
+          t: :task,
+          v: :verbose
         ]
 
   @impl Mix.Task
   @spec run(list()) :: no_return()
   def run(opts) do
+    _stop_watch = StopWatch.init!()
+    StopWatch.start!(:recode)
+
     opts = opts!(opts)
 
+    opts =
+      opts
+      |> Keyword.get(:config, ".recode.exs")
+      |> config!()
+      |> validate_config!()
+      |> validate_tasks!()
+      |> update_task_configs!()
+      |> Keyword.merge(Keyword.take(opts, [:verbose, :autocorrect, :dry, :inputs]))
+      |> Keyword.put(:cli_opts, acc_tasks(opts))
+      |> update(:verbose)
+
     opts
-    |> Keyword.get(:config, ".recode.exs")
-    |> config!()
-    |> validate_config!()
-    |> Keyword.merge(Keyword.take(opts, [:verbose, :autocorrect, :dry, :inputs]))
-    |> Keyword.put(:cli_opts, acc_tasks(opts))
-    |> update(:verbose)
     |> Runner.run()
-    |> output()
+    |> output(opts[:tasks])
   end
 
-  @spec output(Rewrite.t()) :: no_return()
-  defp output(%Rewrite{sources: sources}) when map_size(sources) == 0 do
+  @spec output(Rewrite.t(), keyword()) :: no_return()
+  defp output(%Rewrite{sources: sources}, _opts) when map_size(sources) == 0 do
     Mix.raise("No sources found")
   end
 
-  defp output(%Rewrite{} = project) do
-    case Rewrite.issues?(project) do
-      true -> exit({:shutdown, 1})
-      false -> exit(:normal)
-    end
+  defp output(%Rewrite{} = project, tasks) do
+    reason =
+      case Rewrite.issues?(project) do
+        false -> :normal
+        true -> {:shutdown, exit_code(project, tasks)}
+      end
+
+    time = (StopWatch.time!(:recode) / 1_000) |> Float.round(2) |> max(0.01)
+    puts([:info, "Finished in #{inspect(time)} seconds."])
+
+    exit(reason)
   end
 
   defp opts!(opts) do
@@ -82,6 +106,17 @@ defmodule Mix.Tasks.Recode do
     end
   end
 
+  defp exit_code(project, tasks) do
+    exit_codes =
+      Enum.into(tasks, %{}, fn {task, config} -> {task, Keyword.get(config, :exit_code, 1)} end)
+
+    Enum.reduce(Rewrite.sources(project), 0, fn source, exit_code ->
+      Enum.reduce(source.issues, exit_code, fn {_version, issue}, exit_code ->
+        Bitwise.bor(exit_code, Map.get(exit_codes, issue.reporter, 1))
+      end)
+    end)
+  end
+
   defp acc_tasks(opts) do
     tasks =
       Enum.reduce(opts, [], fn {key, value}, acc ->
@@ -119,6 +154,40 @@ defmodule Mix.Tasks.Recode do
     end
   end
 
+  defp validate_tasks!(config) do
+    Enum.each(config[:tasks], fn {task, _config} ->
+      task |> Code.ensure_loaded() |> validate_task!(task)
+    end)
+
+    config
+  end
+
+  defp validate_task!({:error, :nofile}, task) do
+    Mix.raise("Recode task #{inspect(task)} not found.")
+  end
+
+  defp validate_task!({:module, _module}, task) do
+    unless Recode.Task 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 ->
+      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)
+  end
+
   defp update(opts, :verbose) do
     case opts[:dry] do
       true -> Keyword.put(opts, :verbose, true)
diff --git a/lib/mix/tasks/recode.help.ex b/lib/mix/tasks/recode.help.ex
index 8d506f3..41a9345 100644
--- a/lib/mix/tasks/recode.help.ex
+++ b/lib/mix/tasks/recode.help.ex
@@ -4,6 +4,10 @@ defmodule Mix.Tasks.Recode.Help do
   @moduledoc """
   Lists all availbale recode tasks with a short description or prints the
   documentation for a given recode task.
+
+  To print the documentation of a task run `mix recode.help {task-name}`. As a
+  task name the module name (e.g. `Nesting`) or the full module name (e.g.
+  `Recode.Task.Nesting`) is accepted.
   """
 
   use Mix.Task
@@ -27,7 +31,10 @@ defmodule Mix.Tasks.Recode.Help do
       IEx.Introspection.h(module)
       :ok
     else
-      Mix.raise("task #{task} not found")
+      Mix.raise("""
+      The recode task #{task} could not be found. \
+      Run "mix recode.help" for a list of recode tasks.\
+      """)
     end
   end
 
@@ -37,25 +44,29 @@ defmodule Mix.Tasks.Recode.Help do
     """)
   end
 
-  defp category(module), do: module.category()
+  defp category(module), do: Recode.Task.category(module)
 
   defp task?(module) do
-    module |> inspect() |> String.starts_with?(@task_namespace)
+    task? = module |> inspect() |> String.starts_with?(@task_namespace)
+
+    task? and not is_nil(Recode.Task.shortdoc(module))
   end
 
   defp task?(module, name) do
     with true <- task?(module) do
-      module |> inspect() |> String.trim_leading(@task_namespace) == name
+      inspect(module) == name or
+        module |> inspect() |> String.trim_leading(@task_namespace) == name
     end
   end
 
   defp info(module) do
     name = module |> inspect() |> String.trim_leading(@task_namespace)
-    {name, module.shortdoc()}
+    {name, Recode.Task.shortdoc(module), Recode.Task.corrector?(module)}
   end
 
   defp print(info) do
     max = max_name_length(info)
+    print("Design tasks:", info.design, max)
     print("Readability tasks:", info.readability, max)
     print("Refactor tasks:", info.refactor, max)
     print("Warning tasks:", info.warning, max)
@@ -64,8 +75,9 @@ defmodule Mix.Tasks.Recode.Help do
   defp print(section, tasks, max) do
     IO.puts(section)
 
-    Enum.each(tasks, fn {task, doc} ->
-      IO.puts(String.pad_trailing(task, max) <> " # " <> doc)
+    Enum.each(tasks, fn {task, doc, corrector?} ->
+      type = if corrector?, do: "Corrector -", else: "Checker   -"
+      IO.puts(String.pad_trailing(task, max) <> " # #{type} #{doc}")
     end)
   end
 
@@ -73,7 +85,7 @@ defmodule Mix.Tasks.Recode.Help do
     info
     |> Map.values()
     |> List.flatten()
-    |> Enum.reduce(0, fn {name, _shortdoc}, max ->
+    |> Enum.reduce(0, fn {name, _shortdoc, _corrector?}, max ->
       max(byte_size(name), max)
     end)
   end
diff --git a/lib/recode/ast.ex b/lib/recode/ast.ex
index 74308d4..498f8e4 100644
--- a/lib/recode/ast.ex
+++ b/lib/recode/ast.ex
@@ -454,6 +454,14 @@ defmodule Recode.AST do
     {module, [], nil}
   end
 
+  def alias_info(
+        {:alias, _meta1, [{{:., _meta2, [{:__MODULE__, _meta3, _args}, _opts]}, _meta4, multi}]}
+      ) do
+    multi = Enum.map(multi, &aliases_concat/1)
+
+    {:__MODULE__, multi, nil}
+  end
+
   def alias_info({:alias, _meta, [{{:., _meta2, [aliases, _opts]}, _meta3, multi}]}) do
     module = aliases_concat(aliases)
     multi = Enum.map(multi, &aliases_concat/1)
diff --git a/lib/recode/config.ex b/lib/recode/config.ex
index 0476a60..1623f9e 100644
--- a/lib/recode/config.ex
+++ b/lib/recode/config.ex
@@ -32,10 +32,12 @@ defmodule Recode.Config do
       {Recode.Task.Dbg, [autocorrect: false]},
       {Recode.Task.EnforceLineLength, [active: false]},
       {Recode.Task.FilterCount, []},
-      {Recode.Task.Nesting, [config: [max_depth: 2]]},
+      {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.TestFileExt, []},
       {Recode.Task.UnusedVariable, [active: false]}
     ]
diff --git a/lib/recode/context.ex b/lib/recode/context.ex
index cca52a6..3f32af4 100644
--- a/lib/recode/context.ex
+++ b/lib/recode/context.ex
@@ -74,8 +74,6 @@ defmodule Recode.Context do
       }
   '''
 
-  import Recode.Utils, only: [ends_with?: 2]
-
   alias Recode.Context
   alias Sourceror.Zipper
 
@@ -635,4 +633,18 @@ defmodule Recode.Context do
   defp attribute_args(nil), do: nil
 
   defp attribute_args({:@, _meta1, [{_name, _meta2, args}]}), do: args
+
+  defp ends_with?(value, suffix) when is_atom(value) and is_atom(suffix) do
+    list = Module.split(value)
+    suffix = Module.split(suffix)
+
+    ends_with?(list, suffix)
+  end
+
+  defp ends_with?(list, suffix) when is_list(list) and is_list(suffix) do
+    case length(list) - length(suffix) do
+      diff when diff < 0 -> false
+      diff -> Enum.drop(list, diff) == suffix
+    end
+  end
 end
diff --git a/lib/recode/formatter.ex b/lib/recode/formatter.ex
index 37d76e8..c36d414 100644
--- a/lib/recode/formatter.ex
+++ b/lib/recode/formatter.ex
@@ -63,7 +63,7 @@ defmodule Recode.Formatter do
 
   defp counts(project) do
     {
-      Rewrite.count(project, ".ex"),
+      Enum.count(project.sources),
       Rewrite.count(project, ".exs")
     }
   end
@@ -188,7 +188,7 @@ defmodule Recode.Formatter do
   end
 
   defp format_issue(%{reporter: Recode.Runner, meta: meta}, _version, _actual, false) do
-    [:warn, "Execution of the #{inspect(meta[:task])} task failed."]
+    [:warn, "Execution of the #{inspect(meta[:task])} task failed.\n"]
   end
 
   defp format_issue(issue, version, actual, _verbose) do
diff --git a/lib/recode/runner/impl.ex b/lib/recode/runner/impl.ex
index 6420a3f..ec9f342 100644
--- a/lib/recode/runner/impl.ex
+++ b/lib/recode/runner/impl.ex
@@ -4,6 +4,7 @@ defmodule Recode.Runner.Impl do
   @behaviour Recode.Runner
 
   alias Recode.Issue
+  alias Recode.Task
   alias Rewrite.Source
 
   @impl true
@@ -163,7 +164,7 @@ defmodule Recode.Runner.Impl do
 
   defp tasks(tasks, :autocorrect, autocorrect) do
     case autocorrect do
-      false -> Enum.filter(tasks, fn {task, _opts} -> task.config(:check) end)
+      false -> Enum.filter(tasks, fn {task, _opts} -> Task.checker?(task) end)
       true -> tasks
     end
   end
@@ -183,7 +184,7 @@ defmodule Recode.Runner.Impl do
   defp correst_first(tasks) do
     groups =
       Enum.group_by(tasks, fn {task, opts} ->
-        task.config(:correct) && Keyword.get(opts, :autocorrect)
+        Task.corrector?(task) && Keyword.get(opts, :autocorrect)
       end)
 
     Enum.concat(Map.get(groups, true, []), Map.get(groups, false, []))
diff --git a/lib/recode/stop_watch.ex b/lib/recode/stop_watch.ex
new file mode 100644
index 0000000..03c6500
--- /dev/null
+++ b/lib/recode/stop_watch.ex
@@ -0,0 +1,72 @@
+defmodule Recode.StopWatch do
+  @moduledoc """
+  A stopwatch to measure times in milliseconds.
+
+  ## Examples
+
+      iex> alias Recode.StopWatch
+      iex> StopWatch.init!(:test)
+      iex> StopWatch.start!(:test, :foo)
+      iex> t1 = StopWatch.time!(:test, :foo)
+      iex> Process.sleep(1)
+      iex> t2 = StopWatch.time!(:test, :foo)
+      iex> Process.sleep(1)
+      iex> StopWatch.stop!(:test, :foo)
+      iex> t3 = StopWatch.time!(:test, :foo)
+      iex> t1 < t2 and t2 < t3
+      true
+      iex> t3 == StopWatch.time!(:test, :foo)
+      true
+  """
+  @name :stop_watch
+
+  defmacro __using__(_opts) do
+    quote do
+      require Recode.StopWatch
+      alias Recode.StopWatch
+    end
+  end
+
+  def init!(name \\ @name) do
+    :ets.new(name, [:set, :public, :named_table])
+  rescue
+    ArgumentError ->
+      reraise ArgumentError,
+              [message: "StopWatch #{inspect(name)} already exisits."],
+              __STACKTRACE__
+  end
+
+  @doc false
+  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
+
+  @doc false
+  def _time!(name \\ @name, key, timestamp) do
+    case :ets.match(name, {{key, :_}, :"$1"}) do
+      [[timestamp1], [timestamp2]] -> abs(timestamp1 - timestamp2)
+      [[timestamp1]] -> abs(timestamp - timestamp1)
+    end
+  end
+
+  defmacro start!(name \\ @name, key) do
+    quote bind_quoted: [name: name, key: key] do
+      Recode.StopWatch._put!(name, key, :start, System.monotonic_time(:millisecond))
+    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(:millisecond))
+    end
+  end
+
+  defmacro time!(name \\ @name, key) do
+    quote bind_quoted: [name: name, key: key] do
+      Recode.StopWatch._time!(name, key, System.monotonic_time(:millisecond))
+    end
+  end
+end
diff --git a/lib/recode/task.ex b/lib/recode/task.ex
index b016b07..5439cbe 100644
--- a/lib/recode/task.ex
+++ b/lib/recode/task.ex
@@ -5,56 +5,94 @@ defmodule Recode.Task do
 
   alias Rewrite.Source
 
+  @type config :: keyword()
+  @type message :: String.t()
+  @type task :: module()
+  @type category :: atom()
+
   @doc """
   Applies a task with the given `source` and `opts`.
   """
   @callback run(source :: Source.t(), opts :: Keyword.t()) :: Source.t()
 
   @doc """
-  Returns the configuration for the given `key`.
+  Sets a callback to check and manipulate `config` before any recode task runs.
+
+  When `init` returns an error tuple, the `mix recode` task raises an exception
+  with the returned `message`.
   """
-  @callback config(key :: :check | :correct | :refactor) :: boolean
+  @callback init(config()) :: {:ok, config} | {:error, message()}
 
-  @optional_callbacks config: 1
+  # a callback for mox
+  @doc false
+  @callback __attributes__ :: any
 
-  defmacro __using__(opts) do
-    config = Keyword.validate!(opts, check: false, correct: false)
+  @optional_callbacks init: 1
 
-    quote do
-      @behaviour Recode.Task
+  @doc """
+  Returns `true` if the given `task` provides a check for sources.
+  """
+  @spec checker?(task()) :: boolean()
+  def checker?(task) when is_atom(task), do: attribute(task, :checker)
 
-      @config unquote(config)
+  @doc """
+  Returns `true` if the given `task` provides a correction functionality for
+  sources.
+  """
+  @spec corrector?(task()) :: boolean()
+  def corrector?(task) when is_atom(task), do: attribute(task, :corrector)
+
+  @doc """
+  Returns the category for the given `task`.
+  """
+  @spec category(task()) :: category()
+  def category(task) when is_atom(task), do: attribute(task, :category)
 
-      for name <- [:shortdoc, :category] do
-        Module.register_attribute(__MODULE__, name, persist: true)
-      end
+  @doc """
+  Returns the shortdoc for the given `task`.
 
-      @doc """
-      Returns the config entry for the given key.
-      """
-      def config(key) when key in [:check, :correct] do
-        Keyword.fetch!(@config, key)
-      end
+  Returns `nil` if `@shortdoc` is not available for the `task`.
+  """
+  @spec shortdoc(task()) :: String.t() | nil
+  def shortdoc(task) when is_atom(task), do: attribute(task, :shortdoc)
+
+  defp attribute(task, key) when key in [:corrector, :checker, :category] do
+    task.__attributes__()
+    |> Keyword.fetch!(:__recode_task_config__)
+    |> Keyword.fetch!(key)
+  end
+
+  defp attribute(task, key) do
+    task.__attributes__()
+    |> Keyword.get(key, [])
+    |> unwrap()
+  end
+
+  defp unwrap([]), do: nil
+
+  defp unwrap([value]), do: value
+
+  defmacro __using__(opts) do
+    config =
+      Keyword.validate!(opts,
+        checker: true,
+        corrector: false,
+        category: nil
+      )
+
+    quote do
+      @behaviour Recode.Task
 
-      @doc """
-      Returns the shortdoc for recode task.
+      @__recode_task_config__ unquote(config)
 
-      Returns `nil` if `@shortdoc` is not available.
-      """
-      def shortdoc, do: attribute(:shortdoc)
+      Module.register_attribute(__MODULE__, :__recode_task_config__, persist: true)
+      Module.register_attribute(__MODULE__, :shortdoc, persist: true)
 
-      @doc """
-      Returns the category for recode task.
+      def init(config), do: {:ok, config}
 
-      Returns `nil` if `@catgoey` is not available.
-      """
-      def category, do: attribute(:category)
+      def __attributes__, do: __MODULE__.__info__(:attributes)
 
-      defp attribute(key) do
-        with [shortdoc] <- Keyword.get(__MODULE__.__info__(:attributes), key) do
-          shortdoc
-        end
-      end
+      defoverridable init: 1
     end
   end
 end
diff --git a/lib/recode/task/alias_expansion.ex b/lib/recode/task/alias_expansion.ex
index d5b088e..0b98bbd 100644
--- a/lib/recode/task/alias_expansion.ex
+++ b/lib/recode/task/alias_expansion.ex
@@ -1,4 +1,6 @@
 defmodule Recode.Task.AliasExpansion do
+  @shortdoc "Exapnds multi aliases to separate aliases."
+
   @moduledoc """
   Multi aliases makes module uses harder to search for in large code bases.
 
@@ -12,11 +14,7 @@ defmodule Recode.Task.AliasExpansion do
   This task rewrites the code when `mix recode` runs with `autocorrect: true`.
   """
 
-  @shortdoc "Exapnds multi aliases to separate aliases."
-
-  @category :readability
-
-  use Recode.Task, correct: true, check: true
+  use Recode.Task, corrector: true, category: :readability
 
   alias Recode.Issue
   alias Recode.Task.AliasExpansion
diff --git a/lib/recode/task/alias_order.ex b/lib/recode/task/alias_order.ex
index 7ba3871..6e12b33 100644
--- a/lib/recode/task/alias_order.ex
+++ b/lib/recode/task/alias_order.ex
@@ -1,4 +1,6 @@
 defmodule Recode.Task.AliasOrder do
+  @shortdoc "Checks if aliases are sorted alphabetically."
+
   @moduledoc """
   Alphabetically sorted lists are easier to read.
 
@@ -15,11 +17,7 @@ defmodule Recode.Task.AliasOrder do
       alias Bravo
   """
 
-  @shortdoc "Checks if aliases are sorted alphabetically."
-
-  @category :readability
-
-  use Recode.Task, correct: true, check: true
+  use Recode.Task, corrector: true, category: :readability
 
   alias Recode.AST
   alias Recode.Issue
@@ -199,17 +197,25 @@ defmodule Recode.Task.AliasOrder do
 
     case module1 == module2 do
       true -> length(multi1) < length(multi2)
-      false -> lt?(module1, module2)
+      false -> lower_then?(module1, module2)
     end
   end
 
   defp sort({:__aliases__, _meta1, args1}, {:__aliases__, _meta2, args2}) do
-    lt?(args1, args2)
+    lower_then?(args1, args2)
   end
 
-  defp lt?([value1], [value2]), do: lt?(value1, value2)
+  defp lower_then?([value1], [value2]), do: lower_then?(value1, value2)
+
+  defp lower_then?([_value], []), do: false
+
+  defp lower_then?([], [_value]), do: true
+
+  defp lower_then?([value | rest1], [value | rest2]), do: lower_then?(rest1, rest2)
+
+  defp lower_then?([value1 | _], [value2 | _]), do: lower_then?(value1, value2)
 
-  defp lt?(value1, value2) when is_atom(value1) and is_atom(value2) do
+  defp lower_then?(value1, value2) when is_atom(value1) and is_atom(value2) do
     String.upcase(to_string(value1)) < String.upcase(to_string(value2))
   end
 
diff --git a/lib/recode/task/dbg.ex b/lib/recode/task/dbg.ex
index 81fb047..dc4c98a 100644
--- a/lib/recode/task/dbg.ex
+++ b/lib/recode/task/dbg.ex
@@ -1,15 +1,13 @@
 defmodule Recode.Task.Dbg do
+  @shortdoc "There should be no calls to dbg."
+
   @moduledoc """
   Calls to `dbg/2` should only appear in debug sessions.
 
   This task rewrites the code when `mix recode` runs with `autocorrect: true`.
   """
 
-  @shortdoc "There should be no calls to dbg."
-
-  @category :warning
-
-  use Recode.Task, correct: true, check: true
+  use Recode.Task, corrector: true, category: :warning
 
   alias Recode.Issue
   alias Recode.Task.Dbg
diff --git a/lib/recode/task/enforce_line_length.ex b/lib/recode/task/enforce_line_length.ex
index 6d97ed9..3b1123c 100644
--- a/lib/recode/task/enforce_line_length.ex
+++ b/lib/recode/task/enforce_line_length.ex
@@ -1,4 +1,6 @@
 defmodule Recode.Task.EnforceLineLength do
+  @shortdoc "Forces expressions to one line."
+
   @moduledoc """
   The `EnforceLineLength` task writes multiline expressions into one line if
   they do not exceed the maximum line length.
@@ -33,11 +35,7 @@ defmodule Recode.Task.EnforceLineLength do
   and with the option `skip: :fn` the code keeps unchanged.
   """
 
-  @shortdoc "Forces expressions to one line."
-
-  @category :readability
-
-  use Recode.Task, correct: true, check: false
+  use Recode.Task, corrector: true, category: :readability
 
   alias Recode.AST
   alias Recode.Task.EnforceLineLength
diff --git a/lib/recode/task/filter_count.ex b/lib/recode/task/filter_count.ex
index 880f962..616f087 100644
--- a/lib/recode/task/filter_count.ex
+++ b/lib/recode/task/filter_count.ex
@@ -1,4 +1,6 @@
 defmodule Recode.Task.FilterCount do
+  @shortdoc "Checks calls like Enum.filter(...) |> Enum.count()."
+
   @moduledoc """
   `Enum.count/2` is more efficient than `Enum.filter/2 |> Enum.count/1`.
 
@@ -18,11 +20,7 @@ defmodule Recode.Task.FilterCount do
   This task rewrites the code when `mix recode` runs with `autocorrect: true`.
   """
 
-  @shortdoc "Checks calls like Enum.filter(...) |> Enum.count()."
-
-  @category :refactor
-
-  use Recode.Task, correct: true, check: true
+  use Recode.Task, corrector: true, category: :refactor
 
   alias Recode.Issue
   alias Recode.Task.FilterCount
diff --git a/lib/recode/task/format.ex b/lib/recode/task/format.ex
index 0486c45..173ffb7 100644
--- a/lib/recode/task/format.ex
+++ b/lib/recode/task/format.ex
@@ -1,15 +1,13 @@
 defmodule Recode.Task.Format do
+  @shortdoc "Does the same as `mix format`."
+
   @moduledoc """
   This task runs the Elixir formatter.
 
   This task runs as first task by any `mix recode` call.
   """
 
-  @shortdoc "Does the same as `mix format`."
-
-  @category :readability
-
-  use Recode.Task, correct: true, check: true
+  use Recode.Task, corrector: true, category: :readability
 
   alias Recode.Issue
   alias Recode.Task.Format
diff --git a/lib/recode/task/nesting.ex b/lib/recode/task/nesting.ex
index 67b8998..d4373c0 100644
--- a/lib/recode/task/nesting.ex
+++ b/lib/recode/task/nesting.ex
@@ -9,13 +9,12 @@ defmodule Recode.Task.Nesting do
   ## Options
 
     * `:max_depth` - the maximum allowable depth, defaults to `2`.
+
   """
 
   @shortdoc "Checks code nesting depth in functions and macros."
 
-  @category :refactor
-
-  use Recode.Task, check: true
+  use Recode.Task, category: :refactor
 
   alias Recode.Issue
   alias Recode.Task.Nesting
@@ -28,15 +27,18 @@ defmodule Recode.Task.Nesting do
 
   @impl Recode.Task
   def run(source, opts) do
-    max_depth = Keyword.get(opts, :max_depth, @max_depth)
-
     source
     |> Source.get(:quoted)
     |> Zipper.zip()
-    |> Zipper.traverse_while([], traverse_defs(max_depth))
+    |> Zipper.traverse_while([], traverse_defs(opts[:max_depth]))
     |> update(source)
   end
 
+  @impl Recode.Task
+  def init(opts) do
+    {:ok, Keyword.put_new(opts, :max_depth, @max_depth)}
+  end
+
   defp update({_zipper, issues}, source) do
     Source.add_issues(source, issues)
   end
diff --git a/lib/recode/task/pipe_fun_one.ex b/lib/recode/task/pipe_fun_one.ex
index 9896c37..fd9cfad 100644
--- a/lib/recode/task/pipe_fun_one.ex
+++ b/lib/recode/task/pipe_fun_one.ex
@@ -1,4 +1,6 @@
 defmodule Recode.Task.PipeFunOne do
+  @shortdoc "Add parentheses to one-arity functions."
+
   @moduledoc """
   Add parentheses to one-arity functions.
 
@@ -11,11 +13,7 @@ defmodule Recode.Task.PipeFunOne do
   This task rewrites the code when `mix recode` runs with `autocorrect: true`.
   """
 
-  @shortdoc "Add parentheses to one-arity functions."
-
-  @category :readability
-
-  use Recode.Task, correct: true, check: true
+  use Recode.Task, corrector: true, category: :readability
 
   alias Recode.Issue
   alias Recode.Task.PipeFunOne
diff --git a/lib/recode/task/single_pipe.ex b/lib/recode/task/single_pipe.ex
index 703f5f7..877690d 100644
--- a/lib/recode/task/single_pipe.ex
+++ b/lib/recode/task/single_pipe.ex
@@ -1,4 +1,6 @@
 defmodule Recode.Task.SinglePipe do
+  @shortdoc "Pipes should only be used when piping data through multiple calls."
+
   @moduledoc """
   Pipes (`|>`) should only be used when piping data through multiple calls.
 
@@ -18,11 +20,7 @@ defmodule Recode.Task.SinglePipe do
   This task rewrites the code when `mix recode` runs with `autocorrect: true`.
   """
 
-  @shortdoc "Pipes should only be used when piping data through multiple calls."
-
-  @category :readability
-
-  use Recode.Task, correct: true, check: true
+  use Recode.Task, corrector: true, category: :readability
 
   alias Recode.Issue
   alias Recode.Task.SinglePipe
diff --git a/lib/recode/task/specs.ex b/lib/recode/task/specs.ex
index 569a469..05b5e86 100644
--- a/lib/recode/task/specs.ex
+++ b/lib/recode/task/specs.ex
@@ -1,4 +1,6 @@
 defmodule Recode.Task.Specs do
+  @shortdoc "Checks for specs."
+
   @moduledoc """
   Function, macros and callbacks should have typespecs.
 
@@ -11,11 +13,7 @@ defmodule Recode.Task.Specs do
     * `:macros` - when `true`, macros are also checked, defaults to `false`.
   """
 
-  @shortdoc "Checks for specs."
-
-  @category :readability
-
-  use Recode.Task, check: true
+  use Recode.Task, category: :readability
 
   alias Recode.Context
   alias Recode.Issue
diff --git a/lib/recode/task/tag_fixme.ex b/lib/recode/task/tag_fixme.ex
new file mode 100644
index 0000000..c59dacb
--- /dev/null
+++ b/lib/recode/task/tag_fixme.ex
@@ -0,0 +1,35 @@
+defmodule Recode.Task.TagFIXME do
+  @shortdoc "Checks if there are FIXME tags in the sources."
+
+  @moduledoc """
+  #{@shortdoc}
+
+  FIXME tags in comments and docs are used as a reminder and should be handeld in
+  the near future.
+
+  ## Examples
+
+      # FIXME: this function returns a wrong value
+      def fun do
+        # ...
+      end
+
+  ## Options
+
+    * `:include_docs` - includes `@doc`, `@moduledoc` and `@shortdoc` to the
+                        check when set to `true`. Defaults to `true`.
+
+  """
+
+  use Recode.Task, category: :design
+
+  alias Recode.Task.Tags
+
+  @impl Recode.Task
+  def init(opts) do
+    Tags.init(Keyword.merge([tag: "FIXME", reporter: Recode.Task.TagFIXME], opts))
+  end
+
+  @impl Recode.Task
+  defdelegate run(source, opts), to: Tags
+end
diff --git a/lib/recode/task/tag_todo.ex b/lib/recode/task/tag_todo.ex
new file mode 100644
index 0000000..efd074d
--- /dev/null
+++ b/lib/recode/task/tag_todo.ex
@@ -0,0 +1,35 @@
+defmodule Recode.Task.TagTODO do
+  @shortdoc "Checks if there are TODO tags in the sources."
+
+  @moduledoc """
+  #{@shortdoc}
+
+  TODO tags in comments and docs are used as a reminder and should be handeld in
+  the near future.
+
+  ## Examples
+
+      # TODO: refactor this function
+      def fun do
+        # ...
+      end
+
+  ## Options
+
+    * `:include_docs` - includes `@doc`, `@moduledoc` and `@shortdoc` to the
+                        check when set to `true`. Defaults to `true`.
+
+  """
+
+  use Recode.Task, category: :design
+
+  alias Recode.Task.Tags
+
+  @impl Recode.Task
+  def init(opts) do
+    Tags.init(Keyword.merge([tag: "TODO", reporter: Recode.Task.TagTODO], opts))
+  end
+
+  @impl Recode.Task
+  defdelegate run(source, opts), to: Tags
+end
diff --git a/lib/recode/task/tags.ex b/lib/recode/task/tags.ex
new file mode 100644
index 0000000..1dbf2c8
--- /dev/null
+++ b/lib/recode/task/tags.ex
@@ -0,0 +1,117 @@
+defmodule Recode.Task.Tags do
+  @moduledoc false
+
+  use Recode.Task, category: :design
+
+  alias Recode.Issue
+  alias Rewrite.Source
+  alias Sourceror.Zipper
+
+  @defaults include_docs: true
+
+  @impl Recode.Task
+  def run(source, opts) do
+    source
+    |> Source.get(:quoted)
+    |> Zipper.zip()
+    |> Zipper.traverse([], fn zipper, issues ->
+      check_tags(zipper, issues, opts)
+    end)
+    |> update(source)
+  end
+
+  @impl Recode.Task
+  def init(opts) do
+    cond do
+      not Keyword.has_key?(opts, :tag) ->
+        {:error,
+         """
+         Recode.Task.Tags needs a configuration entry for :tag (e.g. tag: "TODO")\
+         """}
+
+      not Keyword.has_key?(opts, :reporter) ->
+        {:error,
+         """
+         Recode.Task.Tags needs a configuration entry for :reporter \
+         (e.g. reporter: "Recode.Task.TagTODO")\
+         """}
+
+      true ->
+        {:ok, Keyword.merge(@defaults, opts)}
+    end
+  end
+
+  defp update({_zipper, issues}, source) do
+    Source.add_issues(source, issues)
+  end
+
+  defp check_tags({{doc, _meta, args}, _zipper_meta} = zipper, issues, opts)
+       when doc in [:moduledoc, :doc, :shortdoc] do
+    issues =
+      if opts[:include_docs] && doc?(args) do
+        args
+        |> doc()
+        |> find_tags(opts)
+        |> Enum.concat(issues)
+      else
+        issues
+      end
+
+    {zipper, issues}
+  end
+
+  defp check_tags({{_op, meta, _args}, _zipper_meta} = zipper, issues, opts) do
+    issues =
+      meta
+      |> comments()
+      |> Enum.flat_map(fn comment -> find_tags(comment, opts) end)
+      |> Enum.concat(issues)
+
+    {zipper, issues}
+  end
+
+  defp check_tags(zipper, issues, _opts), do: {zipper, issues}
+
+  defp doc?([{:__block__, _meta, [text]}]), do: text != false
+
+  defp doc([{:__block__, meta, [text]}]) do
+    doc = Keyword.put(meta, :text, text)
+
+    case String.length(doc[:delimiter] || "") do
+      3 -> Keyword.update!(doc, :line, fn line -> line + 1 end)
+      _ -> doc
+    end
+  end
+
+  defp comments(meta) do
+    Keyword.get(meta, :leading_comments, []) ++ Keyword.get(meta, :trailing_comments, [])
+  end
+
+  defp find_tags(meta, opts) when is_map(meta) do
+    find_tags(Enum.into(meta, []), opts)
+  end
+
+  defp find_tags(meta, opts) do
+    meta
+    |> Keyword.get(:text, "")
+    |> String.split("\n")
+    |> find_tags(~r/^#?\s*#{opts[:tag]}/, 0, [])
+    |> issues(meta, opts)
+  end
+
+  defp find_tags([], _regex, _line_number, acc), do: acc
+
+  defp find_tags([line | lines], regex, line_number, acc) do
+    case Regex.match?(regex, line) do
+      true -> find_tags(lines, regex, line_number + 1, [{line_number, line} | acc])
+      false -> find_tags(lines, regex, line_number + 1, acc)
+    end
+  end
+
+  defp issues(tags, meta, opts) do
+    Enum.map(tags, fn {line, text} ->
+      text = Regex.replace(~r/^[#\s]*/, text, "")
+      Issue.new(opts[:reporter], "Found a tag: #{text}", line: line + meta[:line])
+    end)
+  end
+end
diff --git a/lib/recode/task/unused_variable.ex b/lib/recode/task/unused_variable.ex
index c898a1d..878d8da 100644
--- a/lib/recode/task/unused_variable.ex
+++ b/lib/recode/task/unused_variable.ex
@@ -1,13 +1,11 @@
 defmodule Recode.Task.UnusedVariable do
+  @shortdoc "Checks if unused variables occur."
+
   @moduledoc """
   Prepend unused variables with `_`.
   """
 
-  @shortdoc "Checks if unused variables occur."
-
-  @category :warning
-
-  use Recode.Task, correct: true, check: true
+  use Recode.Task, corrector: true, category: :warning
 
   alias Recode.Issue
   alias Rewrite.Source
diff --git a/lib/recode/utils.ex b/lib/recode/utils.ex
deleted file mode 100644
index e518a0f..0000000
--- a/lib/recode/utils.ex
+++ /dev/null
@@ -1,39 +0,0 @@
-defmodule Recode.Utils do
-  @moduledoc """
-  This module provides utility functions for `recode`.
-  """
-
-  @doc """
-  Returns `true` id `value` ends with `suffix`.
-
-  Both, value and suffix must be lists or module aliases.
-
-  ## Examples
-
-      iex> Recode.Utils.ends_with?(Foo.Bar, Bar)
-      true
-      iex> Recode.Utils.ends_with?(Foo, Foo.Bar)
-      false
-      iex> Recode.Utils.ends_with?(Foo.Bar, Baz)
-      false
-
-      iex> Recode.Utils.ends_with?([1, 2, 3], [2, 3])
-      true
-      iex> Recode.Utils.ends_with?([1, 2, 3], [2, 9])
-      false
-  """
-  @spec ends_with?(value :: list() | module(), suffix :: list() | module()) :: boolean
-  def ends_with?(value, suffix) when is_atom(value) and is_atom(suffix) do
-    list = Module.split(value)
-    suffix = Module.split(suffix)
-
-    ends_with?(list, suffix)
-  end
-
-  def ends_with?(list, suffix) when is_list(list) and is_list(suffix) do
-    case length(list) - length(suffix) do
-      diff when diff < 0 -> false
-      diff -> Enum.drop(list, diff) == suffix
-    end
-  end
-end
diff --git a/mix.exs b/mix.exs
index fe6ec45..9090698 100644
--- a/mix.exs
+++ b/mix.exs
@@ -41,12 +41,16 @@ defmodule Recode.MixProject do
         Tasks: [
           Recode.Task.AliasExpansion,
           Recode.Task.AliasOrder,
+          Recode.Task.Dbg,
           Recode.Task.EnforceLineLength,
           Recode.Task.FilterCount,
           Recode.Task.Format,
+          Recode.Task.Nesting,
           Recode.Task.PipeFunOne,
           Recode.Task.SinglePipe,
           Recode.Task.Specs,
+          Recode.Task.TagFIXME,
+          Recode.Task.TagTODO,
           Recode.Task.TestFileExt,
           Recode.Task.UnusedVariable
         ]


Test patch:
diff --git a/lib/recode/task/test_file_ext.ex b/lib/recode/task/test_file_ext.ex
index 7eca51e..bfbfcb4 100644
--- a/lib/recode/task/test_file_ext.ex
+++ b/lib/recode/task/test_file_ext.ex
@@ -1,4 +1,6 @@
 defmodule Recode.Task.TestFileExt do
+  @shortdoc "Checks the file extension of test files."
+
   @moduledoc """
   Tests must be in a file with the extension `*_test.exs`.
 
@@ -6,11 +8,7 @@ defmodule Recode.Task.TestFileExt do
   issue.
   """
 
-  @shortdoc "Checks the file extension of test files."
-
-  @category :warning
-
-  use Recode.Task, correct: true, check: true
+  use Recode.Task, corrector: true, category: :warning
 
   alias Recode.Issue
   alias Recode.Task.TestFileExt
@@ -32,10 +30,6 @@ defmodule Recode.Task.TestFileExt do
     String.replace(path, ~r/_test\.ex$/, "_test.exs")
   end
 
-  defp update_path(nil) do
-    nil
-  end
-
   defp update_source(source, path, true) do
     Source.update(source, TestFileExt, :path, path)
   end
diff --git a/test/mix/tasks/recode.help_test.exs b/test/mix/tasks/recode.help_test.exs
index b55e2d8..6ff4441 100644
--- a/test/mix/tasks/recode.help_test.exs
+++ b/test/mix/tasks/recode.help_test.exs
@@ -12,21 +12,24 @@ defmodule Mix.Tasks.Recode.Gen.HelpTest do
       end)
 
     assert output == """
+           Design tasks:
+           TagFIXME          # Checker   - Checks if there are FIXME tags in the sources.
+           TagTODO           # Checker   - Checks if there are TODO tags in the sources.
            Readability tasks:
-           AliasExpansion    # Exapnds multi aliases to separate aliases.
-           AliasOrder        # Checks if aliases are sorted alphabetically.
-           EnforceLineLength # Forces expressions to one line.
-           Format            # Does the same as `mix format`.
-           PipeFunOne        # Add parentheses to one-arity functions.
-           SinglePipe        # Pipes should only be used when piping data through multiple calls.
-           Specs             # Checks for specs.
+           AliasExpansion    # Corrector - Exapnds multi aliases to separate aliases.
+           AliasOrder        # Corrector - Checks if aliases are sorted alphabetically.
+           EnforceLineLength # Corrector - Forces expressions to one line.
+           Format            # Corrector - Does the same as `mix format`.
+           PipeFunOne        # Corrector - Add parentheses to one-arity functions.
+           SinglePipe        # Corrector - Pipes should only be used when piping data through multiple calls.
+           Specs             # Checker   - Checks for specs.
            Refactor tasks:
-           FilterCount       # Checks calls like Enum.filter(...) |> Enum.count().
-           Nesting           # Checks code nesting depth in functions and macros.
+           FilterCount       # Corrector - Checks calls like Enum.filter(...) |> Enum.count().
+           Nesting           # Checker   - Checks code nesting depth in functions and macros.
            Warning tasks:
-           Dbg               # There should be no calls to dbg.
-           TestFileExt       # Checks the file extension of test files.
-           UnusedVariable    # Checks if unused variables occur.
+           Dbg               # Corrector - There should be no calls to dbg.
+           TestFileExt       # Corrector - Checks the file extension of test files.
+           UnusedVariable    # Corrector - Checks if unused variables occur.
            """
   end
 
@@ -40,7 +43,11 @@ defmodule Mix.Tasks.Recode.Gen.HelpTest do
   end
 
   test "mix recode.help FooBar" do
-    assert_raise Mix.Error, "task FooBar not found", fn ->
+    message = """
+    The recode task FooBar could not be found. Run "mix recode.help" for a list of recode tasks.\
+    """
+
+    assert_raise Mix.Error, message, fn ->
       Help.run(["FooBar"])
     end
   end
diff --git a/test/mix/tasks/recode_test.exs b/test/mix/tasks/recode_test.exs
index 17d6e00..d5b66d4 100644
--- a/test/mix/tasks/recode_test.exs
+++ b/test/mix/tasks/recode_test.exs
@@ -27,8 +27,10 @@ defmodule Mix.Tasks.RecodeTest do
       ":test" |> source("test.exs") |> project()
     end)
 
-    assert catch_exit(Tasks.Recode.run(["--config", "test/fixtures/config.exs", "--dry"])) ==
-             :normal
+    capture_io(fn ->
+      assert catch_exit(Tasks.Recode.run(["--config", "test/fixtures/config.exs", "--dry"])) ==
+               :normal
+    end)
   end
 
   test "mix recode --config test/fixtures/config.exs -" do
@@ -39,7 +41,10 @@ defmodule Mix.Tasks.RecodeTest do
       ":test" |> source("test.exs") |> project()
     end)
 
-    assert catch_exit(Tasks.Recode.run(["--config", "test/fixtures/config.exs", "-"])) == :normal
+    capture_io(fn ->
+      assert catch_exit(Tasks.Recode.run(["--config", "test/fixtures/config.exs", "-"])) ==
+               :normal
+    end)
   end
 
   test "mix recode file_1.ex file_2.ex" do
@@ -50,10 +55,12 @@ defmodule Mix.Tasks.RecodeTest do
       ":test" |> source("test.exs") |> project()
     end)
 
-    assert catch_exit(
-             Tasks.Recode.run(["--config", "test/fixtures/config.exs", "file_1.ex", "file_2.ex"])
-           ) ==
-             :normal
+    capture_io(fn ->
+      assert catch_exit(
+               Tasks.Recode.run(["--config", "test/fixtures/config.exs", "file_1.ex", "file_2.ex"])
+             ) ==
+               :normal
+    end)
   end
 
   test "mix recode raises exception for unknown config file" do
diff --git a/test/recode/runner/impl_test.exs b/test/recode/runner/impl_test.exs
index 9142ef5..696a9fa 100644
--- a/test/recode/runner/impl_test.exs
+++ b/test/recode/runner/impl_test.exs
@@ -8,6 +8,8 @@ defmodule Recode.Runner.ImplTest do
   alias Recode.Task.SinglePipe
   alias Recode.TaskMock
 
+  @task_config __recode_task_config__: [checker: true, corrector: true]
+
   setup :verify_on_exit!
 
   setup_all context do
@@ -44,7 +46,7 @@ defmodule Recode.Runner.ImplTest do
         assert config == [autocorrect: true]
         source
       end)
-      |> expect(:config, fn :correct -> true end)
+      |> expect(:__attributes__, fn -> @task_config end)
 
       config = Keyword.put(config, :tasks, [{TaskMock, []}])
 
@@ -59,7 +61,7 @@ defmodule Recode.Runner.ImplTest do
         assert config == [autocorrect: true, foo: :bar]
         source
       end)
-      |> expect(:config, fn :correct -> true end)
+      |> expect(:__attributes__, fn -> @task_config end)
 
       config = Keyword.put(config, :tasks, [{TaskMock, config: [foo: :bar]}])
 
@@ -77,7 +79,7 @@ defmodule Recode.Runner.ImplTest do
         assert source.code == code
         source
       end)
-      |> expect(:config, fn :correct -> true end)
+      |> expect(:__attributes__, fn -> @task_config end)
 
       capture_io(code, fn -> assert Runner.run(config) end)
     end
@@ -88,7 +90,7 @@ defmodule Recode.Runner.ImplTest do
         assert config == [autocorrect: true, foo: :bar]
         source
       end)
-      |> expect(:config, 1, fn :correct -> true end)
+      |> expect(:__attributes__, fn -> @task_config end)
 
       config =
         Keyword.put(config, :tasks, [
@@ -107,7 +109,7 @@ defmodule Recode.Runner.ImplTest do
         assert config == [autocorrect: true, foo: :bar]
         source
       end)
-      |> expect(:config, 1, fn :correct -> true end)
+      |> expect(:__attributes__, fn -> @task_config end)
 
       config =
         Keyword.put(config, :tasks, [
@@ -128,7 +130,7 @@ defmodule Recode.Runner.ImplTest do
       |> expect(:run, 2, fn _source, _config ->
         raise "An Exception Occurred"
       end)
-      |> expect(:config, 1, fn :correct -> true end)
+      |> expect(:__attributes__, fn -> @task_config end)
 
       config = Keyword.put(config, :tasks, [{TaskMock, []}])
 
diff --git a/test/recode/stop_watch_test.exs b/test/recode/stop_watch_test.exs
new file mode 100644
index 0000000..428f5e5
--- /dev/null
+++ b/test/recode/stop_watch_test.exs
@@ -0,0 +1,18 @@
+defmodule Recode.StopWatchTest do
+  use ExUnit.Case
+
+  alias Recode.StopWatch
+
+  doctest Recode.StopWatch
+
+  describe "init/1" do
+    test "raises an ArgumentError when already initalized" do
+      message = "StopWatch :foo already exisits."
+
+      assert_raise(ArgumentError, message, fn ->
+        StopWatch.init!(:foo)
+        StopWatch.init!(:foo)
+      end)
+    end
+  end
+end
diff --git a/test/recode/task/alias_order_test.exs b/test/recode/task/alias_order_test.exs
index fb373cb..9ca29ab 100644
--- a/test/recode/task/alias_order_test.exs
+++ b/test/recode/task/alias_order_test.exs
@@ -146,6 +146,29 @@ defmodule Recode.Task.AliasOrderTest do
     |> assert_issues(2)
   end
 
+  test "reports issues for unordered multiple groups with dots" do
+    """
+    defmodule MyModule do
+      alias Yankee.{Bravo.Kilo, Alpha.Lima}
+      alias Zulu.{Foxtrot, Echo}
+    end
+    """
+    |> run_task(AliasOrder, autocorrect: false)
+    |> assert_issues(2)
+  end
+
+  test "reports issues for unordered multiple groups with multi dots" do
+    """
+    defmodule MyModule do
+      alias Whiskey.{Bravo.Charlie.Lima, Bravo.Charlie.Kilo}
+      alias Yankee.{Bravo.Charlie.Lima, Bravo.Charlie}
+      alias Zulu.{Bravo.Charlie, Bravo.Charlie.Lima}
+    end
+    """
+    |> run_task(AliasOrder, autocorrect: false)
+    |> assert_issues(2)
+  end
+
   test "reports no issues" do
     """
     defmodule MyModule do
@@ -259,6 +282,18 @@ defmodule Recode.Task.AliasOrderTest do
       |> assert_issue()
     end
 
+    test "reports an issue for unsorted aliases in multi with and without __MODULE__" do
+      """
+      defmodule MyModule do
+        alias Alpha.Bravo
+        alias Alpha.Bravo.{Charlie, Delta}
+        alias __MODULE__.{UserSocket, Endpoint}
+      end
+      """
+      |> run_task(AliasOrder, autocorrect: false)
+      |> assert_issue()
+    end
+
     test "sorts aliases with __MODULE__" do
       code = """
       defmodule MyModule do
diff --git a/test/recode/task/nesting_test.exs b/test/recode/task/nesting_test.exs
index bc30098..a43a0e3 100644
--- a/test/recode/task/nesting_test.exs
+++ b/test/recode/task/nesting_test.exs
@@ -20,7 +20,7 @@ defmodule Recode.Task.NestingTest do
         end
       end
       """
-      |> run_task(Nesting, max_depth: 2)
+      |> run_task(Nesting)
       |> refute_issues()
     end
 
@@ -51,7 +51,7 @@ defmodule Recode.Task.NestingTest do
         end
       end
       """
-      |> run_task(Nesting, max_depth: 2)
+      |> run_task(Nesting)
       |> refute_issues()
     end
 
@@ -93,7 +93,7 @@ defmodule Recode.Task.NestingTest do
         end
       end
       """
-      |> run_task(Nesting, max_depth: 2)
+      |> run_task(Nesting)
       |> assert_issue_with(reporter: Nesting, line: 5)
     end
 
@@ -117,7 +117,7 @@ defmodule Recode.Task.NestingTest do
         end
       end
       """
-      |> run_task(Nesting, max_depth: 2)
+      |> run_task(Nesting)
       |> assert_issue_with(reporter: Nesting, line: 5)
     end
 
@@ -169,7 +169,7 @@ defmodule Recode.Task.NestingTest do
         end
       end
       """
-      |> run_task(Nesting, max_depth: 2)
+      |> run_task(Nesting)
       |> assert_issues(2)
     end
 
@@ -185,8 +185,14 @@ defmodule Recode.Task.NestingTest do
         end
       end
       """
-      |> run_task(Nesting, max_depth: 2)
+      |> run_task(Nesting)
       |> assert_issue_with(reporter: Nesting, line: 5)
     end
   end
+
+  describe "init/1" do
+    test "sets default max_depth" do
+      assert Nesting.init([]) == {:ok, max_depth: 2}
+    end
+  end
 end
diff --git a/test/recode/task/specs_test.exs b/test/recode/task/specs_test.exs
index 3c48e5f..b99f96b 100644
--- a/test/recode/task/specs_test.exs
+++ b/test/recode/task/specs_test.exs
@@ -192,4 +192,20 @@ defmodule Recode.Task.SpecsTest do
     |> run_task(Specs, macros: true)
     |> assert_issue_with(reporter: Specs)
   end
+
+  test "reports no issues for macro __using__" do
+    code = """
+    defmodule MyModule do
+      defmacro __using__(_opts) do
+        quote do
+          import MyModule.Foo
+        end
+      end
+    end
+    """
+
+    code
+    |> run_task(Specs, macros: true)
+    |> refute_issues()
+  end
 end
diff --git a/test/recode/task/tag_fixme_test.exs b/test/recode/task/tag_fixme_test.exs
new file mode 100644
index 0000000..01cf13e
--- /dev/null
+++ b/test/recode/task/tag_fixme_test.exs
@@ -0,0 +1,76 @@
+defmodule Recode.Task.TagFIXMETest do
+  use RecodeCase
+
+  alias Recode.Task.TagFIXME
+
+  describe "run/1" do
+    #
+    # cases NOT raising issues
+    #
+
+    test "does not trigger" do
+      ~s'''
+      defmodule FIXME do
+        @moduledoc """
+        The FIXME module
+        """
+
+        # Returns FIXME atom
+        def todo(x), do: :FIXME
+      end
+      '''
+      |> run_task(TagFIXME)
+      |> refute_issues()
+    end
+
+    test "does not triggers tags in doc when deactivated" do
+      ~s'''
+      defmodule FIXME do
+        @moduledoc """
+        The FIXME module
+        FIXME add examples
+        """
+
+        def todo(x), do: :FIXME
+      end
+      '''
+      |> run_task(TagFIXME, include_docs: false)
+      |> refute_issues()
+    end
+
+    #
+    # cases NOT raising issues
+    #
+
+    test "triggers an issue for a tag in comment" do
+      ~s'''
+      defmodule FIXME do
+        @moduledoc """
+        The FIXME module
+        """
+
+        # FIXME: add spec
+        def todo(x), do: :FIXME
+      end
+      '''
+      |> run_task(TagFIXME)
+      |> assert_issue_with(
+        reporter: TagFIXME,
+        line: 6,
+        message: "Found a tag: FIXME: add spec"
+      )
+    end
+  end
+
+  describe "init/1" do
+    test "returns an ok tuple with added defaults" do
+      assert TagFIXME.init(tag: "FIXME", reporter: TagFIXME) ==
+               {:ok, include_docs: true, tag: "FIXME", reporter: TagFIXME}
+    end
+
+    test "returns an ok tuple" do
+      assert TagFIXME.init(tag: "FIXME", reporter: TagFIXME, include_docs: false) ==
+               {:ok, tag: "FIXME", reporter: TagFIXME, include_docs: false}
+    end
+  end
+end
diff --git a/test/recode/task/tag_todo_test.exs b/test/recode/task/tag_todo_test.exs
new file mode 100644
index 0000000..825a813
--- /dev/null
+++ b/test/recode/task/tag_todo_test.exs
@@ -0,0 +1,76 @@
+defmodule Recode.Task.TagTODOTest do
+  use RecodeCase
+
+  alias Recode.Task.TagTODO
+
+  describe "run/1" do
+    #
+    # cases NOT raising issues
+    #
+
+    test "does not trigger" do
+      ~s'''
+      defmodule TODO do
+        @moduledoc """
+        The TODO module
+        """
+
+        # Returns TODO atom
+        def todo(x), do: :TODO
+      end
+      '''
+      |> run_task(TagTODO)
+      |> refute_issues()
+    end
+
+    test "does not triggers tags in doc when deactivated" do
+      ~s'''
+      defmodule TODO do
+        @moduledoc """
+        The TODO module
+        TODO add examples
+        """
+
+        def todo(x), do: :TODO
+      end
+      '''
+      |> run_task(TagTODO, include_docs: false)
+      |> refute_issues()
+    end
+
+    #
+    # cases NOT raising issues
+    #
+
+    test "triggers an issue for a tag in comment" do
+      ~s'''
+      defmodule TODO do
+        @moduledoc """
+        The TODO module
+        """
+
+        # TODO: add spec
+        def todo(x), do: :TODO
+      end
+      '''
+      |> run_task(TagTODO)
+      |> assert_issue_with(
+        reporter: TagTODO,
+        line: 6,
+        message: "Found a tag: TODO: add spec"
+      )
+    end
+  end
+
+  describe "init/1" do
+    test "returns an ok tuple with added defaults" do
+      assert TagTODO.init(tag: "TODO", reporter: TagTODO) ==
+               {:ok, include_docs: true, tag: "TODO", reporter: TagTODO}
+    end
+
+    test "returns an ok tuple" do
+      assert TagTODO.init(tag: "TODO", reporter: TagTODO, include_docs: false) ==
+               {:ok, tag: "TODO", reporter: TagTODO, include_docs: false}
+    end
+  end
+end
diff --git a/test/recode/task/tags_test.exs b/test/recode/task/tags_test.exs
new file mode 100644
index 0000000..2519e7c
--- /dev/null
+++ b/test/recode/task/tags_test.exs
@@ -0,0 +1,241 @@
+defmodule Recode.Task.TagsTest do
+  use RecodeCase
+
+  alias Recode.Task.Tags
+
+  describe "run/1" do
+    #
+    # cases NOT raising issues
+    #
+
+    test "does not trigger" do
+      ~s'''
+      defmodule TODO do
+        @moduledoc """
+        The TODO module
+        """
+
+        # Returns TODO atom
+        def todo(x), do: :TODO
+      end
+      '''
+      |> run_task(Tags, tag: "TODO", reporter: Tags)
+      |> refute_issues()
+    end
+
+    test "does not triggers tags in doc when deactivated" do
+      ~s'''
+      defmodule TODO do
+        @moduledoc """
+        The TODO module
+        TODO add examples
+        """
+
+        def todo(x), do: :TODO
+      end
+      '''
+      |> run_task(Tags, tag: "TODO", reporter: Tags, include_docs: false)
+      |> refute_issues()
+    end
+
+    test "triggers no issue when @doc is false" do
+      ~s'''
+      defmodule Foo do
+        @moduledoc false
+
+        @doc false
+        def foo(x), do: x
+      end
+      '''
+      |> run_task(Tags, tag: "TODO", reporter: Tags)
+      |> refute_issues()
+    end
+
+    test "triggers no issue a comment in a keyword list" do
+      ~s'''
+      defmodule Foo do
+        def foo do
+          [
+            a: 5,
+            # a comment
+            b: 1
+          ]
+        end
+      end
+      '''
+      |> run_task(Tags, tag: "TODO", reporter: Tags)
+      |> refute_issues()
+    end
+
+    #
+    # cases raising issues
+    #
+
+    test "triggers an issue for a tag in comment" do
+      ~s'''
+      defmodule TODO do
+        @moduledoc """
+        The TODO module
+        """
+
+        # TODO: add spec
+        def todo(x), do: :TODO
+      end
+      '''
+      |> run_task(Tags, tag: "TODO", reporter: Tags)
+      |> assert_issue_with(
+        reporter: Tags,
+        line: 6,
+        message: "Found a tag: TODO: add spec"
+      )
+    end
+
+    test "triggers multiple issues for tags in one comment" do
+      ~s'''
+      defmodule TODO do
+        @moduledoc """
+        The TODO module
+        """
+
+        # TODO: add spec
+        # TODO: add spec
+        def todo(x), do: :TODO
+      end
+      '''
+      |> run_task(Tags, tag: "TODO", reporter: Tags)
+      |> assert_issues(2)
+    end
+
+    test "triggers an issue for a tag in moduledoc" do
+      ~s'''
+      defmodule TODO do
+        @moduledoc """
+        The TODO module
+        TODO add examples
+        """
+
+        def todo(x), do: :TODO
+      end
+      '''
+      |> run_task(Tags, tag: "TODO", reporter: Tags)
+      |> assert_issue_with(
+        reporter: Tags,
+        line: 4,
+        message: "Found a tag: TODO add examples"
+      )
+    end
+
+    test "triggers an issue for a tag in oneline moduledoc" do
+      ~s'''
+      defmodule TODO do
+        @moduledoc "TODO add impl"
+      end
+      '''
+      |> run_task(Tags, tag: "TODO", reporter: Tags)
+      |> assert_issue_with(
+        reporter: Tags,
+        line: 2,
+        message: "Found a tag: TODO add impl"
+      )
+    end
+
+    test "triggers an issue for a tag in oneline doc" do
+      ~s'''
+      defmodule TODO do
+        @doc "TODO add impl"
+        def todo, do: :todo
+      end
+      '''
+      |> run_task(Tags, tag: "TODO", reporter: Tags)
+      |> assert_issue_with(
+        reporter: Tags,
+        line: 2,
+        message: "Found a tag: TODO add impl"
+      )
+    end
+
+    test "triggers an issue for a tag in doc" do
+      ~s'''
+      defmodule TODO do
+        @doc """
+        TODO add impl
+        """
+        def todo, do: :todo
+      end
+      '''
+      |> run_task(Tags, tag: "TODO", reporter: Tags)
+      |> assert_issue_with(
+        reporter: Tags,
+        line: 3,
+        message: "Found a tag: TODO add impl"
+      )
+    end
+
+    test "triggers an issue for a tag in doc with trailing and penidng text" do
+      ~s'''
+      defmodule TODO do
+        @doc """
+        Lorem ispum ...
+
+        TODO do it
+
+        Lorem ispum ...
+        """
+        def todo, do: :todo
+      end
+      '''
+      |> run_task(Tags, tag: "TODO", reporter: Tags)
+      |> assert_issue_with(
+        reporter: Tags,
+        line: 5,
+        message: "Found a tag: TODO do it"
+      )
+    end
+
+    test "triggers an issue for a tag in a keyword list" do
+      ~s'''
+      defmodule Foo do
+        def foo do
+          [
+            a: 5,
+            # TODO: add b
+            c: 1
+          ]
+        end
+      end
+      '''
+      |> run_task(Tags, tag: "TODO", reporter: Tags)
+      |> assert_issue_with(
+        reporter: Tags,
+        line: 5,
+        message: "Found a tag: TODO: add b"
+      )
+    end
+  end
+
+  describe "init/1" do
+    test "returns an error tuple for missing :tag" do
+      message = ~s|Recode.Task.Tags needs a configuration entry for :tag (e.g. tag: "TODO")|
+      assert Tags.init([]) == {:error, message}
+    end
+
+    test "returns an error tuple for missing :reporter" do
+      message = """
+      Recode.Task.Tags needs a configuration entry for :reporter \
+      (e.g. reporter: "Recode.Task.TagTODO")\
+      """
+
+      assert Tags.init(tag: "TODO") == {:error, message}
+    end
+
+    test "returns an ok tuple with added defaults" do
+      assert Tags.init(tag: "TODO", reporter: Tags) ==
+               {:ok, include_docs: true, tag: "TODO", reporter: Tags}
+    end
+
+    test "returns an ok tuple" do
+      assert Tags.init(tag: "TODO", reporter: Tags, include_docs: false) ==
+               {:ok, tag: "TODO", reporter: Tags, include_docs: false}
+    end
+  end
+end
diff --git a/test/recode/task/unused_variable_test.exs b/test/recode/task/unused_variable_test.exs
index 6d5325d..9bb6a48 100644
--- a/test/recode/task/unused_variable_test.exs
+++ b/test/recode/task/unused_variable_test.exs
@@ -24,7 +24,7 @@ defmodule Recode.Task.UnusedVariableTest do
       |> assert_code(expected)
     end
 
-    test "fix simple unused variables in comments" do
+    test "ignores comments" do
       code = """
       def foo(bar) do
         # bar is not used
@@ -42,24 +42,15 @@ defmodule Recode.Task.UnusedVariableTest do
       |> assert_code(expected)
     end
 
-    test "fix in fun call" do
-      code = """
-      def foo() do
-        bar = String.to_atom()
-        IO.inspect(bar)
-      end
+    test "keeps code" do
       """
-
-      expected = """
       def foo() do
         bar = String.to_atom()
         IO.inspect(bar)
       end
       """
-
-      code
       |> run_task(UnusedVariable, autocorrect: true)
-      |> assert_code(expected)
+      |> refute_update()
     end
 
     test "fix in module" do
diff --git a/test/recode/task_test.exs b/test/recode/task_test.exs
new file mode 100644
index 0000000..74d65df
--- /dev/null
+++ b/test/recode/task_test.exs
@@ -0,0 +1,58 @@
+defmodule Recode.TaskTest do
+  use ExUnit.Case
+
+  alias Recode.Task
+
+  defmodule Dummy do
+    @shortdoc "Lorem ispum Dummy"
+
+    use Recode.Task
+
+    @impl true
+    def run(source, _opts), do: source
+  end
+
+  defmodule CorrectorDummy do
+    @shortdoc "Lorem ispum CorrectorDummy"
+
+    use Recode.Task, corrector: true, checker: false, category: :test
+
+    @impl true
+    def run(source, _opts), do: source
+
+    @impl true
+    def init(config) do
+      if Keyword.has_key?(config, :test) do
+        {:ok, config}
+      else
+        {:error, "missing :test"}
+      end
+    end
+  end
+
+  test "corrector?/1" do
+    assert Task.corrector?(Dummy) == false
+    assert Task.corrector?(CorrectorDummy) == true
+  end
+
+  test "checker?/1" do
+    assert Task.checker?(Dummy) == true
+    assert Task.checker?(CorrectorDummy) == false
+  end
+
+  test "category/1" do
+    assert Task.category(Dummy) == nil
+    assert Task.category(CorrectorDummy) == :test
+  end
+
+  test "shortdoc/1" do
+    assert Task.shortdoc(Dummy) == "Lorem ispum Dummy"
+    assert Task.shortdoc(CorrectorDummy) == "Lorem ispum CorrectorDummy"
+  end
+
+  test "init/1" do
+    assert Dummy.init(foo: :bar) == {:ok, foo: :bar}
+    assert CorrectorDummy.init(foo: :bar) == {:error, "missing :test"}
+    assert CorrectorDummy.init(test: :bar) == {:ok, test: :bar}
+  end
+end
diff --git a/test/recode/utils_test.exs b/test/recode/utils_test.exs
deleted file mode 100644
index f5cef26..0000000
--- a/test/recode/utils_test.exs
+++ /dev/null
@@ -1,5 +0,0 @@
-defmodule Recode.UtilsTest do
-  use ExUnit.Case
-
-  doctest Recode.Utils
-end
diff --git a/test/support/recode_case.ex b/test/support/recode_case.ex
index 7e14d1d..821e584 100644
--- a/test/support/recode_case.ex
+++ b/test/support/recode_case.ex
@@ -83,12 +83,11 @@ defmodule RecodeCase do
   def run_task(code, task, opts \\ [])
 
   def run_task(code, task, opts) when is_binary(code) do
-    code
-    |> Source.Ex.from_string()
-    |> task.run(opts)
+    run_task(Source.Ex.from_string(code), task, opts)
   end
 
   def run_task(%Source{} = source, task, opts) do
+    {:ok, opts} = task.init(opts)
     task.run(source, opts)
   end