Current section

Files

Jump to
credence lib rule no_enum_take_negative.ex
Raw

lib/rule/no_enum_take_negative.ex

defmodule Credence.Rule.NoEnumTakeNegative do
@moduledoc """
Performance rule: Detects `Enum.take(list, -n)` where `n` is a positive
integer literal.
For linked lists, `Enum.take(list, -n)` must internally determine the list
length, then traverse again to the cut point — effectively two full
traversals. If the list was just sorted, sorting in the opposite direction
and taking a positive count is more efficient.
## Bad
sorted = Enum.sort(nums)
top_three = Enum.take(sorted, -3)
## Good
top_three = Enum.sort(nums, :desc) |> Enum.take(3)
"""
@behaviour Credence.Rule
alias Credence.Issue
@impl true
def check(ast, _opts) do
{_ast, issues} =
Macro.prewalk(ast, [], fn
# Direct: Enum.take(list, -3)
{{:., _, [{:__aliases__, _, [:Enum]}, :take]}, meta, [_, {:-, _, [n]}]} = node, issues
when is_integer(n) and n > 0 ->
{node, [build_issue(n, meta) | issues]}
# Piped: list |> Enum.take(-3) — the negative is the only explicit arg
{{:., _, [{:__aliases__, _, [:Enum]}, :take]}, meta, [{:-, _, [n]}]} = node, issues
when is_integer(n) and n > 0 ->
{node, [build_issue(n, meta) | issues]}
node, issues ->
{node, issues}
end)
Enum.reverse(issues)
end
defp build_issue(n, meta) do
%Issue{
rule: :no_enum_take_negative,
severity: :warning,
message:
"`Enum.take(list, -#{n})` forces a double traversal of the list to take from the end. " <>
"Sort in the opposite direction and use `Enum.take(list, #{n})` instead.",
meta: %{line: Keyword.get(meta, :line)}
}
end
end