Mutare.Transform.NodeRange (mutare v0.1.2)

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.