Current section
Files
Jump to
Current section
Files
lib/syntax/no_fn_with_capture.ex
defmodule Credence.Syntax.NoFnWithCapture do
@moduledoc """
Repairs `fn(&1 ...)` — the `fn` keyword mistakenly mixed with capture syntax.
LLMs translating from other languages repeatedly emit `fn(&1 > 0)`, gluing the
`fn` keyword onto a capture body. In Elixir `fn` opens a clause that needs
`-> body end`, so `fn(&1 ...)` never parses — the compiler reports a mismatched
delimiter (the `)` arrives where an `end` was expected). It is always a syntax
error, so this is a REPAIR: the fix rewrites the leading `fn(` to `&(`,
yielding the idiomatic capture form.
Only `fn(` immediately followed by a capture variable (`&1`, `&2`, …) is
touched — `fn(x) -> ... end` (parenthesised parameters) is valid Elixir and a
capture variable can never legally appear in that position, so the match fires
exclusively on the malformed shape.
## Bad (won't parse)
Enum.filter(list, fn(&1 > 0))
## Good
Enum.filter(list, &(&1 > 0))
"""
use Credence.Syntax.Rule
alias Credence.Issue
# `fn(` (standalone — not the tail of an identifier like `myfn(`) immediately
# followed by a capture variable (`&1`, `&2`, …).
@fn_capture_pattern ~r/\bfn\(&\d/
@impl true
def analyze(source) do
source
|> String.split("\n")
|> Enum.with_index(1)
|> Enum.flat_map(fn {line, line_no} ->
if fn_capture_line?(line) do
[
%Issue{
rule: :no_fn_with_capture,
message:
"`fn(` followed by a capture variable is a parse error; " <>
"use `&(...)` capture syntax instead.",
meta: %{line: line_no}
}
]
else
[]
end
end)
end
@impl true
def fix(source) do
source
|> String.split("\n")
|> Enum.map_join("\n", fn line ->
if fn_capture_line?(line), do: fix_line(line), else: line
end)
end
# `check` and `fix` share this one predicate, so they never disagree. Comment
# lines are skipped: `fn(&1 ...)` inside a comment (or a heredoc body line that
# starts with `#`) is text, not the parse error we repair — rewriting it would
# corrupt non-code content of an already-broken file.
defp fn_capture_line?(line) do
not comment?(line) and Regex.match?(@fn_capture_pattern, line)
end
defp comment?(line), do: Regex.match?(~r/^\s*#/, line)
defp fix_line(line) do
Regex.replace(@fn_capture_pattern, line, fn match ->
String.replace_prefix(match, "fn(", "&(")
end)
end
end