Current section

Files

Jump to
credence lib semantic no_non_negated_integer.ex
Raw

lib/semantic/no_non_negated_integer.ex

defmodule Credence.Semantic.NoNonNegatedInteger do
@moduledoc """
Fixes compiler errors about the undefined type `non_negated_integer/0`.
LLMs commonly produce `non_negated_integer()` in typespecs when they mean
`non_neg_integer()`. The Elixir compiler emits:
type non_negated_integer/0 undefined (no such type in Module)
The fix is the textual substitution `non_negated_integer`
`non_neg_integer` — both types describe the same set of values — but it is
applied **only on the line the compiler flagged**. That line is a type-only
context (`@spec`/`@type`/`@callback`/…), so the rename touches a type
reference and nothing else; a blanket replace across the whole source would
also rewrite an unrelated string literal, comment, atom or identifier named
`non_negated_integer` elsewhere in the file, which changes the program's
answer.
The rule runs at a lower `priority` than `NoBareNamesInSpec`: that rule's
`type _/0 undefined` matcher also matches this diagnostic but cannot fix the
parenthesised type call (`non_negated_integer()`), so this rule must be tried
first or it would never fire.
"""
use Credence.Semantic.Rule
alias Credence.Issue
@impl true
def priority, do: 400
@impl true
def match?(%{severity: :error, message: msg}) when is_binary(msg) do
String.contains?(msg, "non_negated_integer") and
String.contains?(msg, "undefined")
end
def match?(_), do: false
@impl true
def to_issue(diagnostic) do
%Issue{
rule: :no_non_negated_integer,
message: diagnostic.message,
meta: %{line: line(diagnostic)}
}
end
@impl true
def fix(source, diagnostic) do
case line(diagnostic) do
line_no when is_integer(line_no) ->
source
|> String.split("\n")
|> Enum.with_index(1)
|> Enum.map_join("\n", fn
{text, ^line_no} ->
String.replace(text, "non_negated_integer", "non_neg_integer")
{text, _} ->
text
end)
_ ->
source
end
end
defp line(%{position: {line, _col}}), do: line
defp line(%{position: line}) when is_integer(line), do: line
defp line(_), do: nil
end