Sourceror.get_range/1, corrected where its answer would corrupt a survivor's reported
location or diff: surrounding parentheses, a leading parenthesized callee, a bitstring's
over-counted end, and an escaped-delimiter under-count.
Surrounding parentheses. Sourceror extends a parenthesized node's range over the
parentheses written around it. The text a Mutare.Site renders for that node never includes
them — (a + b) * c records a + b → a - b — so patched over the wider span the report
would show a - b * c, a different program from the mutant that ran, and &(&1 > 2) would
become the unparseable &&1 >= 2. The parentheses belong to the context the node sits in, so
get/1 ranges the node without them, however many layers or however spaced.
A leading call on a parenthesized callee. Sourceror ranges (fn x -> x end).(1) and
(a).b from inside the callee's parentheses, and with them every node that begins with
one. A whole-expression replacement over that span leaves the ( behind —
(fn … end).(1) |> f() → nil patched to the unparseable (nil. get/1 starts the range at
the earliest start down the node's left spine — and, for the mirror image
(0 == (if x do … end), ranged to inside the )), ends it at the latest end down the right
spine.
The escaped-delimiter under-count in sigils and interpolated strings.
Sourceror computes a sigil's end column from the stored content length
(range.ex get_end_pos_for_interpolation_segments/3, String.length of the
<<>> segments). The tokenizer keeps a sigil's content raw — \n/\\/\t
stay two-character sequences — except it collapses an escaped closing
delimiter (\/ → / in ~r/…/, \} → } in ~r{…}), so the stored content
is one byte shorter per such escape and the range falls short by that many
columns. A textual patch over the short range leaves the sigil's tail in place —
and when the mutation only drops a trailing flag (~r/…/u → ~r/…/,
RegexLiteral), the patch lands exactly on the dropped u and the diff shows
no change at all (a survivor with an empty diff). get/1 adds back one
column per collapsed closing delimiter. (Escaped delimiters before the last
interpolation are already accounted for — their absolute closing position is
baked into the segment metadata — so only the trailing binary segments are
counted; an opening delimiter escape \{ keeps its backslash and never
collapses, so only the closing char is counted.)
The same collapse hits the interpolated string family — "a\"#{x}\"b" and
its charlist / quoted-atom cousins also range from segment lengths, and the
tokenizer collapses \" → " (or \' → ') there too, so an escaped quote
after the last interpolation shortens the range and the patch leaves the
original closing quote behind (toast("…\"#{x}\".") mutated to "" renders
as toast(""")). get/1 applies the same trailing-segment count to those
three container shapes. Non-interpolated strings are immune (their raw content
keeps the backslash), and heredocs never escape their fence, so only the
quote-delimited (non-heredoc), single-line, interpolated forms are corrected.
Only Mutare.Report reads the range, so the quirk is invisible at runtime:
the metamutant is built from the AST, never the range. It corrupts only a
surviving mutant's rendered location — the human diff, and the endColumn the
machine reporters emit (Mutare.Report.Json / Mutare.Report.Sarif).
See NOTES "Sourceror range".
Summary
Functions
Like Sourceror.get_range/1, correcting the sigil/interpolated-string under-count.
Functions
@spec get(Macro.t()) :: Sourceror.Range.t() | nil
Like Sourceror.get_range/1, correcting the sigil/interpolated-string under-count.