Current section
Files
Jump to
Current section
Files
lib/pattern/no_sort_then_at_unfixable.ex
defmodule Credence.Pattern.NoSortThenAtUnfixable do
@moduledoc """
Performance rule (flag-only companion): Detects `Enum.sort |> Enum.at(index)`
patterns where the index is **not** a compile-time literal `0` or `-1`, or the
sort direction cannot be statically determined.
These cases require human or LLM judgement to rewrite because the correct
replacement depends on the runtime value of the index and/or sort direction.
## Examples that trigger this rule
Enum.sort(nums, :desc) |> Enum.at(k - 1) # variable index
Enum.at(Enum.sort(nums), mid) # variable index
Enum.sort(nums, dir) |> Enum.at(0) # variable direction
Enum.sort(nums, fn a, b -> a > b end) |> Enum.at(0) # custom comparator
## What to do
For index 0 → `Enum.min/1` or `Enum.max/1`.
For index -1 → `Enum.max/1` or `Enum.min/1` (reversed).
For other indices → `Enum.take/2` on the sorted list, or a partial-sort /
quickselect algorithm.
"""
use Credence.Pattern.Rule
alias Credence.Issue
@impl true
def fixable?, do: false
@impl true
def check(ast, _opts) do
{_ast, issues} =
Macro.prewalk(ast, [], fn
{:|>, meta, [left, right]} = node, issues ->
if remote_call?(right, :Enum, :at) and remote_call?(rightmost(left), :Enum, :sort) do
{node, [build_issue(meta) | issues]}
else
{node, issues}
end
{{:., _, [{:__aliases__, _, [:Enum]}, :at]}, meta,
[
{{:., _, [{:__aliases__, _, [:Enum]}, :sort]}, _, _} | _rest
]} = 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_sort_then_at,
message:
"Sorting a list and then accessing a single element with `Enum.at/2` is O(n log n) " <>
"when you may only need O(n). For min/max, use `Enum.min/1` or `Enum.max/1`. " <>
"For top-k, consider `Enum.take/2` on the sorted result.",
meta: %{line: Keyword.get(meta, :line)}
}
end
end