Mutare.Transform.NodeRange (mutare v0.2.0)

Copy Markdown View Source

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

get(node)

@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.