Current section
Files
Jump to
Current section
Files
lib/rule/no_string_length_for_char_check.ex
defmodule Credence.Rule.NoStringLengthForCharCheck do
@moduledoc """
Performance rule: Detects `String.length(x) == 1` (or `!= 1`) used to
validate that a string is a single character.
`String.length/1` traverses the entire string to count grapheme clusters,
making it O(n). For a simple single-character check, pattern matching on
the result of `String.graphemes/1` or using `String.to_charlist/1` is more
efficient and expressive.
## Bad
if String.length(target_char) != 1 do
raise ArgumentError, "expected a single character"
end
## Good
case String.graphemes(target_char) do
[_single] -> :ok
_ -> raise ArgumentError, "expected a single character"
end
# Or use a function head with a guard on byte_size for ASCII:
def count_char(string, <<_::utf8>> = target) do
...
end
"""
@behaviour Credence.Rule
alias Credence.Issue
@impl true
def check(ast, _opts) do
{_ast, issues} =
Macro.prewalk(ast, [], fn
# Match: String.length(x) == 1, String.length(x) != 1, etc.
{op, meta,
[
{{:., _, [{:__aliases__, _, [:String]}, :length]}, _, [_arg]},
1
]} = node,
issues
when op in [:==, :!=, :===, :!==] ->
{node, [build_issue(meta) | issues]}
# Match the reversed form: 1 == String.length(x)
{op, meta,
[
1,
{{:., _, [{:__aliases__, _, [:String]}, :length]}, _, [_arg]}
]} = node,
issues
when op in [:==, :!=, :===, :!==] ->
{node, [build_issue(meta) | issues]}
node, issues ->
{node, issues}
end)
Enum.reverse(issues)
end
defp build_issue(meta) do
%Issue{
rule: :no_string_length_for_char_check,
severity: :info,
message:
"`String.length/1` traverses the entire string (O(n)) just to check for a single character. " <>
"Use pattern matching (e.g. `<<_::utf8>>`) or `String.graphemes/1` with a match instead.",
meta: %{line: Keyword.get(meta, :line)}
}
end
end