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
|> Code.string_to_quoted!()
|> Macro.postwalk(fn
# Direct call: Enum.join(list, "") → Enum.join(list)
{{:., _, [{:__aliases__, _, [:Enum]}, :join]}, _meta, [list_arg, ""]} ->
{{:., [], [{:__aliases__, [], [:Enum]}, :join]}, [], [list_arg]}
# Piped call: ... |> Enum.join("") → ... |> Enum.join()
{{:., _, [{:__aliases__, _, [:Enum]}, :join]}, _meta, [""]} ->
{{:., [], [{:__aliases__, [], [:Enum]}, :join]}, [], []}
# Direct call: Enum.map_join(list, "", mapper) → Enum.map_join(list, mapper)
{{:., _, [{:__aliases__, _, [:Enum]}, :map_join]}, _meta, [list_arg, "", mapper]} ->
{{:., [], [{:__aliases__, [], [:Enum]}, :map_join]}, [], [list_arg, mapper]}
# Piped call: ... |> Enum.map_join("", mapper) → ... |> Enum.map_join(mapper)
{{:., _, [{:__aliases__, _, [:Enum]}, :map_join]}, _meta, ["", mapper]} ->
{{:., [], [{:__aliases__, [], [:Enum]}, :map_join]}, [], [mapper]}
node ->
node
end)
|> Macro.to_string()
|> Code.format_string!()
|> IO.iodata_to_binary()
end
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