rscheck-cli 0.1.0-alpha.3

CLI frontend for the rscheck policy engine.
Documentation
use crate::report::TextEdit;

#[derive(Debug, thiserror::Error)]
pub enum FixError {
    #[error("invalid line/column")]
    InvalidLineColumn,
    #[error("invalid utf-8 boundaries for edit")]
    InvalidUtf8Boundary,
    #[error("edit out of bounds")]
    OutOfBounds,
    #[error("overlapping edits")]
    Overlap,
}

pub fn line_col_to_byte_offset(text: &str, line_1: u32, col_1: u32) -> Result<usize, FixError> {
    if line_1 == 0 || col_1 == 0 {
        return Err(FixError::InvalidLineColumn);
    }

    let mut current_line: u32 = 1;
    let mut idx: usize = 0;
    while current_line < line_1 {
        let Some(pos) = text[idx..].find('\n') else {
            return Err(FixError::InvalidLineColumn);
        };
        idx += pos + 1;
        current_line += 1;
    }

    let line_end = text[idx..].find('\n').map_or(text.len(), |p| idx + p);
    let line_slice = &text[idx..line_end];

    let target_col = (col_1 - 1) as usize;
    let mut col: usize = 0;
    let mut byte_in_line: usize = 0;
    for (bidx, ch) in line_slice.char_indices() {
        if col == target_col {
            byte_in_line = bidx;
            break;
        }
        col += 1;
        byte_in_line = bidx + ch.len_utf8();
    }
    if col < target_col && target_col != col {
        return Err(FixError::InvalidLineColumn);
    }

    Ok(idx + byte_in_line)
}

pub fn apply_text_edits(text: &str, edits: &[TextEdit]) -> Result<String, FixError> {
    let mut out = text.to_string();
    let mut ordered = edits.to_vec();
    ordered.sort_by(|a, b| {
        b.byte_start
            .cmp(&a.byte_start)
            .then(b.byte_end.cmp(&a.byte_end))
    });

    let mut last_start: Option<u32> = None;
    for e in &ordered {
        if e.byte_start > e.byte_end {
            return Err(FixError::OutOfBounds);
        }
        if let Some(last) = last_start {
            if e.byte_end > last {
                return Err(FixError::Overlap);
            }
        }
        last_start = Some(e.byte_start);

        let start = e.byte_start as usize;
        let end = e.byte_end as usize;
        if end > out.len() {
            return Err(FixError::OutOfBounds);
        }
        if !out.is_char_boundary(start) || !out.is_char_boundary(end) {
            return Err(FixError::InvalidUtf8Boundary);
        }
        out.replace_range(start..end, &e.replacement);
    }

    Ok(out)
}

pub fn find_use_insertion_offset(text: &str) -> usize {
    let mut offset: usize = 0;
    for line in text.split_inclusive('\n') {
        let trimmed = line.trim_start();
        let is_inner_attr = trimmed.starts_with("#![");
        let is_inner_doc = trimmed.starts_with("//!") || trimmed.starts_with("/*!");
        if is_inner_attr || is_inner_doc {
            offset += line.len();
            continue;
        }
        break;
    }
    offset
}