Current section

Files

Jump to
credence lib pattern no_redundant_underscore_bind.ex
Raw

lib/pattern/no_redundant_underscore_bind.ex

defmodule Credence.Pattern.NoRedundantUnderscoreBind do
@moduledoc """
Detects `_ = var` bindings in **pattern position** (function heads, `case`/`fn`
clause heads, `with`/`for` generators) and simplifies them to just `var`.
In pattern position `_ = var` matches anything and binds it to `var`, which is
identical to just `var` — the underscore adds no value.
## Bad
def foo(_ = x), do: x + 1
## Good
def foo(x), do: x + 1
## Not flagged — statement position
A standalone `_ = var` in a block body is NOT redundant: it is the idiom
Elixir's own warning recommends to consume a bound-but-unused value without the
"variable has no effect / is never returned" warning (NimbleParsec- and
macro-generated code rely on it). Rewriting it to a bare `var` statement
reintroduces exactly that warning, so we only fire in pattern position.
"""
use Credence.Pattern.Rule
alias Credence.Issue
@impl true
def check(ast, _opts) do
pattern_binds = pattern_bind_positions(ast)
{_ast, issues} =
Macro.prewalk(ast, [], fn
{:=, meta, [{:_, _, nil}, {name, _, ctx}]} = node, acc
when is_atom(name) and is_atom(ctx) ->
if MapSet.member?(pattern_binds, position(meta)) 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
pattern_binds = pattern_bind_positions(ast)
Credence.RuleHelpers.patches_from_postwalk(ast, fn
{:=, meta, [{:_, _, nil}, {name, _, ctx}]} = node
when is_atom(name) and is_atom(ctx) ->
if MapSet.member?(pattern_binds, position(meta)) do
# Replace _ = var with just var
elem(node, 2) |> List.last()
else
node
end
node ->
node
end)
end
# Positions of `_ = var` nodes that sit in genuine PATTERN position, where
# `_ = x` is exactly equivalent to `x`: function-head arguments, `->` clause
# heads (case/fn/with/cond/receive/rescue), and `<-` generators (with/for).
# A `_ = var` anywhere else is a statement-position discard (see moduledoc) and
# must be left alone.
defp pattern_bind_positions(ast) do
{_ast, set} =
Macro.prewalk(ast, MapSet.new(), fn
{dt, _, [{:when, _, [{fn_name, _, args} | _]} | _]} = node, acc
when dt in [:def, :defp, :defmacro, :defmacrop] and is_atom(fn_name) and is_list(args) ->
{node, collect_binds(args, acc)}
{dt, _, [{fn_name, _, args} | _]} = node, acc
when dt in [:def, :defp, :defmacro, :defmacrop] and is_atom(fn_name) and is_list(args) ->
{node, collect_binds(args, acc)}
{:->, _, [patterns, _body]} = node, acc when is_list(patterns) ->
{node, collect_binds(patterns, acc)}
{:<-, _, [pattern, _expr]} = node, acc ->
{node, collect_binds(pattern, acc)}
node, acc ->
{node, acc}
end)
set
end
defp collect_binds(pattern_ast, acc) do
{_ast, set} =
Macro.prewalk(pattern_ast, acc, fn
{:=, meta, [{:_, _, nil}, {name, _, ctx}]} = node, a
when is_atom(name) and is_atom(ctx) ->
{node, MapSet.put(a, position(meta))}
node, a ->
{node, a}
end)
set
end
defp position(meta), do: {Keyword.get(meta, :line), Keyword.get(meta, :column)}
defp build_issue(meta) do
%Issue{
rule: :no_redundant_underscore_bind,
message: "Redundant `_ = var` binding. Use just the variable name instead.",
meta: %{line: Keyword.get(meta, :line)}
}
end
end