Current section
Files
Jump to
Current section
Files
lib/semantic/undefined_string_alphanumeric.ex
defmodule Credence.Semantic.UndefinedStringAlphanumeric do
@moduledoc """
Fixes the common LLM hallucination where `String.alphanumeric?/1` is called
as if it were a real Elixir standard library function. It is not.
LLMs translating Python's `str.isalnum()` frequently produce:
Enum.filter(&String.alphanumeric?/1)
This compiles but warns `String.alphanumeric?/1 is undefined or private`,
which becomes a hard error under `--warnings-as-errors`.
The fix replaces:
- `&String.alphanumeric?/1` → `fn char -> String.match?(char, ~r/^[a-zA-Z0-9]$/) end`
- `String.alphanumeric?(expr)` → `String.match?(expr, ~r/^[a-zA-Z0-9]$/)`
"""
use Credence.Semantic.Rule
alias Credence.Issue
@replacement "fn char -> String.match?(char, ~r/^[a-zA-Z0-9]$/) end"
@impl true
def match?(%{severity: :warning, message: msg}) do
String.contains?(msg, "String.alphanumeric?") and
String.contains?(msg, "is undefined or private")
end
def match?(_), do: false
@impl true
def to_issue(%{message: msg, position: position}) do
%Issue{
rule: :undefined_string_alphanumeric,
message: msg,
meta: %{line: extract_line(position)}
}
end
@impl true
def fix(source, %{position: position}) do
line_no = extract_line(position)
if line_no do
fix_line(source, line_no)
else
source
end
end
defp extract_line({line, _col}) when is_integer(line), do: line
defp extract_line(line) when is_integer(line), do: line
defp extract_line(_), do: nil
defp fix_line(source, line_no) do
source
|> String.split("\n")
|> Enum.with_index(1)
|> Enum.map_join("\n", fn
{line, ^line_no} -> replace_alphanumeric(line)
{line, _} -> line
end)
end
defp replace_alphanumeric(line) do
line
# Capture form: &String.alphanumeric?/1
|> String.replace("&String.alphanumeric?/1", @replacement)
# Direct call form: String.alphanumeric?(expr)
|> then(fn l ->
Regex.replace(~r/String\.alphanumeric\?\(([^)]+)\)/, l, fn _, arg ->
"String.match?(#{String.trim(arg)}, ~r/^[a-zA-Z0-9]$/)"
end)
end)
end
end