Sourceror.get_range/1 with corrections for three upstream quirks that would
otherwise corrupt a survivor's reported location.
1. The bare-atom over-count. Sourceror sizes an atom literal as its name
plus one column for a colon — right for a written atom (:foo, leading
colon) and for a keyword-list key (foo:, trailing colon). But the three
reserved-word atoms true/false/nil are written bare, with no colon, so
their range comes back one column too wide. A textual patch over that range then
eats the following character — e.g. String.split(re, trim: true) with the
true swapped renders as …, trim: false (closing paren swallowed). get/1
trims that phantom column.
2. 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.
3. The multi-line unary-negation over-count. Sourceror sizes a prefix
not X / !X as if the operator's own width extended the operand, so a
multi-line negation reports an end a few columns past the operand's real end
(not exists(\n …\n) lands a few columns into whatever trails the closing
delimiter; a single-line negation is exact). A prefix operator ends exactly
where its operand does, so get/1 clamps the node's end back to the operand's.
Unlike quirks 1–2 this one never touches the human diff — that reads whole
source lines by range and the over-count is in the column, not the line — but
the machine reporters emit the raw endColumn (Mutare.Report.Json /
Mutare.Report.Sarif), so it is corrected at the source. (A parenthesized
not(X) carries no meta to distinguish it and takes this path too; there the
clamp lands one column short of the outer paren — still far closer than the raw
over-count, and never past the real end.)
Only Mutare.Report reads the range, so the quirks are invisible at runtime:
the metamutant is built from the AST, never the range. They corrupt only a
surviving mutant's rendered location — the human diff (quirks 1–2) or a machine
reporter's end column (quirk 3). See NOTES "Sourceror range".
Summary
Functions
Like Sourceror.get_range/1, correcting the bare-atom and multi-line-negation over-counts and the sigil/interpolated-string under-count.
Functions
@spec get(Macro.t()) :: Sourceror.Range.t() | nil
Like Sourceror.get_range/1, correcting the bare-atom and multi-line-negation over-counts and the sigil/interpolated-string under-count.