Credence.Pattern.NoReduceForGroupBy (credence v0.7.0)

Copy Markdown

Detects a manual Enum.group_by/2 written as Enum.reduce/3 with an empty-map accumulator that prepends each element onto a per-key list via Map.update(acc, key, [elem], &[elem | &1]), immediately followed by |> Map.new(fn {k, v} -> {k, Enum.reverse(v)} end) to restore insertion order.

Only this complete pipeline is flagged, because only this complete pipeline is exactly equivalent to Enum.group_by/2 on every input.

Why the full pipeline is required

The bare reduce builds each value list in reverse insertion order (prepend with [elem | &1]):

Enum.reduce(["a1", "a2"], %{}, fn x, acc ->
  Map.update(acc, String.first(x), [x], &[x | &1])
end)
#=> %{"a" => ["a2", "a1"]}

whereas Enum.group_by/2 keeps insertion order (%{"a" => ["a1", "a2"]}). The trailing Map.new(fn {k, v} -> {k, Enum.reverse(v)} end) reverses each value list, making the whole expression equal to:

Enum.group_by(enum, fn x -> String.first(x) end)

A bare reduce without the reverse is therefore not flagged: it produces a different (reverse-order) result, so there is no behaviour-preserving fix.

Flagged pattern (auto-fixed)

Enum.reduce(enum, %{}, fn x, acc ->
  Map.update(acc, key(x), [x], &[x | &1])
end)
|> Map.new(fn {k, v} -> {k, Enum.reverse(v)} end)

becomes

Enum.group_by(enum, fn x -> key(x) end)

The reduce may be written directly (Enum.reduce(enum, %{}, fn ...)) or piped (enum |> Enum.reduce(%{}, fn ...)). The key may be computed inline in the Map.update call, or via a single key = ... binding immediately preceding the Map.update (the only other statement in the function body).