# Recode
[](https://hex.pm/packages/recode)
[](https://github.com/hrzndhrn/recode/actions)
[](https://github.com/hrzndhrn//blob/main/LICENSE.md)
A linter with autocorrection.
`recode` is an experimental project to play around with the great
[`sourceror`](https://github.com/doorgan/sourceror) package by @doorgan.
This library is still under development, breaking changes are expected.
`recode` can correct and check the following things:
```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`.
LocalsWithoutParens # Corrector - Removes parens from locals without parens.
Moduledoc # Checker - There should be a @moduledoc in any module.
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.
UnnecessaryIfUnless # Corrector - Removes redundant booleans
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.
IOInspect # Corrector - There should be no calls to IO.inspect.
TestFile # 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.
## Installation
The package can be installed by adding `recode` to your list of dependencies
in `mix.exs`:
```elixir
def deps do
[
{:recode, "~> 0.7", only: :dev, runtime: false}
]
end
```
`Recode` requires Elixir 1.13.0 or higher. If you add `recode` to a project that
supports lower Elixir versions you could add recode as following:
```elixir
def deps do
[
# your deps
] ++ recode()
end
defp recode() do
case Version.match?(System.version(), "~> 1.13") do
true -> [{:recode, "~> 0.7", only: :dev, runtime: false}]
false -> []
end
end
```
Documentation can be found at [https://hexdocs.pm/recode](https://hexdocs.pm/recode).
## Usage
To start with `recode` a configuration file is needed.
```sh
mix recode.gen.config
```
This mix task generates the config file `.recode.exs`.
```elixir
[
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.
# 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`.
verbose: false,
# Can be overwritten by calling `mix recode "lib/**/*.ex"`.
inputs: ["{mix,.formatter}.exs", "{apps,config,lib,test}/**/*.{ex,exs}"],
formatters: [Recode.CLIFormatter],
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
# 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.FilterCount, []},
{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.TestFile, []},
{Recode.Task.UnnecessaryIfUnless, []},
{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 task runs the linter with autocorrection. The switch `--dry` (alias
`-d`) prevents the update of the files and shows all changes in the console.
```
> cd examples/my_code
> mix recode --dry --no-color --no-manifest
Read 24 files in 0.01s
...................................................!...............!!.........................................................................!....!..!................................!........!...............!..!...!..................!....!.....!................!........!.....!...............!...!!..!.........!...!.!........!!..!..!!!.!....
File: .formatter.exs
Updates: 1
Changed by: Format
...|
13 13 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}", "priv/**/*.json"],
14 14 | locals_without_parens: [noop: 1],
15 - | subdirectories: ["priv/*/migrations"],
15 + | subdirectories: ["priv/*/migrations"]
16 16 |]
17 17 |
File: lib/my_code.ex
[Specs 15/3] Functions should have a @spec type specification.
File: lib/my_code/alias_expansion.ex
Updates: 1
Changed by: AliasExpansion
1 1 |defmodule MyCode.AliasExpansion do
2 - | alias MyCode.{PipeFunOne, SinglePipe}
2 + | alias MyCode.PipeFunOne
3 + | alias MyCode.SinglePipe
3 4 |
4 5 | def foo(x) do
...|
[Moduledoc 1/1] The module Elixir.MyCode.AliasExpansion is missing @moduledoc.
[Specs 5/3] Functions should have a @spec type specification.
File: lib/my_code/alias_order.ex
Updates: 2
Changed by: AliasOrder, AliasExpansion
...|
12 12 |
13 13 |defmodule Mycode.AliasOrder do
14 - | alias MyCode.SinglePipe
14 + | alias MyCode.Echo
15 + | alias MyCode.Foxtrot
15 16 | alias MyCode.PipeFunOne
16 - | alias MyCode.{Foxtrot, Echo}
17 + | alias MyCode.SinglePipe
17 18 |
18 19 | @doc false
...|
[Moduledoc 13/1] The module Elixir.Mycode.AliasOrder is missing @moduledoc.
File: lib/my_code/deep.ex
[Moduledoc 1/1] The module Elixir.MyCode.Deep is missing @moduledoc.
[Specs 2/3] Functions should have a @spec type specification.
[Nesting 6/11] The body is nested too deep (max depth: 2).
File: lib/my_code/multi.ex
Updates: 4
Changed by: SinglePipe, PipeFunOne, FilterCount, Format
1 1 |defmodule MyCode.Multi do
2 - |
3 2 | import MyCode.Fun
4 3 |
...|
6 5 |
7 6 | def pipe(x) do
8 - | x |> double |> double() |> dbg()
7 + | x |> double() |> double() |> dbg()
9 8 | end
10 9 |
11 10 | def single(x) do
12 - | x |> double()
11 + | double(x)
13 12 | end
14 13 |
...|
19 18 | def my_count(list) do
20 19 | list
21 - | |> Enum.filter(fn x -> rem(x, 2) == 0 end)
22 - | |> Enum.count()
20 + | |> Enum.count(fn x -> rem(x, 2) == 0 end)
23 21 | |> IO.inspect()
24 22 | end
...|
[Moduledoc 1/1] The module Elixir.MyCode.Multi is missing @moduledoc.
[Specs 4/3] Functions should have a @spec type specification.
[Specs 6/3] Functions should have a @spec type specification.
[Dbg 7/34] There should be no calls to dbg.
[Specs 10/3] Functions should have a @spec type specification.
[Specs 14/3] Functions should have a @spec type specification.
[Specs 18/3] Functions should have a @spec type specification.
[IOInspect 21/8] There should be no calls to IO.inspect.
File: lib/my_code/pipe_fun_one.ex
Updates: 1
Changed by: PipeFunOne
...|
5 5 |
6 6 | def pipe(x) do
7 - | x |> double |> double()
7 + | x |> double() |> double()
8 8 | end
9 9 |end
...|
File: lib/my_code/same_line.ex
[Moduledoc 1/1] The module Elixir.MyCode.SameLine is missing @moduledoc.
[Specs 2/3] Functions should have a @spec type specification.
File: lib/my_code/single_pipe.ex
Updates: 1
Changed by: SinglePipe
...|
5 5 |
6 6 | def single_pipe(x) do
7 - | x |> double()
7 + | double(x)
8 8 | end
9 9 |
10 - | def reverse(a), do: a |> Enum.reverse()
10 + | def reverse(a), do: Enum.reverse(a)
11 11 |end
12 12 |
File: lib/my_code/tags.ex
[TagTODO 3/-] Found a tag: TODO: add docs
[TagFIXME 6/-] Found a tag: FIXME: add more functions
[Specs 7/3] Functions should have a @spec type specification.
File: lib/my_code/trailing_comma.ex
Updates: 2
Changed by: SinglePipe, Format
...|
3 3 |
4 4 | def list do
5 - | [
5 + | Enum.reverse([
6 6 | 100_000,
7 7 | 200_000,
...|
14 14 | 900_000,
15 15 | 1_000_000,
16 - | 2_000_000,
17 - | ] |> Enum.reverse()
16 + | 2_000_000
17 + | ])
18 18 | end
19 19 |end
...|
File: mix.exs
[Moduledoc 1/1] The module Elixir.MyCode.MixProject is missing @moduledoc.
File: priv/repo/migrations/20190417140000_create_users.exs
Updates: 2
Changed by: LocalsWithoutParens, Format
...|
4 4 | def up do
5 5 | create table("users") do
6 - | add :first_name, :string, size: 40
7 - | add(:last_name, :string, size: 40)
6 + | add :first_name, :string, size: 40
7 + | add :last_name, :string, size: 40
8 8 |
9 9 | timestamps()
...|
[Moduledoc 1/1] The module Elixir.MyRepo.Migrations.CreateUsers is missing @moduledoc.
[Specs 4/3] Functions should have a @spec type specification.
[Specs 13/3] Functions should have a @spec type specification.
File: test/my_code_test.exs
Updates: 1
Changed by: TestFile
Moved from: test/my_code_test.ex
[Moduledoc 1/1] The module Elixir.MyCodeTest is missing @moduledoc.
[TestFile -/-] The file must be renamed to test/my_code_test.exs so that ExUnit can find it.
Completed 342 tasks in 0.02s.
Files processed: 24 (.ex: 16, .exs: 7, .json: 1)
Moved 1 file
Updated 8 files
Found 26 issues
Finished in 0.06s.
```
The switch `--no-autocorrect` runs the linter without any file changes. In this
mode, all correctors are working as checkers.
```
> cd examples/my_code
> mix recode --no-autocorrect --no-color --no-manifest
Read 24 files in 0.02s
...............................!!...!.!.............................!...............................................!...!..!!.....!...................!.....!..........!....!.......!....!!....!..!................!..........................!.....................................!.......!..!!..........!..!..!.!......!............!..........!...
File: lib/my_code.ex
[Specs 15/3] Functions should have a @spec type specification.
File: lib/my_code/alias_expansion.ex
[Moduledoc 1/1] The module Elixir.MyCode.AliasExpansion is missing @moduledoc.
[AliasExpansion 2/3] Avoid multi aliases.
[Specs 4/3] Functions should have a @spec type specification.
File: lib/my_code/alias_order.ex
[Moduledoc 13/1] The module Elixir.Mycode.AliasOrder is missing @moduledoc.
[AliasOrder 15/3] The alias `MyCode.PipeFunOne` is not alphabetically ordered among its group
[AliasOrder 16/3] The alias `MyCode` is not alphabetically ordered among its group
[AliasExpansion 16/3] Avoid multi aliases.
[AliasOrder 16/26] The alias `Echo` is not alphabetically ordered among its multi group
File: lib/my_code/deep.ex
[Moduledoc 1/1] The module Elixir.MyCode.Deep is missing @moduledoc.
[Specs 2/3] Functions should have a @spec type specification.
[Nesting 6/11] The body is nested too deep (max depth: 2).
File: lib/my_code/fun.ex
[LocalsWithoutParens 4/7] Unnecessary parens
File: lib/my_code/multi.ex
[Moduledoc 1/1] The module Elixir.MyCode.Multi is missing @moduledoc.
[Specs 4/3] Functions should have a @spec type specification.
[Specs 6/3] Functions should have a @spec type specification.
[PipeFunOne 7/7] Use parentheses for one-arity functions in pipes.
[Dbg 7/32] There should be no calls to dbg.
[Specs 10/3] Functions should have a @spec type specification.
[SinglePipe 11/7] Use a function call when a pipeline is only one function long.
[Specs 14/3] Functions should have a @spec type specification.
[Specs 18/3] Functions should have a @spec type specification.
[FilterCount 20/12] `Enum.count/2` is more efficient than `Enum.filter/2 |> Enum.count/1`
[IOInspect 22/8] There should be no calls to IO.inspect.
File: lib/my_code/pipe_fun_one.ex
[PipeFunOne 7/7] Use parentheses for one-arity functions in pipes.
File: lib/my_code/same_line.ex
[Moduledoc 1/1] The module Elixir.MyCode.SameLine is missing @moduledoc.
[Specs 2/3] Functions should have a @spec type specification.
File: lib/my_code/single_pipe.ex
[SinglePipe 7/7] Use a function call when a pipeline is only one function long.
[SinglePipe 10/25] Use a function call when a pipeline is only one function long.
File: lib/my_code/tags.ex
[TagTODO 3/-] Found a tag: TODO: add docs
[TagFIXME 6/-] Found a tag: FIXME: add more functions
[Specs 7/3] Functions should have a @spec type specification.
File: lib/my_code/trailing_comma.ex
[SinglePipe 18/5] Use a function call when a pipeline is only one function long.
File: mix.exs
[Moduledoc 1/1] The module Elixir.MyCode.MixProject is missing @moduledoc.
File: priv/repo/migrations/20190417140000_create_users.exs
[Moduledoc 1/1] The module Elixir.MyRepo.Migrations.CreateUsers is missing @moduledoc.
[Specs 4/3] Functions should have a @spec type specification.
[LocalsWithoutParens 7/7] Unnecessary parens
[Specs 13/3] Functions should have a @spec type specification.
File: test/my_code_test.ex
[Moduledoc 1/1] The module Elixir.MyCodeTest is missing @moduledoc.
[TestFile -/-] The file must be renamed to test/my_code_test.exs so that ExUnit can find it.
[TestFile -/-] The file must be renamed to test/my_code_test.exs so that ExUnit can find it.
Completed 342 tasks in 0.01s.
Files processed: 24 (.ex: 17, .exs: 6, .json: 1)
Found 41 issues
Finished in 0.04s.
```
With the switch `--autocorrect` (alias `-a`), correctors that are configured
with `autocorrect: false` going into the corrections mode.
Use the switch `--task` (alias `-t`) to run a specific task. This switch can be
used multiple times.
The last two switches are helpful for the task `IOInspect` and `Dbg`. Both of
the tasks are correctors configured with `autocorrect: false` in the default
configuration. The following example shows how to run these two tasks.
```
> cd examples/my_code
> mix recode -t IOInspect -t Dbg
Found 18 files, including 3 scripts.
....................................
File: lib/my_code/multi.ex
[Dbg 9/32] There should be no calls to dbg.
[IOInspect 24/8] There should be no calls to IO.inspect.
Finished in 0.04 seconds.
```
To delete all occurrences of `dbg` and `IO.inspect` the following call can be
used.
```
> cd examples/my_code
> mix recode -av -t IOInspect -t Dbg
Found 18 files, including 3 scripts.
....................................
File: lib/my_code/multi.ex
Updates: 2
Changed by: Dbg, IOInspect
...|
7 7 |
8 8 | def pipe(x) do
9 - | x |> double |> double() |> dbg()
9 + | x |> double |> double()
10 10 | end
11 11 |
...|
22 22 | |> Enum.filter(fn x -> rem(x, 2) == 0 end)
23 23 | |> Enum.count()
24 - | |> IO.inspect()
25 24 | end
26 25 |end
...|
Finished in 0.04 seconds.
```
The `-av` stands for the switches `--autocorrect` and `--verbose`. The switch
`--verbose` causes `recode` to display all changes as a diff on the console.
### `mix format`
You can also run Recode together with `mix format` by adding
`Recode.FormatterPlugin` to your `.formatter.exs` plugins:
```elixir
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
plugins: [Recode.FormatterPlugin]
]
```
If you are using [ElixirLS](https://github.com/elixir-lsp/elixir-ls), you will
need to enable `recode` for the test environment:
```elixir
def deps do
[
{:recode, "~> 0.7", only: [:dev, :test], runtime: false}
]
end
```
### `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.
```
## Code style and formatting
Code style and formatting are essential aspects of writing clean and
maintainable code. Different developers and teams may have varying preferences
when it comes to how code should be structured and formatted. Tools like
`recode` can help enforce consistency and improve readability by applying
predefined rules to the codebase. However, it's important to remember that
these rules are not set in stone and may not necessarily reflect the best
practices for every project.
Ultimately, the goal of code formatting is to make the code more
understandable and maintainable. It can help identify potential issues and
improve the overall quality of the code. While `recode` offers a set of rules to
follow, developers are free to choose which rules they want to apply based on
their own preferences and the requirements of the project.
It's worth noting that discussions around code style and formatting can be
subjective, and there is no one-size-fits-all solution. Developers should feel
empowered to make informed decisions about which formatting rules to apply and
adapt them as needed to suit their specific needs. There are numerous
resources available online that provide insights into code formatting best
practices, allowing developers to explore different approaches and find what
works best for them.
## Differences to Credo
`recode` was started as a plugin for `credo`. Unfortunately it was not possible
to implement autocorrection as a plugin because Credo's traversal of the code does
not support changing the code.
Maybe some code lines from `recode` could be used as inspiration for `credo`
to bring the autocorrect feature to `credo`.
Other differences:
* `recode` requires Elixir 1.13, `credo` requires Elixir 1.7
* `recode` has autocorrection
* `credo` has much more checkers
* `credo` is faster
* `credo` has more features