Current section
Files
Jump to
Current section
Files
lib/pattern/no_enum_into_empty_mapset.ex
defmodule Credence.Pattern.NoEnumIntoEmptyMapset do
@moduledoc """
Detects `Enum.into/2` and `Enum.into/3` targeting an empty `MapSet.new()`
and suggests `MapSet.new/1` or `MapSet.new/2` instead.
## Why this matters
`Enum.into(enum, MapSet.new())` and `Enum.into(enum, MapSet.new(), fun)` are
the generic collectable path for building MapSets. `MapSet.new/1` and
`MapSet.new/2` exist specifically for this purpose, are more readable, and
signal intent clearly.
LLMs frequently generate the `Enum.into` form because it is the
general-purpose collector they learn from many examples. An Elixir
developer would reach for `MapSet.new` by default.
## Flagged patterns
| Pattern | Suggested replacement |
| ---------------------------------------------- | ------------------------------ |
| `Enum.into(enum, MapSet.new())` | `MapSet.new(enum)` |
| `Enum.into(enum, MapSet.new(), fun)` | `MapSet.new(enum, fun)` |
| `enum \|> Enum.into(MapSet.new())` | `MapSet.new(enum)` |
| `enum \|> Enum.into(MapSet.new(), fun)` | `MapSet.new(enum, fun)` |
Only the bare `MapSet.new()` call with no arguments is targeted.
`Enum.into(enum, existing_mapset)` where `existing_mapset` is not
`MapSet.new()` is left alone — it merges into an existing MapSet,
which `MapSet.new` does not do.
"""
use Credence.Pattern.Rule
alias Credence.Issue
@impl true
def check(ast, _opts) do
{_ast, issues} =
Macro.prewalk(ast, [], fn node, issues ->
case check_node(node) do
{:ok, issue} -> {node, [issue | issues]}
:error -> {node, issues}
end
end)
Enum.reverse(issues)
end
@impl true
def fix_patches(ast, _opts) do
Credence.RuleHelpers.patches_from_postwalk(ast, fn
# Piped 3-arg: enum |> Enum.into(MapSet.new(), fun) → enum |> MapSet.new(fun)
{:|>, pipe_meta,
[
enum,
{{:., dot_meta, [{:__aliases__, alias_meta, [:Enum]}, :into]}, call_meta,
[{{:., _, [{:__aliases__, _, [:MapSet]}, :new]}, _, []}, fun]}
]} ->
{:|>, pipe_meta,
[
enum,
{{:., dot_meta, [{:__aliases__, alias_meta, [:MapSet]}, :new]}, call_meta, [fun]}
]}
# Piped 2-arg: enum |> Enum.into(MapSet.new()) → enum |> MapSet.new()
# Keep the pipe (consistent with the 3-arg clause above): un-piping into
# `MapSet.new(enum)` nests a multi-stage `enum` pipe as an argument, which
# mix format then wraps — worse than just continuing the chain.
{:|>, pipe_meta,
[
enum,
{{:., dot_meta, [{:__aliases__, alias_meta, [:Enum]}, :into]}, call_meta,
[{{:., _, [{:__aliases__, _, [:MapSet]}, :new]}, _, []}]}
]} ->
{:|>, pipe_meta,
[enum, {{:., dot_meta, [{:__aliases__, alias_meta, [:MapSet]}, :new]}, call_meta, []}]}
# Direct 3-arg: Enum.into(enum, MapSet.new(), fun) → MapSet.new(enum, fun)
{{:., dot_meta, [{:__aliases__, alias_meta, [:Enum]}, :into]}, call_meta,
[enum, {{:., _, [{:__aliases__, _, [:MapSet]}, :new]}, _, []}, fun]} ->
{{:., dot_meta, [{:__aliases__, alias_meta, [:MapSet]}, :new]}, call_meta, [enum, fun]}
# Direct 2-arg: Enum.into(enum, MapSet.new()) → MapSet.new(enum)
{{:., dot_meta, [{:__aliases__, alias_meta, [:Enum]}, :into]}, call_meta,
[enum, {{:., _, [{:__aliases__, _, [:MapSet]}, :new]}, _, []}]} ->
{{:., dot_meta, [{:__aliases__, alias_meta, [:MapSet]}, :new]}, call_meta, [enum]}
node ->
node
end)
end
# ── check helpers ────────────────────────────────────────────────
defp check_node(
{:|>, _,
[
_enum,
{{:., meta, [{:__aliases__, _, [:Enum]}, :into]}, _,
[{{:., _, [{:__aliases__, _, [:MapSet]}, :new]}, _, []} | _]}
]}
) do
{:ok, build_issue(meta)}
end
defp check_node(
{{:., meta, [{:__aliases__, _, [:Enum]}, :into]}, _,
[_enum, {{:., _, [{:__aliases__, _, [:MapSet]}, :new]}, _, []} | _rest]}
) do
{:ok, build_issue(meta)}
end
defp check_node(_), do: :error
defp build_issue(meta) do
%Issue{
rule: :no_enum_into_empty_mapset,
message:
"`Enum.into/2` targeting `MapSet.new()` can be replaced with `MapSet.new/1` " <>
"(or `MapSet.new/2` with a transform function). `MapSet.new` is the idiomatic way " <>
"to construct a MapSet from an enumerable.",
meta: %{line: Keyword.get(meta, :line)}
}
end
end