Current section

Files

Jump to
credence lib pattern no_explicit_sum_reduce.ex
Raw

lib/pattern/no_explicit_sum_reduce.ex

defmodule Credence.Pattern.NoExplicitSumReduce do
@moduledoc "Flags explicit sum-reduction patterns inside Enum.reduce/3."
use Credence.Pattern.Rule
alias Credence.Issue
@impl true
def fixable?, do: true
@impl true
def priority, do: 501
@impl true
def check(ast, _opts) do
{_ast, issues} =
Macro.prewalk(ast, [], fn
{{:., _, _}, meta, args} = node, issues ->
if reduce_call?(node) and sum_reduce_body?(args) do
issue = %Issue{
rule: :no_explicit_sum_reduce,
message: "Explicit sum-reduction detected. Prefer Enum.sum/1.",
meta: %{line: Keyword.get(meta, :line)}
}
{node, [issue | issues]}
else
{node, issues}
end
node, issues ->
{node, issues}
end)
Enum.reverse(issues)
end
@impl true
def fix(source, _opts) do
source
|> Sourceror.parse_string!()
|> Macro.postwalk(fn
{{:., _, _}, _, args} = node ->
if reduce_call?(node) and sum_reduce_body?(args) do
[enum | _] = args
enum_sum_call(enum)
else
node
end
node ->
node
end)
|> Sourceror.to_string()
end
defp enum_sum_call(enum) do
{{:., [], [{:__aliases__, [], [:Enum]}, :sum]}, [], [enum]}
end
defp reduce_call?({{:., _, [{:__aliases__, _, [:Enum]}, :reduce]}, _, _}), do: true
defp reduce_call?({{:., _, [:Enum, :reduce]}, _, _}), do: true
defp reduce_call?(_), do: false
# Notice how we pattern match the two arguments (v1, v2) passed into the anonymous function
defp sum_reduce_body?([
_enum,
_acc,
{:fn, _, [{:->, _, [[{v1, _, _}, {v2, _, _}], body]}]}
])
when is_atom(v1) and is_atom(v2) do
explicit_sum?(body, v1, v2)
end
defp sum_reduce_body?(_), do: false
defp explicit_sum?({:__block__, _, [body]}, v1, v2), do: explicit_sum?(body, v1, v2)
# Ensure the variables being added are EXACTLY the variables passed into the fn args
defp explicit_sum?({:+, _, [{op1, _, _}, {op2, _, _}]}, v1, v2) do
Enum.sort([op1, op2]) == Enum.sort([v1, v2])
end
defp explicit_sum?(_, _, _), do: false
end