Current section
Files
Jump to
Current section
Files
lib/pattern/no_split_to_count.ex
defmodule Credence.Pattern.NoSplitToCount do
@moduledoc """
Detects `length(String.split(string, separator)) - 1` used to count
substring occurrences.
This is a direct translation of Python's `str.count(target)`. While it
works correctly, it allocates the full list of split segments only to
count them and throw the list away.
## Bad
count = length(String.split(downcased, target)) - 1
## Good
# For single-character targets, count matching graphemes directly:
count = downcased |> String.graphemes() |> Enum.count(&(&1 == target))
# For multi-character targets, use the Erlang binary matching BIF:
count = :binary.matches(downcased, target) |> length()
## Auto-fix
Not auto-fixable — the best replacement depends on whether the
separator is a single character or a multi-character substring.
"""
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
# length(String.split(str, sep)) - 1
{:-, meta,
[
{:length, _, [{{:., _, [{:__aliases__, _, [:String]}, :split]}, _, [_str, _sep]}]},
1
]} = node,
acc ->
{node, [build_issue(meta) | acc]}
# length(String.split(str, sep)) - 1 (with unary minus AST for 1)
{:-, meta,
[
{:length, _, [{{:., _, [{:__aliases__, _, [:String]}, :split]}, _, [_str, _sep]}]},
{:__block__, _, [1]}
]} = node,
acc ->
{node, [build_issue(meta) | acc]}
node, acc ->
{node, acc}
end)
Enum.reverse(issues)
end
defp build_issue(meta) do
%Issue{
rule: :no_split_to_count,
message: """
`length(String.split(str, sep)) - 1` allocates a list of all split \
segments just to count them.
For single-character targets, count graphemes directly:
str |> String.graphemes() |> Enum.count(&(&1 == target))
For multi-character targets, use the Erlang BIF:
:binary.matches(str, target) |> length()
""",
meta: %{line: Keyword.get(meta, :line)}
}
end
end