Current section

Files

Jump to
credence lib pattern prefer_enum_slice.ex
Raw

lib/pattern/prefer_enum_slice.ex

defmodule Credence.Pattern.PreferEnumSlice do
@moduledoc """
Readability rule: flags `Enum.drop/2` followed by `Enum.take/2` and rewrites it
to `Enum.slice/3`.
`Enum.drop(list, start) |> Enum.take(len)` keeps elements `[start, start+len)`,
which is exactly `Enum.slice(list, start, len)`**but only when both `start`
and `len` are non-negative**. With a negative `start`, `Enum.drop` counts from
the end while `Enum.slice`'s start indexes from the end differently; with a
negative `len`, `Enum.take` keeps the *last* `len` while `Enum.slice` rejects a
negative length. So the rule fires only when **both amounts are non-negative
integer literals** — a variable amount could be negative at runtime and is not
rewritten.
## Bad
Enum.drop(list, 5) |> Enum.take(10)
Enum.take(Enum.drop(list, 5), 10)
## Good
Enum.slice(list, 5, 10)
## Not flagged
Enum.drop(list, start) |> Enum.take(len) # variable amounts (could be negative)
Enum.drop(list, -1) |> Enum.take(2) # negative drop
Enum.drop(list, 1) |> Enum.take(-2) # negative take
"""
use Credence.Pattern.Rule
alias Credence.Issue
alias Credence.RuleHelpers
@impl true
def check(ast, _opts) do
{_ast, issues} =
Macro.prewalk(ast, [], fn
# Pattern 1: ... |> Enum.drop(start) |> Enum.take(len)
{:|>, _,
[
{:|>, _, [_left, {{:., _, [{:__aliases__, _, [:Enum]}, :drop]}, _, [drop_amount]}]},
{{:., _, [{:__aliases__, _, [:Enum]}, :take]}, take_meta, [take_amount]}
]} = node,
issues ->
maybe_flag(node, drop_amount, take_amount, take_meta, issues)
# Pattern 2: Enum.take(Enum.drop(list, start), len)
{{:., _, [{:__aliases__, _, [:Enum]}, :take]}, take_meta,
[
{{:., _, [{:__aliases__, _, [:Enum]}, :drop]}, _, [_collection, drop_amount]},
take_amount
]} = node,
issues ->
maybe_flag(node, drop_amount, take_amount, take_meta, issues)
# Pattern 3: Enum.drop(list, start) |> Enum.take(len)
{:|>, _,
[
{{:., _, [{:__aliases__, _, [:Enum]}, :drop]}, _, [_collection, drop_amount]},
{{:., _, [{:__aliases__, _, [:Enum]}, :take]}, take_meta, [take_amount]}
]} = node,
issues ->
maybe_flag(node, drop_amount, take_amount, take_meta, issues)
node, issues ->
{node, issues}
end)
Enum.reverse(issues)
end
defp maybe_flag(node, drop_amount, take_amount, take_meta, issues) do
if slice_safe?(drop_amount, take_amount) do
{node, [build_issue(take_meta) | issues]}
else
{node, issues}
end
end
@impl true
def fix_patches(ast, _opts) do
RuleHelpers.patches_from_postwalk(ast, fn
# Pattern 1
{:|>, pipe_meta,
[
{:|>, _, [left, {{:., _, [{:__aliases__, _, [:Enum]}, :drop]}, _, [drop_amount]}]},
{{:., _, [{:__aliases__, _, [:Enum]}, :take]}, _, [take_amount]}
]} = node ->
if slice_safe?(drop_amount, take_amount) do
{:|>, pipe_meta,
[
left,
{{:., [], [{:__aliases__, [], [:Enum]}, :slice]}, [], [drop_amount, take_amount]}
]}
else
node
end
# Pattern 2
{{:., _, [{:__aliases__, _, [:Enum]}, :take]}, _,
[
{{:., _, [{:__aliases__, _, [:Enum]}, :drop]}, _, [collection, drop_amount]},
take_amount
]} = node ->
if slice_safe?(drop_amount, take_amount) do
{{:., [], [{:__aliases__, [], [:Enum]}, :slice]}, [],
[collection, drop_amount, take_amount]}
else
node
end
# Pattern 3
{:|>, _,
[
{{:., _, [{:__aliases__, _, [:Enum]}, :drop]}, _, [collection, drop_amount]},
{{:., _, [{:__aliases__, _, [:Enum]}, :take]}, _, [take_amount]}
]} = node ->
if slice_safe?(drop_amount, take_amount) do
{{:., [], [{:__aliases__, [], [:Enum]}, :slice]}, [],
[collection, drop_amount, take_amount]}
else
node
end
node ->
node
end)
end
# Equivalent to Enum.slice/3 only when both amounts are non-negative integer
# literals. A negative drop/take has different semantics, and a variable could
# be negative at runtime.
defp slice_safe?(drop_amount, take_amount),
do: non_neg_int?(drop_amount) and non_neg_int?(take_amount)
# Sourceror wraps integer literals as {:__block__, meta, [n]}.
defp non_neg_int?({:__block__, _, [n]}) when is_integer(n) and n >= 0, do: true
defp non_neg_int?(n) when is_integer(n) and n >= 0, do: true
defp non_neg_int?(_), do: false
defp build_issue(meta) do
%Issue{
rule: :prefer_enum_slice,
message:
"Using `Enum.drop/2` followed by `Enum.take/2` (with non-negative literal " <>
"counts) is verbose. Use `Enum.slice/3` instead for clearer intent.",
meta: %{line: Keyword.get(meta, :line)}
}
end
end