Current section
Files
Jump to
Current section
Files
lib/pattern/no_redundant_enum_join_separator.ex
defmodule Credence.Pattern.NoRedundantEnumJoinSeparator do
@moduledoc """
Readability rule: Detects `Enum.join("")` and `Enum.map_join("", mapper)`
where the empty-string separator is passed explicitly.
`Enum.join/1` and `Enum.map_join/2` already default to `""`, so the argument
adds visual noise without changing behaviour.
## Bad
graphemes |> Enum.join("")
Enum.join(list, "")
items |> Enum.map_join("", &to_string/1)
Enum.map_join(items, "", &to_string/1)
## Good
graphemes |> Enum.join()
Enum.join(list)
items |> Enum.map_join(&to_string/1)
Enum.map_join(items, &to_string/1)
"""
use Credence.Pattern.Rule
alias Credence.Issue
@impl true
def fixable?, do: true
@impl true
def check(ast, _opts) do
{_ast, issues} =
Macro.prewalk(ast, [], fn
# Direct call: Enum.join(list, "")
{{:., _, [{:__aliases__, _, [:Enum]}, :join]}, meta, [_list, ""]} = node, issues ->
{node, [build_issue(meta) | issues]}
# In a pipe the separator is the only explicit arg: ... |> Enum.join("")
# The piped value becomes the first arg, so the AST call has [""]
{{:., _, [{:__aliases__, _, [:Enum]}, :join]}, meta, [""]} = node, issues ->
{node, [build_issue(meta) | issues]}
# Direct call: Enum.map_join(list, "", mapper)
{{:., _, [{:__aliases__, _, [:Enum]}, :map_join]}, meta, [_list, "", _mapper]} = node,
issues ->
{node, [build_issue(meta) | issues]}
# In a pipe the separator is the first explicit arg: ... |> Enum.map_join("", mapper)
# The piped value becomes the first arg, so the AST call has ["", mapper]
{{:., _, [{:__aliases__, _, [:Enum]}, :map_join]}, meta, ["", _mapper]} = node, issues ->
{node, [build_issue(meta) | issues]}
node, issues ->
{node, issues}
end)
Enum.reverse(issues)
end
@impl true
def fix(source, _opts) do
source
|> Sourceror.parse_string!()
|> Macro.postwalk(fn
# Direct call: Enum.join(list, "") → Enum.join(list)
{{:., dot_m, [{:__aliases__, al_m, [:Enum]}, :join]}, call_m, [list_arg, sep]} = node ->
if empty_string?(sep),
do: {{:., dot_m, [{:__aliases__, al_m, [:Enum]}, :join]}, call_m, [list_arg]},
else: node
# Piped call: ... |> Enum.join("") → ... |> Enum.join()
{{:., dot_m, [{:__aliases__, al_m, [:Enum]}, :join]}, call_m, [sep]} = node ->
if empty_string?(sep),
do: {{:., dot_m, [{:__aliases__, al_m, [:Enum]}, :join]}, call_m, []},
else: node
# Direct call: Enum.map_join(list, "", mapper) → Enum.map_join(list, mapper)
{{:., dot_m, [{:__aliases__, al_m, [:Enum]}, :map_join]}, call_m, [list_arg, sep, mapper]} =
node ->
if empty_string?(sep),
do:
{{:., dot_m, [{:__aliases__, al_m, [:Enum]}, :map_join]}, call_m, [list_arg, mapper]},
else: node
# Piped call: ... |> Enum.map_join("", mapper) → ... |> Enum.map_join(mapper)
{{:., dot_m, [{:__aliases__, al_m, [:Enum]}, :map_join]}, call_m, [sep, mapper]} = node ->
if empty_string?(sep),
do: {{:., dot_m, [{:__aliases__, al_m, [:Enum]}, :map_join]}, call_m, [mapper]},
else: node
# Collapse single-step pipe: x |> Enum.join() → Enum.join(x)
# (fires after the "" removal above, since postwalk is bottom-up)
{:|>, _, [lhs, {{:., dot_m, [{:__aliases__, al_m, [:Enum]}, :join]}, call_m, []}]} = node ->
case lhs do
{:|>, _, _} -> node
_ -> {{:., dot_m, [{:__aliases__, al_m, [:Enum]}, :join]}, call_m, [lhs]}
end
# Collapse single-step pipe: x |> Enum.map_join(mapper) → Enum.map_join(x, mapper)
{:|>, _, [lhs, {{:., dot_m, [{:__aliases__, al_m, [:Enum]}, :map_join]}, call_m, [mapper]}]} =
node ->
case lhs do
{:|>, _, _} -> node
_ -> {{:., dot_m, [{:__aliases__, al_m, [:Enum]}, :map_join]}, call_m, [lhs, mapper]}
end
node ->
node
end)
|> Sourceror.to_string()
end
# Sourceror wraps string literals with metadata: {:__block__, meta, [""]}
# Match both the wrapped and bare forms.
defp empty_string?(""), do: true
defp empty_string?({:__block__, _, [""]}), do: true
defp empty_string?(_), do: false
defp build_issue(meta) do
%Issue{
rule: :no_redundant_enum_join_separator,
message:
"`Enum.join/1` and `Enum.map_join/2` already default to an empty string separator. " <>
"Remove the redundant `\"\"` argument.",
meta: %{line: Keyword.get(meta, :line)}
}
end
end