Current section
Files
Jump to
Current section
Files
lib/rule/no_manual_string_reverse.ex
defmodule Credence.Rule.NoManualStringReverse do
@moduledoc """
Readability & performance rule: Detects the pattern
`String.graphemes(s) |> Enum.reverse() |> Enum.join()` which is a manual
reimplementation of `String.reverse/1`.
`String.reverse/1` handles Unicode grapheme clusters correctly and avoids
creating an intermediate list, making it both clearer and faster.
## Bad
reversed = str |> String.graphemes() |> Enum.reverse() |> Enum.join()
## Good
reversed = String.reverse(str)
"""
@behaviour Credence.Rule
alias Credence.Issue
@impl true
def check(ast, _opts) do
{_ast, issues} =
Macro.prewalk(ast, [], fn
# Pipeline form: ... |> String.graphemes() |> Enum.reverse() |> Enum.join()
#
# `a |> b() |> c()` parses as {:|>, _, [{:|>, _, [a, b]}, c]}.
# So the outer pipe has `c` on the right and the inner chain on the left.
# We check: right == Enum.join, predecessor == Enum.reverse, predecessor's predecessor == String.graphemes.
{:|>, meta, [left, right]} = node, issues ->
if remote_call?(right, :Enum, :join) and remote_call?(rightmost(left), :Enum, :reverse) do
grandparent =
case left do
{:|>, _, [inner_left, _]} -> rightmost(inner_left)
_ -> nil
end
if grandparent != nil and remote_call?(grandparent, :String, :graphemes) do
{node, [build_issue(meta) | issues]}
else
{node, issues}
end
else
{node, issues}
end
# Nested call form: Enum.join(Enum.reverse(String.graphemes(s)))
{{:., _, [{:__aliases__, _, [:Enum]}, :join]}, meta,
[
{{:., _, [{:__aliases__, _, [:Enum]}, :reverse]}, _,
[
{{:., _, [{:__aliases__, _, [:String]}, :graphemes]}, _, _}
]}
]} = node,
issues ->
{node, [build_issue(meta) | issues]}
node, issues ->
{node, issues}
end)
Enum.reverse(issues)
end
defp rightmost({:|>, _, [_, right]}), do: right
defp rightmost(other), do: other
defp remote_call?(node, mod, func) do
match?({{:., _, [{:__aliases__, _, [^mod]}, ^func]}, _, _}, node)
end
defp build_issue(meta) do
%Issue{
rule: :no_manual_string_reverse,
severity: :warning,
message:
"Use `String.reverse/1` instead of `String.graphemes/1 |> Enum.reverse/0 |> Enum.join/0`. " <>
"It is clearer and avoids creating an intermediate list.",
meta: %{line: Keyword.get(meta, :line)}
}
end
end