Current section
Files
Jump to
Current section
Files
lib/pattern/prefer_concat_over_flat_map_identity.ex
defmodule Credence.Pattern.PreferConcatOverFlatMapIdentity do
@moduledoc """
Detects `Enum.flat_map(enumerable, fn x -> x end)` which is an identity
flat-map and can be simplified to `Enum.concat(enumerable)`.
LLMs sometimes generate `Enum.flat_map(fn x -> x end)` instead of the
simpler `Enum.concat/1`. This pattern also appears in piped form.
## Bad
matrix |> Enum.flat_map(fn row -> row end)
Enum.flat_map(list_of_lists, fn x -> x end)
## Good
Enum.concat(matrix)
Enum.concat(list_of_lists)
"""
use Credence.Pattern.Rule
alias Credence.Issue
@impl true
def check(ast, _opts) do
{_ast, issues} =
Macro.prewalk(ast, [], fn
# Direct: Enum.flat_map(list, identity_fn)
{{:., _, [{:__aliases__, _, [:Enum]}, :flat_map]}, meta, [_list, callback]} = node, acc ->
if identity_fn?(callback) do
{node, [build_issue(meta) | acc]}
else
{node, acc}
end
# Piped: list |> Enum.flat_map(identity_fn)
{:|>, _, [_lhs, {{:., _, [{:__aliases__, _, [:Enum]}, :flat_map]}, meta, [callback]}]} =
node,
acc ->
if identity_fn?(callback) do
{node, [build_issue(meta) | acc]}
else
{node, acc}
end
node, acc ->
{node, acc}
end)
Enum.reverse(issues)
end
@impl true
def fix_patches(ast, _opts) do
{_ast, patches} =
Macro.prewalk(ast, [], fn
# Direct: Enum.flat_map(list, identity_fn)
{{:., _, [{:__aliases__, _, [:Enum]}, :flat_map]}, _meta, [list, callback]} = node, acc ->
if identity_fn?(callback) do
{node, [direct_patch(node, list) | acc]}
else
{node, acc}
end
# Piped: list |> Enum.flat_map(identity_fn). Patch only the RHS call,
# leaving the `|>` and the LHS source byte-identical.
{:|>, _,
[_lhs, {{:., _, [{:__aliases__, _, [:Enum]}, :flat_map]}, _meta, [callback]} = rhs]} =
node,
acc ->
if identity_fn?(callback) do
{node, [piped_patch(rhs) | acc]}
else
{node, acc}
end
node, acc ->
{node, acc}
end)
Enum.reverse(patches)
end
defp direct_patch(node, list) do
list_text = Sourceror.to_string(list)
%{
range: Sourceror.get_range(node),
change: "Enum.concat(#{list_text})"
}
end
defp piped_patch(rhs_node) do
%{
range: Sourceror.get_range(rhs_node),
change: "Enum.concat()"
}
end
# fn x -> x end (single-clause, same variable in arg and body)
defp identity_fn?({:fn, _, [{:->, _, [[{var, _, ctx}], {var, _, ctx}]}]})
when is_atom(var) and is_atom(ctx),
do: true
# & &1
defp identity_fn?({:&, _, [{:&, _, [1]}]}), do: true
# &(&1)
defp identity_fn?({:&, _, [{:&, _, [{:__block__, _, [1]}]}]}), do: true
# &Function.identity/1
defp identity_fn?(
{:&,
[
{:/, _,
[
{{:., _, [{:__aliases__, [:Function]}, :identity]}, _, []},
1
]}
]}
),
do: true
# &Function.identity/1 with __block__-wrapped 1
defp identity_fn?(
{:&,
[
{:/, _,
[
{{:., _, [{:__aliases__, [:Function]}, :identity]}, _, []},
{:__block__, _, [1]}
]}
]}
),
do: true
defp identity_fn?(_), do: false
defp build_issue(meta) do
%Issue{
rule: :prefer_concat_over_flat_map_identity,
message: """
`Enum.flat_map/2` with an identity function is equivalent to `Enum.concat/1`.
Simplify:
Enum.concat(enumerable)
""",
meta: %{line: Keyword.get(meta, :line)}
}
end
end