Current section
Files
Jump to
Current section
Files
lib/rule/no_take_while_length_check.ex
defmodule Credence.Rule.NoTakeWhileLengthCheck do
@moduledoc """
Detects `Enum.take_while/2` piped into `length/1` or `Enum.count/1`,
which materializes an intermediate list only to count it.
## Why this matters
LLMs generate this pattern when they think procedurally: "keep going
while the condition holds, then check how far I got." The idiomatic
Elixir approach depends on intent:
# Flagged — materializes a list just to count it
0..(half - 1)
|> Enum.take_while(fn i ->
Enum.at(graphemes, start + i) == Enum.at(graphemes, start + len - 1 - i)
end)
|> length() == half
# If checking "did all pass?" → use Enum.all?/2
Enum.all?(0..(half - 1), fn i -> ... end)
# If counting consecutive matches → use Enum.reduce_while/3
Enum.reduce_while(range, 0, fn i, count ->
if condition, do: {:cont, count + 1}, else: {:halt, count}
end)
`Enum.take_while |> length` always allocates a throwaway list.
`Enum.all?` and `Enum.reduce_while` do not.
## Flagged patterns
- `Enum.take_while(enum, fun) |> length()`
- `Enum.take_while(enum, fun) |> Enum.count()`
- `length(Enum.take_while(enum, fun))`
- `Enum.count(Enum.take_while(enum, fun))`
- `enum |> Enum.take_while(fun) |> length()`
## Severity
`:warning`
"""
@behaviour Credence.Rule
alias Credence.Issue
@impl true
def check(ast, _opts) do
{_ast, issues} =
Macro.prewalk(ast, [], fn node, issues ->
case check_node(node) do
{:ok, issue} -> {node, [issue | issues]}
:error -> {node, issues}
end
end)
Enum.reverse(issues)
end
# ------------------------------------------------------------
# NODE MATCHING
# ------------------------------------------------------------
# Pipeline form: ... |> Enum.take_while(fn) |> length()
# or: ... |> Enum.take_while(fn) |> Enum.count()
defp check_node({:|>, meta, _} = node) do
pipeline = flatten_pipeline(node)
check_pipeline(pipeline, meta)
end
# Direct call: length(Enum.take_while(enum, fun))
defp check_node({:length, meta, [inner]}) do
if take_while_call?(inner) do
{:ok, build_issue(meta)}
else
:error
end
end
# Direct call: Enum.count(Enum.take_while(enum, fun))
defp check_node({{:., meta, [mod, :count]}, _, [inner]}) do
if enum_module?(mod) and take_while_call?(inner) do
{:ok, build_issue(meta)}
else
:error
end
end
defp check_node(_), do: :error
# ------------------------------------------------------------
# PIPELINE ANALYSIS
# ------------------------------------------------------------
defp check_pipeline(steps, meta) do
steps
|> Enum.chunk_every(2, 1, :discard)
|> Enum.any?(fn [first, second] ->
take_while_step?(first) and length_step?(second)
end)
|> then(fn
true -> {:ok, build_issue(meta)}
false -> :error
end)
end
# ------------------------------------------------------------
# STEP DETECTION
# ------------------------------------------------------------
# Enum.take_while(enum, fun) — full call
defp take_while_call?({{:., _, [mod, :take_while]}, _, args})
when is_list(args) and length(args) == 2 do
enum_module?(mod)
end
defp take_while_call?(_), do: false
# Enum.take_while(fun) — pipeline form (1 explicit arg)
defp take_while_step?({{:., _, [mod, :take_while]}, _, args})
when is_list(args) do
enum_module?(mod)
end
defp take_while_step?(_), do: false
# length() — pipeline form (0 explicit args)
defp length_step?({:length, _, []}), do: true
# Enum.count() — pipeline form (0 explicit args)
defp length_step?({{:., _, [mod, :count]}, _, []}) do
enum_module?(mod)
end
defp length_step?(_), do: false
# ------------------------------------------------------------
# HELPERS
# ------------------------------------------------------------
defp flatten_pipeline({:|>, _, [left, right]}) do
flatten_pipeline(left) ++ [right]
end
defp flatten_pipeline(expr), do: [expr]
defp enum_module?({:__aliases__, _, [:Enum]}), do: true
defp enum_module?(_), do: false
# ------------------------------------------------------------
# MESSAGE GENERATION
# ------------------------------------------------------------
defp build_issue(meta) do
%Issue{
rule: :no_take_while_length_check,
severity: :warning,
message: build_message(),
meta: %{line: Keyword.get(meta, :line)}
}
end
defp build_message do
"""
`Enum.take_while/2` piped into `length/1` allocates an intermediate \
list just to count it.
If checking whether all elements pass the condition:
Enum.all?(enumerable, predicate)
If counting consecutive matches from the start:
Enum.reduce_while(enumerable, 0, fn elem, count ->
if condition, do: {:cont, count + 1}, else: {:halt, count}
end)
"""
end
end