Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unnecessary If/Unless #89

Merged
merged 19 commits into from
Jul 7, 2024
Merged
34 changes: 18 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,25 @@ on `sourceror`.
> 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.
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.
FilterCount # Corrector - Checks calls like Enum.filter(...) |> Enum.count().
Nesting # Checker - Checks code nesting depth in functions and macros.
UnnecessaryIfUnless # Corrector - Changes `if expr, do: true, else: false` to `expr`
Warning tasks:
Dbg # Corrector - There should be no calls to dbg.
IOInspect # Corrector - There should be no calls to IO.inspect.
TestFileExt # Corrector - Checks the file extension of test files.
UnusedVariable # Corrector - Checks if unused variables occur.
Dbg # Corrector - There should be no calls to dbg.
IOInspect # Corrector - There should be no calls to IO.inspect.
TestFileExt # Corrector - Checks the file extension of test files.
UnusedVariable # Corrector - Checks if unused variables occur.
NickNeck marked this conversation as resolved.
Show resolved Hide resolved
```

It is also possible to run `recode` in a none-autocorrect mode to just lint your
Expand Down Expand Up @@ -114,6 +115,7 @@ This mix task generates the config file `.recode.exs`.
{Recode.Task.TagFIXME, [exit_code: 2]},
{Recode.Task.TagTODO, [exit_code: 4]},
{Recode.Task.TestFileExt, []},
{Recode.Task.UnnecessaryIfUnless, []},
{Recode.Task.UnusedVariable, [active: false]}
]
]
Expand Down
1 change: 1 addition & 0 deletions lib/recode/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ defmodule Recode.Config do
{Recode.Task.TagFIXME, exit_code: 2},
{Recode.Task.TagTODO, exit_code: 4},
{Recode.Task.TestFileExt, []},
{Recode.Task.UnnecessaryIfUnless, []},
{Recode.Task.UnusedVariable, [active: false]}
]
]
Expand Down
110 changes: 110 additions & 0 deletions lib/recode/task/unnecessary_if_unless.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
defmodule Recode.Task.UnnecessaryIfUnless do
@shortdoc "Removes redundant booleans"

@moduledoc """
Redudant booleans make code needlesly verbose.

# preferred
foo == bar

# not preferred
if foo == bar do
true
else
false
end

This task rewrites the code when `mix recode` runs with `autocorrect: true`.
"""

use Recode.Task, corrector: true, category: :readability

alias Recode.Issue
alias Recode.Task.UnnecessaryIfUnless
alias Rewrite.Source
alias Sourceror.Zipper

@impl Recode.Task
def run(source, opts) do
{zipper, issues} =
source
|> Source.get(:quoted)
|> Zipper.zip()
|> Zipper.traverse([], fn zipper, issues ->
remove_unecessary_if_unless(zipper, issues, opts[:autocorrect])
end)

case opts[:autocorrect] do
true ->
Source.update(source, UnnecessaryIfUnless, :quoted, Zipper.root(zipper))

false ->
Source.add_issues(source, issues)
end
end

defp remove_unecessary_if_unless(
%Zipper{node: {conditional, meta, body}} = zipper,
issues,
true
)
when conditional in [:if, :unless] do
case extract(body, conditional) do
{:ok, expr} ->
expr = put_leading_comments(expr, meta)

{Zipper.replace(zipper, expr), issues}

:error ->
{zipper, issues}
end
end

defp remove_unecessary_if_unless(
%Zipper{node: {conditional, meta, body}} = zipper,
issues,
false
)
when conditional in [:if, :unless] do
issues =
case extract(body, conditional) do
{:ok, _expr} ->
message = "Avoid unnecessary `if` and `unless`"
issue = Issue.new(UnnecessaryIfUnless, message, meta)
[issue | issues]

:error ->
issues
end

{zipper, issues}
end

defp remove_unecessary_if_unless(zipper, issues, _autocorrect), do: {zipper, issues}

defp extract(
[
expr,
[
{{:__block__, _, [:do]}, {:__block__, _, [left]}},
{{:__block__, _, [:else]}, {:__block__, _, [right]}}
]
],
conditional
)
when is_boolean(left) and is_boolean(right) do
case {conditional, left, right} do
{:if, true, false} -> {:ok, expr}
{:if, false, true} -> {:ok, {:not, [], [expr]}}
{:unless, true, false} -> {:ok, {:not, [], [expr]}}
{:unless, false, true} -> {:ok, expr}
end
end

defp extract(_, _), do: :error

defp put_leading_comments(expr, meta) do
comments = meta[:leading_comments] || []
Sourceror.append_comments(expr, comments)
end
end
1 change: 1 addition & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ defmodule Recode.MixProject do
Recode.Task.TagFIXME,
Recode.Task.TagTODO,
Recode.Task.TestFileExt,
Recode.Task.UnnecessaryIfInless,
Recode.Task.UnusedVariable
]
]
Expand Down
173 changes: 173 additions & 0 deletions test/recode/task/unnecessary_if_unless_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
defmodule Recode.Task.UnnecessaryIfUnlessTest do
use RecodeCase

alias Recode.Task.UnnecessaryIfUnless

describe "run/1" do
test "equals" do
code = """
if foo == bar do
true
else
false
end
"""

expected = """
foo == bar
"""

code
|> run_task(UnnecessaryIfUnless, autocorrect: true)
|> assert_code(expected)
end

test "predicate" do
code = """
if foo?(bar) do
true
else
false
end
"""

expected = """
foo?(bar)
"""

code
|> run_task(UnnecessaryIfUnless, autocorrect: true)
|> assert_code(expected)
end

test "keyword syntax" do
code = """
if foo?(bar), do: true, else: false
"""

expected = """
foo?(bar)
"""

code
|> run_task(UnnecessaryIfUnless, autocorrect: true)
|> assert_code(expected)
end

test "negates code with reverse booleans" do
code = """
if some_call?() && (some_other_call?() || another_call?()) do
false
else
true
end
"""

expected = """
not (some_call?() && (some_other_call?() || another_call?()))
"""

code
|> run_task(UnnecessaryIfUnless, autocorrect: true)
|> assert_code(expected)
end

test "maintains leading comments" do
code = """
# I'm a comment
if foo?(bar) do
true
else
false
end
"""

expected = """
# I'm a comment
foo?(bar)
"""

code
|> run_task(UnnecessaryIfUnless, autocorrect: true)
|> assert_code(expected)
end

test "keeps code as is without booleans" do
code = """
if foo?(bar) do
bar
else
false
end
"""

expected = """
if foo?(bar) do
bar
else
false
end
"""

code
|> run_task(UnnecessaryIfUnless, autocorrect: true)
|> assert_code(expected)
end

test "works with unless" do
code = """
unless foo?(bar) do
false
else
true
end
"""

expected = """
foo?(bar)
"""

code
|> run_task(UnnecessaryIfUnless, autocorrect: true)
|> assert_code(expected)
end

test "negates unless" do
code = """
unless foo?(bar) do
true
else
false
end
"""

expected = """
not foo?(bar)
"""

code
|> run_task(UnnecessaryIfUnless, autocorrect: true)
|> assert_code(expected)
end

test "reports no issues" do
"""
foo == bar
"""
|> run_task(UnnecessaryIfUnless, autocorrect: false)
|> refute_issues()
end

test "reports an issue" do
"""
if foo == bar do
true
else
false
end
"""
|> run_task(UnnecessaryIfUnless, autocorrect: false)
|> assert_issue_with(reporter: UnnecessaryIfUnless)
end
end
end
Loading