Credence.Pattern.PreferMapIntersectOverMapsetIntersection (credence v0.8.0)

Copy Markdown

Detects MapSet-based intersection of map keys that can be replaced with Map.intersect/3 (Elixir 1.14+).

The verbose pipeline Map.keys(a) |> MapSet.new() |> MapSet.intersection(MapSet.new(Map.keys(b))) |> MapSet.to_list() followed by an Enum.map that fetches and merges values from both maps can be replaced with a single Map.intersect/3 call.

Bad

common_keys =
  Map.keys(freq1)
  |> MapSet.new()
  |> MapSet.intersection(MapSet.new(Map.keys(freq2)))
  |> MapSet.to_list()

common_keys
|> Enum.map(fn element ->
  count1 = Map.fetch!(freq1, element)
  count2 = Map.fetch!(freq2, element)
  {element, min(count1, count2)}
end)
|> Enum.sort()

Good

freq1
|> Map.intersect(freq2, fn _key, count1, count2 -> min(count1, count2) end)
|> Enum.sort_by(fn {key, _value} -> key end)

Scope — what makes the rewrite safe

The whole two-statement shape must be present (the rule flags and fixes the same node — never one without the other), and every gap that could change the answer is closed:

  • The intersection assignment feeds exactly one Enum.map |> Enum.sort. The assigned variable is used once; the fix deletes its binding, so a second use would strand it unbound.
  • freq1/freq2 are bare variables. They are spliced verbatim into the Map.intersect/3 call.
  • The merge expression is a pure arithmetic combination of count1/count2 and numeric literals (min/max/+/-/*/div/rem/abs). This both rules out a merge that references the element key (which becomes _key and would be unbound) and guarantees the result is independent of evaluation order — MapSet.to_list and Map.intersect enumerate keys in different orders, but for a pure merge the final Enum.sort/Enum.sort_by(key) (keys are unique, so sorting by key equals sorting by the whole {key, value} tuple) produces an identical list.