Current section

Files

Jump to
credence lib semantic undefined_function.ex
Raw

lib/semantic/undefined_function.ex

defmodule Credence.Semantic.UndefinedFunction do
@moduledoc """
Fixes compiler warnings about undefined functions with known replacements.
LLMs sometimes use functions that don't exist in the expected module.
This rule maintains a mapping of known corrections.
## Example
# Warning: Enum.last/1 is undefined or private
list |> Enum.last()
# Fixed:
list |> List.last()
"""
use Credence.Semantic.Rule
alias Credence.Issue
# Map of {module, function, arity} → {correct_module, correct_function}
@replacements %{
{"Enum", "last", 1} => {"List", "last"},
{"Enum", "last", 0} => {"List", "last"}
}
@impl true
def match?(%{severity: :warning, message: msg}) do
String.contains?(msg, "is undefined or private") and
parse_function_ref(msg) != nil and
Map.has_key?(@replacements, parse_function_ref(msg))
end
def match?(_), do: false
@impl true
def to_issue(%{message: msg, position: position}) do
%Issue{
rule: :undefined_function,
message: msg,
meta: %{line: extract_line(position)}
}
end
@impl true
def fix(source, %{message: msg, position: position}) do
line_no = extract_line(position)
case parse_function_ref(msg) do
{mod, fun, _arity} = key ->
case Map.get(@replacements, key) do
{new_mod, new_fun} ->
replace_on_line(source, line_no, "#{mod}.#{fun}", "#{new_mod}.#{new_fun}")
nil ->
source
end
nil ->
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 parse_function_ref(msg) do
case Regex.run(~r/(\w+)\.(\w+)\/(\d+) is undefined or private/, msg) do
[_, mod, fun, arity] -> {mod, fun, String.to_integer(arity)}
_ -> nil
end
end
defp replace_on_line(source, line_no, old, new) do
source
|> String.split("\n")
|> Enum.with_index(1)
|> Enum.map(fn
{line, ^line_no} -> String.replace(line, old, new, global: false)
{line, _} -> line
end)
|> Enum.join("\n")
end
end