Current section
Files
Jump to
Current section
Files
lib/syntax/no_doc_with_do_block.ex
defmodule Credence.Syntax.NoDocWithDoBlock do
@moduledoc """
Repairs a documentation attribute written with a stray `do` block.
LLMs repeatedly emit `@doc "..." do` — gluing a `do` onto a `@doc`
attribute. A documentation attribute (`@doc`, `@moduledoc`, `@typedoc`)
never takes a `do` block, and the dangling `do` has no matching `end`, so the
module fails to parse (`missing terminator: end`). It is always a mistake, so
this is a REPAIR: drop the trailing ` do`, which rebalances the block
structure and leaves the attribute and its value untouched.
The match is anchored to a documentation attribute whose value is a **complete
double-quoted string literal** immediately followed by a bare ` do` at end of
line. That is the canonical LLM mistake (`@doc "..." do`), and a string-literal
doc with a do-block never compiles — so the only thing this can match is the
bug. A normal `@doc "..."`, a `def f do`, or a value whose string merely
contains the word "do" (which ends in a quote, not ` do`) are never touched.
Deliberately left alone (narrowed out): a documentation attribute whose value
is an *expression* ending in `do`, e.g. a multi-line conditional doc
@doc (if prod? do
"..."
else
"..."
end)
This is valid, compiling code. Because the syntax phase runs every rule's
`fix` whenever the source fails to parse for *any* reason, a line-by-line
regex broad enough to catch `@doc (... do` would also strip the `do` from
this valid block when some unrelated part of the file fails to parse. Anchoring
to a complete string literal keeps the repair to the provably-bug shape.
## Bad (won't parse — missing terminator: end)
@doc "top_n_items/2" do
def find_top_n(map, n), do: Enum.take(map, n)
## Good
@doc "top_n_items/2"
def find_top_n(map, n), do: Enum.take(map, n)
"""
use Credence.Syntax.Rule
alias Credence.Issue
# A documentation attribute whose value is a complete double-quoted string
# literal, immediately followed by a stray bare `do` at end of line.
@doc_do ~r/^(\s*@(?:doc|moduledoc|typedoc)\s+"(?:[^"\\]|\\.)*")\s+do\s*$/
@impl true
def analyze(source) do
source
|> String.split("\n")
|> Enum.with_index(1)
|> Enum.flat_map(fn {line, line_no} ->
if Regex.match?(@doc_do, line) do
[
%Issue{
rule: :no_doc_with_do_block,
message: "Documentation attribute has a stray `do`; remove it.",
meta: %{line: line_no}
}
]
else
[]
end
end)
end
@impl true
def fix(source) do
source
|> String.split("\n")
|> Enum.map_join("\n", &Regex.replace(@doc_do, &1, "\\1"))
end
end