splitrs 0.3.4

AST-based Rust refactoring tool with trait separation, config files, and intelligent module generation
Documentation
//! Verbatim source slicing: map `proc_macro2::Span` (line/column) back to exact
//! original source byte ranges so split items can be emitted byte-for-byte,
//! preserving inline `//` comments and original formatting that `prettyplease`
//! would otherwise strip or reflow.

// `span_slice` / `item_verbatim` are part of this module's public API and are
// exercised by the unit tests, but the binary target currently only calls
// `item_verbatim_with_indent` / `impl_header_verbatim`. The binary recompiles
// these modules directly (separate from the lib target), so the unused public
// methods would emit `dead_code` on the bin build. Mirror the `file_analyzer`
// pattern and allow it: these items are an intentional shared internal API.
#![allow(dead_code)]

use proc_macro2::{LineColumn, Span};

/// Index over the original source enabling (line, column) -> byte-offset mapping
/// and exact slice extraction for spans.
pub struct SourceMap<'a> {
    source: &'a str,
    /// Byte offset at which each line starts. `line_starts[0]` == 0 (line 1).
    line_starts: Vec<usize>,
}

impl<'a> SourceMap<'a> {
    /// Build a line-offset index over `source`. Line 1 starts at byte 0; each
    /// subsequent entry is the byte offset immediately AFTER a `\n`.
    pub fn new(source: &'a str) -> Self {
        let mut line_starts = vec![0usize];
        for (i, b) in source.bytes().enumerate() {
            if b == b'\n' {
                line_starts.push(i + 1);
            }
        }
        Self {
            source,
            line_starts,
        }
    }

    /// Convert a 1-based line / 0-based UTF-8 *char* column to a byte offset.
    /// Walks `column` chars from the line start so multibyte chars are handled.
    /// Returns `None` if the line is out of range. Clamps a column past
    /// end-of-line to the line's end (defensive; proc-macro2 columns are valid).
    fn line_col_to_byte(&self, line: usize, column: usize) -> Option<usize> {
        if line == 0 || line > self.line_starts.len() {
            return None;
        }
        let line_start = self.line_starts[line - 1];
        // Determine this line's exclusive end (start of next line, or source end).
        let line_end = self
            .line_starts
            .get(line) // next line's start
            .copied()
            .unwrap_or(self.source.len());
        let line_slice = &self.source[line_start..line_end];
        // Walk `column` chars.
        let mut byte = line_start;
        let mut chars = line_slice.char_indices();
        for _ in 0..column {
            match chars.next() {
                Some((_, c)) => byte += c.len_utf8(),
                None => return Some(line_end), // clamp
            }
        }
        Some(byte)
    }

