Skip to main content

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 source_file(&self) -> SourceFile {
93        self.source_file.clone()
94    }
95
96    #[must_use]
97    pub fn start(&self) -> proc_macro2::LineColumn {
98        self.start
99    }
100
101    #[must_use]
102    pub fn end(&self) -> proc_macro2::LineColumn {
103        self.end
104    }
105
106    fn to_string_with_path(&self, path: &str) -> String {
107        format!(
108            "{}:{}:{}-{}:{}",
109            path,
110            self.start.line,
111            self.start.column + 1,
112            self.end.line,
113            self.end.column + 1
114        )
115    }
116
117    #[must_use]
118    pub fn trim_start(&self) -> Self {
119        // smoelius: Ignoring errors is a hack.
120        let Ok(text) = self.source_text() else {
121            return self.clone();
122        };
123
124        let mut start = self.start;
125        for ch in text.chars() {
126            if ch.is_whitespace() {
127                if ch == '\n' {
128                    start.line += 1;
129                    start.column = 0;
130                } else {
131                    start.column += 1;
132                }
133            } else {
134                break;
135            }
136        }
137
138        self.with_start(start)
139    }
140
141    #[must_use]
142    pub fn with_start(&self, start: proc_macro2::LineColumn) -> Self {
143        Self {
144            source_file: self.source_file.clone(),
145            start,
146            end: self.end,
147        }
148    }
149
150    /// Returns the spanned text.
151    pub fn source_text(&self) -> Result<String> {
152        let contents = self.source_file.contents();
153
154        // smoelius: Creating a new `Rewriter` here is just as silly as it is in `attempt_removal`
155        // (see comment therein).
156        // smoelius: `Rewriter`s are now cheap to create because their underlying
157        // `OffsetCalculator`s are shared.
158        let (start, end) = self
159            .source_file
160            .offset_calculator()
161            .borrow_mut()
162            .offsets_from_span(self);
163
164        let bytes = &contents.as_bytes()[start..end];
165        let text = std::str::from_utf8(bytes)?;
166
167        Ok(text.to_owned())
168    }
169
170    pub fn remove(&self) -> Result<(String, Backup)> {
171        let backup = Backup::new(&*self.source_file)?;
172
173        let mut rewriter = Rewriter::with_offset_calculator(
174            self.source_file.contents(),
175            self.source_file.offset_calculator(),
176        );
177
178        let text = rewriter.rewrite(self, "");
179
180        let mut file = OpenOptions::new()
181            .truncate(true)
182            .write(true)
183            .open(&*self.source_file)?;
184        file.write_all(rewriter.contents().as_bytes())?;
185
186        Ok((text, backup))
187    }
188}
189
190#[allow(clippy::module_name_repetitions)]
191pub trait ToInternalSpan {
192    fn to_internal_span(&self, source_file: &SourceFile) -> Span;
193}
194
195impl ToInternalSpan for proc_macro2::Span {
196    fn to_internal_span(&self, source_file: &SourceFile) -> Span {
197        Span {
198            source_file: source_file.clone(),
199            start: self.start(),
200            end: self.end(),
201        }
202    }
203}