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 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 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 pub fn source_text(&self) -> Result<String> {
152 let contents = self.source_file.contents();
153
154 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}