necessist_core/
span.rs

1use crate::{__ToConsoleString as ToConsoleString, Backup, Rewriter, SourceFile};
2use anyhow::{Result, anyhow};
3use regex::Regex;
4use sha2::{Digest, Sha256};
5use std::{fs::OpenOptions, io::Write, path::PathBuf, rc::Rc, sync::LazyLock};
6
7#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
8pub struct Span {
9    pub source_file: SourceFile,
10    pub start: proc_macro2::LineColumn,
11    pub end: proc_macro2::LineColumn,
12}
13
14impl std::fmt::Display for Span {
15    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
16        // smoelius: `source_file.to_string()` gives the path relative to the project root.
17        write!(
18            f,
19            "{}",
20            self.to_string_with_path(&self.source_file.to_string())
21        )
22    }
23}
24
25impl rewriter::interface::Span for Span {
26    type LineColumn = proc_macro2::LineColumn;
27    fn line_column(line: usize, column: usize) -> Self::LineColumn {
28        proc_macro2::LineColumn { line, column }
29    }
30    fn start(&self) -> Self::LineColumn {
31        self.start
32    }
33    fn end(&self) -> Self::LineColumn {
34        self.end
35    }
36}
37
38impl ToConsoleString for Span {
39    fn to_console_string(&self) -> String {
40        self.to_string_with_path(&self.source_file.to_console_string())
41    }
42}
43
44static SPAN_RE: LazyLock<Regex> = LazyLock::new(|| {
45    #[allow(clippy::unwrap_used)]
46    Regex::new(r"^([^:]*):([^:]*):([^-]*)-([^:]*):(.*)$").unwrap()
47});
48
49impl Span {
50    #[must_use]
51    pub fn id(&self) -> String {
52        const ID_LEN: usize = 16;
53        let mut hasher = Sha256::new();
54        hasher.update(self.to_string());
55        let digest = hasher.finalize();
56        hex::encode(digest)[..ID_LEN].to_owned()
57    }
58
59    pub fn parse(root: &Rc<PathBuf>, s: &str) -> Result<Self> {
60        let (source_file, start_line, start_column, end_line, end_column) = SPAN_RE
61            .captures(s)
62            .map(|captures| {
63                assert_eq!(6, captures.len());
64                (
65                    captures[1].to_owned(),
66                    captures[2].to_owned(),
67                    captures[3].to_owned(),
68                    captures[4].to_owned(),
69                    captures[5].to_owned(),
70                )
71            })
72            .ok_or_else(|| anyhow!("Span has unexpected format"))?;
73        let start_line = start_line.parse::<usize>()?;
74        let start_column = start_column.parse::<usize>()?;
75        let end_line = end_line.parse::<usize>()?;
76        let end_column = end_column.parse::<usize>()?;
77        let source_file = SourceFile::new(root.clone(), root.join(source_file))?;
78        Ok(Self {
79            source_file,
80            start: proc_macro2::LineColumn {
81                line: start_line,
82                column: start_column - 1,
83            },
84            end: proc_macro2::LineColumn {
85                line: end_line,
86                column: end_column - 1,
87            },
88        })
89    }
90
91    #[must_use]
92    pub fn start(&self) -> proc_macro2::LineColumn {
93        self.start
94    }
95
96    #[must_use]
97    pub fn end(&self) -> proc_macro2::LineColumn {
98        self.end
99    }
100
101    fn to_string_with_path(&self, path: &str) -> String {
102        format!(
103            "{}:{}:{}-{}:{}",
104            path,
105            self.start.line,
106            self.start.column + 1,
107            self.end.line,
108            self.end.column + 1
109        )
110    }
111
112    #[must_use]
113    pub fn trim_start(&self) -> Self {
114        // smoelius: Ignoring errors is a hack.
115        let Ok(text) = self.source_text() else {
116            return self.clone();
117        };
118
119        let mut start = self.start;
120        for ch in text.chars() {
121            if ch.is_whitespace() {
122                if ch == '\n' {
123                    start.line += 1;
124                    start.column = 0;
125                } else {
126                    start.column += 1;
127                }
128            } else {
129                break;
130            }
131        }
132
133        self.with_start(start)
134    }
135
136    #[must_use]
137    pub fn with_start(&self, start: proc_macro2::LineColumn) -> Self {
138        Self {
139            source_file: self.source_file.clone(),
140            start,
141            end: self.end,
142        }
143    }
144
145    /// Returns the spanned text.
146    pub fn source_text(&self) -> Result<String> {
147        let contents = self.source_file.contents();
148
149        // smoelius: Creating a new `Rewriter` here is just as silly as it is in `attempt_removal`
150        // (see comment therein).
151        // smoelius: `Rewriter`s are now cheap to create because their underlying
152        // `OffsetCalculator`s are shared.
153        let (start, end) = self
154            .source_file
155            .offset_calculator()
156            .borrow_mut()
157            .offsets_from_span(self);
158
159        let bytes = &contents.as_bytes()[start..end];
160        let text = std::str::from_utf8(bytes)?;
161
162        Ok(text.to_owned())
163    }
164
165    pub fn remove(&self) -> Result<(String, Backup)> {
166        let backup = Backup::new(&*self.source_file)?;
167
168        let mut rewriter = Rewriter::with_offset_calculator(
169            self.source_file.contents(),
170            self.source_file.offset_calculator(),
171        );
172
173        let text = rewriter.rewrite(self, "");
174
175        let mut file = OpenOptions::new()
176            .truncate(true)
177            .write(true)
178            .open(&*self.source_file)?;
179        file.write_all(rewriter.contents().as_bytes())?;
180
181        Ok((text, backup))
182    }
183}
184
185#[allow(clippy::module_name_repetitions)]
186pub trait ToInternalSpan {
187    fn to_internal_span(&self, source_file: &SourceFile) -> Span;
188}
189
190impl ToInternalSpan for proc_macro2::Span {
191    fn to_internal_span(&self, source_file: &SourceFile) -> Span {
192        Span {
193            source_file: source_file.clone(),
194            start: self.start(),
195            end: self.end(),
196        }
197    }
198}