Current section

Files

Jump to
credence lib pattern no_explicit_product_reduce.ex
Raw

lib/pattern/no_explicit_product_reduce.ex

defmodule Credence.Pattern.NoExplicitProductReduce do
@moduledoc "Flags explicit product-reduction patterns inside Enum.reduce/3."
use Credence.Pattern.Rule
alias Credence.Issue
@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 product_reduce_body?(args) do
issue = %Issue{
rule: :no_explicit_product_reduce,
message: "Explicit product-reduction detected. Prefer Enum.product/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_patches(ast, _opts) do
Credence.RuleHelpers.patches_from_postwalk(ast, fn
{{:., _, _}, _, args} = node ->
if reduce_call?(node) and product_reduce_body?(args) do
[enum | _] = args
enum_product_call(enum)
else
node
end
node ->
node
end)
end
defp enum_product_call(enum) do
{{:., [], [{:__aliases__, [], [:Enum]}, :product]}, [], [enum]}
end
defp reduce_call?({{:., _, [{:__aliases__, _, [:Enum]}, :reduce]}, _, _}), do: true
defp reduce_call?({{:., _, [:Enum, :reduce]}, _, _}), do: true
defp reduce_call?(_), do: false
defp product_reduce_body?([
_enum,
{:__block__, _, [1]},
{:fn, _, [{:->, _, [[{v1, _, c1}, {v2, _, c2}], body]}]}
])
when is_atom(v1) and is_atom(v2) and is_atom(c1) and is_atom(c2) and v1 != v2 do
explicit_product?(body, v1, v2)
end
# Capture syntax: Enum.reduce(enum, 1, &*/2)
defp product_reduce_body?([
_enum,
{:__block__, _, [1]},
{:&, _, [{:/, _, [{:*, _, _}, _arity]}]}
]),
do: true
defp product_reduce_body?(_), do: false
defp explicit_product?({:__block__, _, [body]}, v1, v2), do: explicit_product?(body, v1, v2)
defp explicit_product?({:*, _, [a, b]}, v1, v2) do
case {var_name(a), var_name(b)} do
{{:ok, op1}, {:ok, op2}} -> Enum.sort([op1, op2]) == Enum.sort([v1, v2])
_ -> false
end
end
defp explicit_product?(_, _, _), do: false
# A plain variable reference is `{name, meta, context}` where context is an
# atom (nil or a hygiene context). A call such as `x()` has a list of args in
# that position, so it is excluded.
defp var_name({name, _, context}) when is_atom(name) and is_atom(context), do: {:ok, name}
defp var_name(_), do: :error
end