Current section

Files

Jump to
credence lib syntax fix_python_modulo.ex
Raw

lib/syntax/fix_python_modulo.ex

defmodule Credence.Syntax.FixPythonModulo do
@moduledoc """
Replaces Python's `%` modulo operator with Elixir's `rem/2`.
LLMs translating from Python carry over the `%` infix operator for
modulo arithmetic. In Elixir, `%` is used for maps and structs, not
arithmetic — the modulo function is `rem/2` (or `Integer.mod/2` for
floor-division semantics).
This is a Syntax rule because `a % b` won't parse in Elixir.
## Detected patterns
year % 4 n % 2 == 0
n % divisor 100 % 7
Any `word % word` where `%` is used as an infix operator between two
identifiers or integers.
## Not flagged
Legitimate Elixir `%` usage is not affected:
%{key: value} — map literal
%MyStruct{field: val} — struct literal
%{map | key: new} — map update
Float operands (`n % 2.0`) are skipped because `rem/2` only accepts
integers. Comment lines are also skipped.
## Bad
def leap_year?(year) when year % 4 != 0, do: false
def even?(n), do: n % 2 == 0
## Good
def leap_year?(year) when rem(year, 4) != 0, do: false
def even?(n), do: rem(n, 2) == 0
"""
use Credence.Syntax.Rule
alias Credence.Issue
# Matches word % word where the second word is NOT followed by a dot
# (prevents matching n % 2.0 which would produce broken rem(n, 2).0).
# Safe against maps/structs because %{ and %Struct{ never have \w+ before %.
@modulo_pattern ~r/(\w+)\s*%\s*(\w+)(?!\.)/
@impl true
def analyze(source) do
source
|> String.split("\n")
|> Enum.with_index(1)
|> Enum.flat_map(fn {line, line_no} ->
if modulo_line?(line), do: [build_issue(line_no)], else: []
end)
end
@impl true
def fix(source) do
source
|> String.split("\n")
|> Enum.map_join("\n", fn line ->
if modulo_line?(line), do: fix_line(line), else: line
end)
end
defp modulo_line?(line) do
not comment?(line) and Regex.match?(@modulo_pattern, line)
end
defp comment?(line), do: Regex.match?(~r/^\s*#/, line)
defp fix_line(line) do
Regex.replace(@modulo_pattern, line, "rem(\\1, \\2)")
end
defp build_issue(line_no) do
%Issue{
rule: :python_modulo,
message:
"Python's `%` operator does not exist in Elixir. " <>
"Use `rem(a, b)` for modulo arithmetic.",
meta: %{line: line_no}
}
end
end