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