    /// Exact source slice for `span` (start..end), or `None` if either endpoint
    /// maps out of range. Uses `Span::start()`/`Span::end()` (real `LineColumn`
    /// when `span-locations` is enabled).
    pub fn span_slice(&self, span: Span) -> Option<&'a str> {
        let start = span.start();
        let end = span.end();
        let start_byte = self.line_col_to_byte(start.line, start.column)?;
        let end_byte = self.line_col_to_byte(end.line, end.column)?;
        if start_byte > end_byte || end_byte > self.source.len() {
            return None;
        }
        Some(&self.source[start_byte..end_byte])
    }

    /// Earliest start `LineColumn` among an item's own span start and all its
    /// attributes' span starts. Captures leading `#[...]` and `///` doc comments
    /// (attributes ARE part of the item and carry spans), which begin BEFORE the
    /// item's main token span.
    fn earliest_start(item_span: Span, attrs: &[syn::Attribute]) -> LineColumn {
        use syn::spanned::Spanned;
        let mut earliest = item_span.start();
        for attr in attrs {
            let s = attr.span().start();
            if (s.line, s.column) < (earliest.line, earliest.column) {
                earliest = s;
            }
        }
        earliest
    }

    /// Verbatim slice for an item INCLUDING its leading attributes/doc comments,
    /// from the earliest attribute/item start through the item span end (closing
    /// brace). Inner `//` comments fall naturally within the braces.
    ///
    /// NOTE: starts at the first non-whitespace token column, so the FIRST line's
    /// leading indentation is not included. Use [`Self::item_verbatim_with_indent`]
    /// when you need the original indentation preserved (e.g. emitting methods
    /// inside a re-synthesised `impl` block).
    pub fn item_verbatim(&self, item_span: Span, attrs: &[syn::Attribute]) -> Option<&'a str> {
        let start = Self::earliest_start(item_span, attrs);
        let end = item_span.end();
        let start_byte = self.line_col_to_byte(start.line, start.column)?;
        let end_byte = self.line_col_to_byte(end.line, end.column)?;
        if start_byte > end_byte || end_byte > self.source.len() {
            return None;
        }
        Some(&self.source[start_byte..end_byte])
    }

    /// Like [`Self::item_verbatim`] but starts at the BEGINNING of the line the
    /// earliest token sits on, preserving that line's original leading
    /// indentation. Subsequent lines already retain their indentation (they are
    /// mid-slice). Used for methods/trait-impls so they keep original layout.
    pub fn item_verbatim_with_indent(
        &self,
        item_span: Span,
        attrs: &[syn::Attribute],
    ) -> Option<&'a str> {
        let start = Self::earliest_start(item_span, attrs);
        if start.line == 0 || start.line > self.line_starts.len() {
            return None;
        }
        let start_byte = self.line_starts[start.line - 1]; // include leading indentation
        let end = item_span.end();
        let end_byte = self.line_col_to_byte(end.line, end.column)?;
        if start_byte > end_byte || end_byte > self.source.len() {
            return None;
        }
        Some(&self.source[start_byte..end_byte])
    }

    /// Verbatim text of an impl block's header: from the impl's earliest start
    /// (incl. attributes) up to AND INCLUDING the opening `{`. Lets callers wrap
    /// verbatim method bodies in the byte-faithful original `impl ... {` line.
    pub fn impl_header_verbatim(&self, item: &syn::ItemImpl) -> Option<String> {
        use syn::spanned::Spanned;
        let start = Self::earliest_start(item.span(), &item.attrs);
        let start_byte = self.line_col_to_byte(start.line, start.column)?;
        // Opening-brace location via the brace token's delimiter span.
        let brace_open = item.brace_token.span.open();
        let bo = brace_open.start();
        let brace_open_byte = self.line_col_to_byte(bo.line, bo.column)?;
        // `brace_open_byte` points AT the `{`; include it (ASCII `{` is 1 byte).
        let end = brace_open_byte + 1;
        if start_byte > end || end > self.source.len() {
            return None;
        }
        Some(self.source[start_byte..end].to_string())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use syn::spanned::Spanned;

    #[test]
    fn test_line_starts() {
        // "ab\ncd\n\nef" -> lines start at 0, 3, 6, 7
        let sm = SourceMap::new("ab\ncd\n\nef");
        assert_eq!(sm.line_starts, vec![0, 3, 6, 7]);
    }

    #[test]
    fn test_line_col_to_byte_ascii() {
        let src = "abc\ndef\n";
        let sm = SourceMap::new(src);
        // line 1 col 0 -> 0 ; line 1 col 2 -> 2 ; line 2 col 0 -> 4 ; line 2 col 1 -> 5
        assert_eq!(sm.line_col_to_byte(1, 0), Some(0));
        assert_eq!(sm.line_col_to_byte(1, 2), Some(2));
        assert_eq!(sm.line_col_to_byte(2, 0), Some(4));
        assert_eq!(sm.line_col_to_byte(2, 1), Some(5));
        assert_eq!(sm.line_col_to_byte(0, 0), None);
        assert_eq!(sm.line_col_to_byte(99, 0), None);
    }

    #[test]
    fn test_line_col_to_byte_multibyte() {
        // line 2 is "αβ" — each Greek letter is 2 bytes in UTF-8.
        let src = "a\nαβ\nz";
        let sm = SourceMap::new(src);
        // line 2 starts after "a\n" = byte 2.
        // col 0 -> byte 2 ; col 1 -> byte 2 + 2 = 4 ; col 2 -> byte 6 (end of αβ).
        assert_eq!(sm.line_col_to_byte(2, 0), Some(2));
        assert_eq!(sm.line_col_to_byte(2, 1), Some(4));
        assert_eq!(sm.line_col_to_byte(2, 2), Some(6));
        // verify the slice is exactly "αβ"
        let (a, b) = (
            sm.line_col_to_byte(2, 0).expect("start"),
            sm.line_col_to_byte(2, 2).expect("end"),
        );
        assert_eq!(&src[a..b], "αβ");
    }

    #[test]
    fn test_item_verbatim_preserves_doc_and_inner_comment() {
        let src = "/// outer doc\npub fn f() {\n    // inner note\n    let x = 1;\n}\n";
        let item: syn::ItemFn = syn::parse_str(src).expect("parse ItemFn");
        let sm = SourceMap::new(src);
        let slice = sm
            .item_verbatim(item.span(), &item.attrs)
            .expect("verbatim slice");
        assert!(slice.contains("/// outer doc"), "missing doc: {slice:?}");
        assert!(slice.contains("// inner note"), "missing inner: {slice:?}");
        assert!(slice.contains("let x = 1;"), "missing body: {slice:?}");
    }

    #[test]
    fn test_item_verbatim_with_indent_preserves_indentation() {
        // The fn is indented 4 spaces. parse_str on a single indented fn keeps
        // spans relative to THIS string, so the indent precedes col-of-`pub`.
        let src = "    pub fn f() {\n        let x = 1;\n    }\n";
        let item: syn::ItemFn = syn::parse_str(src).expect("parse ItemFn");
        let sm = SourceMap::new(src);
        let with_indent = sm
            .item_verbatim_with_indent(item.span(), &item.attrs)
            .expect("indented slice");
        assert!(
            with_indent.starts_with("    pub fn f()"),
            "indentation not preserved: {with_indent:?}"
        );
        // The non-indent variant should NOT start with spaces.
        let no_indent = sm.item_verbatim(item.span(), &item.attrs).expect("slice");
        assert!(no_indent.starts_with("pub fn f()"), "got: {no_indent:?}");
    }

    #[test]
    fn test_span_slice_exact() {
        let src = "pub fn g() {}\n";
        let item: syn::ItemFn = syn::parse_str(src).expect("parse");
        let sm = SourceMap::new(src);
        let slice = sm.span_slice(item.span()).expect("slice");
        assert_eq!(slice, "pub fn g() {}");
    }

    #[test]
    fn test_impl_header_verbatim() {
        let src = "impl Foo {\n    fn a(&self) {}\n}\n";
        let item: syn::ItemImpl = syn::parse_str(src).expect("parse impl");
        let sm = SourceMap::new(src);
        let header = sm.impl_header_verbatim(&item).expect("header");
        assert_eq!(header, "impl Foo {");
    }
}