Current section
Files
Jump to
Current section
Files
lib/pattern/no_explicit_max_reduce.ex
defmodule Credence.Pattern.NoExplicitMaxReduce do
@moduledoc """
Strict semantic rule: Flags ONLY explicit max-reduction patterns inside `Enum.reduce/3`.
This rule does NOT perform heuristic detection. It only matches cases where
the reduce body clearly implements a max/argmax operation using:
- `max(a, b)`
- `if a > acc do ... else ...`
- `if a >= acc do ... else ...`
Any deviation (tuple state, maps, pipelines, multiple expressions, etc.)
will NOT be flagged.
"""
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
# MATCH ALL Enum.reduce variants safely
{{:., _, _}, meta, args} = node, issues ->
if reduce_call?(node) and max_reduce_body?(args) do
issue = %Issue{
rule: :no_explicit_max_reduce,
message: "Explicit max-reduction detected. Prefer Enum.max/1 or Enum.max_by/2.",
meta: %{line: Keyword.get(meta, :line)}
}
{node, [issue | issues]}
else
{node, issues}
end
node, issues ->
{node, issues}
end)
Enum.reverse(issues)
end
@impl true
def fix(source, _opts) do
source
|> Sourceror.parse_string!()
|> Macro.postwalk(fn
{{:., _, _}, _, args} = node ->
if reduce_call?(node) and max_reduce_body?(args) do
[enum | _] = args
enum_max_call(enum)
else
node
end
node ->
node
end)
|> Sourceror.to_string()
end
defp enum_max_call(enum) do
{{:., [], [{:__aliases__, [], [:Enum]}, :max]}, [], [enum]}
end
defp reduce_call?({{:., _, [{:__aliases__, _, [:Enum]}, :reduce]}, _, _}), do: true
defp reduce_call?({{:., _, [:Enum, :reduce]}, _, _}), do: true
defp reduce_call?(_), do: false
defp max_reduce_body?([
_enum,
_acc,
{:fn, _, [{:->, _, [_args, body]}]}
]) do
explicit_max?(body)
end
defp max_reduce_body?(_), do: false
# Safely unwrap single-expression blocks (added by formatter/parser occasionally)
defp explicit_max?({:__block__, _, [body]}), do: explicit_max?(body)
# Match unqualified Kernel.max/2 calls
defp explicit_max?({:max, _, [_, _]}), do: true
# Match `if >` (ignoring strict keyword list length to account for AST metadata)
defp explicit_max?({:if, _, [{:>, _, [_, _]}, _opts]}), do: true
# Match `if >=`
defp explicit_max?({:if, _, [{:>=, _, [_, _]}, _opts]}), do: true
# Fallback
defp explicit_max?(_), do: false
end