Current section

Files

Jump to
credence lib pattern no_map_as_set.ex
Raw

lib/pattern/no_map_as_set.ex

defmodule Credence.Pattern.NoMapAsSet do
@moduledoc """
Style rule: Detects using a `Map` with boolean literal values (`true`/`false`)
purely for membership tracking, when `MapSet` is more appropriate.
`Map.put(seen, item, true)` paired with `Map.has_key?(seen, item)` is a
manual reimplementation of `MapSet.put/2` and `MapSet.member?/2`. Using
`MapSet` makes the intent clearer and avoids storing meaningless values.
This rule is **not auto-fixable** because a correct transformation requires
companion changes beyond the flagged call site — initialising the variable
with `MapSet.new()` instead of `%{}`, and replacing the corresponding
`Map.has_key?/2` check with `MapSet.member?/2`. Those changes involve
data-flow analysis that cannot be done safely with a local AST rewrite.
## Bad
# Boolean value used purely for membership tracking
Map.put(seen, item, true)
Map.put(seen, item, false)
# Typical pattern this catches
if Map.has_key?(seen, item) do
{seen, acc}
else
{Map.put(seen, item, true), [item | acc]}
end
## Good
if MapSet.member?(seen, item) do
{seen, acc}
else
{MapSet.put(seen, item), [item | acc]}
end
"""
use Credence.Pattern.Rule
alias Credence.Issue
@impl true
def fixable?, do: false
@impl true
def check(ast, _opts) do
{_ast, issues} =
Macro.prewalk(ast, [], fn
# Map.put(var, key, true/false) — direct call (3 args)
{{:., _, [{:__aliases__, _, [:Map]}, :put]}, meta, [_, _, bool]} = node, issues
when bool in [true, false] ->
{node, [build_issue(meta, bool) | issues]}
# map |> Map.put(key, true/false) — pipeline call (2 args, first is piped)
{{:., _, [{:__aliases__, _, [:Map]}, :put]}, meta, [_, bool]} = node, issues
when bool in [true, false] ->
{node, [build_issue(meta, bool) | issues]}
node, issues ->
{node, issues}
end)
Enum.reverse(issues)
end
defp build_issue(meta, bool) do
%Issue{
rule: :no_map_as_set,
message:
"`Map.put/3` called with boolean literal `#{inspect(bool)}` — " <>
"this suggests the map is used purely for membership tracking. " <>
"Use `MapSet` instead: `MapSet.put/2` and `MapSet.member?/2` " <>
"make the intent explicit.",
meta: %{line: Keyword.get(meta, :line)}
}
end
end