Skip to main content

rscheck_cli/
fix.rs

1use crate::report::TextEdit;
2
3#[derive(Debug, thiserror::Error)]
4pub enum FixError {
5    #[error("invalid line/column")]
6    InvalidLineColumn,
7    #[error("invalid utf-8 boundaries for edit")]
8    InvalidUtf8Boundary,
9    #[error("edit out of bounds")]
10    OutOfBounds,
11    #[error("overlapping edits")]
12    Overlap,
13}
14
15pub fn line_col_to_byte_offset(text: &str, line_1: u32, col_1: u32) -> Result<usize, FixError> {
16    if line_1 == 0 || col_1 == 0 {
17        return Err(FixError::InvalidLineColumn);
18    }
19
20    let mut current_line: u32 = 1;
21    let mut idx: usize = 0;
22    while current_line < line_1 {
23        let Some(pos) = text[idx..].find('\n') else {
24            return Err(FixError::InvalidLineColumn);
25        };
26        idx += pos + 1;
27        current_line += 1;
28    }
29
30    let line_end = text[idx..].find('\n').map_or(text.len(), |p| idx + p);
31    let line_slice = &text[idx..line_end];
32
33    let target_col = (col_1 - 1) as usize;
34    let mut col: usize = 0;
35    let mut byte_in_line: usize = 0;
36    for (bidx, ch) in line_slice.char_indices() {
37        if col == target_col {
38            byte_in_line = bidx;
39            break;
40        }
41        col += 1;
42        byte_in_line = bidx + ch.len_utf8();
43    }
44    if col < target_col && target_col != col {
45        return Err(FixError::InvalidLineColumn);
46    }
47
48    Ok(idx + byte_in_line)
49}
50
51pub fn apply_text_edits(text: &str, edits: &[TextEdit]) -> Result<String, FixError> {
52    let mut out = text.to_string();
53    let mut ordered = edits.to_vec();
54    ordered.sort_by(|a, b| {
55        b.byte_start
56            .cmp(&a.byte_start)
57            .then(b.byte_end.cmp(&a.byte_end))
58    });
59
60    let mut last_start: Option<u32> = None;
61    for e in &ordered {
62        if e.byte_start > e.byte_end {
63            return Err(FixError::OutOfBounds);
64        }
65        if let Some(last) = last_start {
66            if e.byte_end > last {
67                return Err(FixError::Overlap);
68            }
69        }
70        last_start = Some(e.byte_start);
71
72        let start = e.byte_start as usize;
73        let end = e.byte_end as usize;
74        if end > out.len() {
75            return Err(FixError::OutOfBounds);
76        }
77        if !out.is_char_boundary(start) || !out.is_char_boundary(end) {
78            return Err(FixError::InvalidUtf8Boundary);
79        }
80        out.replace_range(start..end, &e.replacement);
81    }
82
83    Ok(out)
84}
85
86pub fn find_use_insertion_offset(text: &str) -> usize {
87    let mut offset: usize = 0;
88    for line in text.split_inclusive('\n') {
89        let trimmed = line.trim_start();
90        let is_inner_attr = trimmed.starts_with("#![");
91        let is_inner_doc = trimmed.starts_with("//!") || trimmed.starts_with("/*!");
92        if is_inner_attr || is_inner_doc {
93            offset += line.len();
94            continue;
95        }
96        break;
97    }
98    offset
99}