1#![doc = include_str!("doc-snapshots/multiple")]
31#![allow(rustdoc::redundant_explicit_links)] #![cfg_attr(do_doc_cfg, feature(doc_cfg))]
35
36use std::{fmt, sync::Arc};
37
38use miette::{
39 Diagnostic, LabeledSpan, MietteSpanContents, SourceCode, SourceOffset, SourceSpan, SpanContents,
40};
41
42#[derive(Debug, Clone)]
46pub struct Error {
47 source_code: MaybeNamedSource<Arc<str>>,
48 syn_error: syn::Error,
49}
50
51impl Error {
52 pub fn new(syn_error: syn::Error, source_code: impl Into<Arc<str>>) -> Self {
67 Self {
68 source_code: MaybeNamedSource {
69 file_name: None,
70 source_code: source_code.into(),
71 },
72 syn_error,
73 }
74 }
75 pub fn new_named(
90 syn_error: syn::Error,
91 source_code: impl Into<Arc<str>>,
92 file_name: impl fmt::Display,
93 ) -> Self {
94 Self {
95 source_code: MaybeNamedSource {
96 file_name: Some(file_name.to_string()),
97 source_code: source_code.into(),
98 },
99 syn_error,
100 }
101 }
102 pub fn source_code(&self) -> &Arc<str> {
104 &self.source_code.source_code
105 }
106 pub fn get(&mut self) -> &syn::Error {
108 &self.syn_error
109 }
110 pub fn get_mut(&mut self) -> &mut syn::Error {
112 &mut self.syn_error
113 }
114 pub fn into_inner(self) -> syn::Error {
116 self.into()
117 }
118 #[cfg_attr(do_doc_cfg, doc(cfg(feature = "render")))]
122 #[cfg(feature = "render")]
123 pub fn render(&self) -> String {
124 use miette::{GraphicalReportHandler, GraphicalTheme};
125 let mut s = String::new();
126 GraphicalReportHandler::new_themed(GraphicalTheme::unicode_nocolor())
127 .with_width(80)
128 .render_report(&mut s, self)
129 .unwrap();
130 s
131 }
132}
133
134impl From<Error> for syn::Error {
135 fn from(value: Error) -> Self {
136 value.syn_error
137 }
138}
139
140impl Diagnostic for Error {
141 fn source_code(&self) -> Option<&dyn SourceCode> {
142 Some(&self.source_code as &dyn SourceCode)
143 }
144
145 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
146 Some(Box::new((&self.syn_error).into_iter().map(
147 move |syn_error| {
148 let span = syn_error.span();
149 let span_start = span.start();
150 let span_end = span.end();
151 let start_offset = SourceOffset::from_location(
152 &self.source_code.source_code,
153 span_start.line,
154 span_start.column + 1,
155 );
156 let end_offset = SourceOffset::from_location(
157 &self.source_code.source_code,
158 span_end.line,
159 span_end.column + 1,
160 );
161 let length = end_offset.offset() - start_offset.offset();
162 LabeledSpan::new_with_span(
163 Some(syn_error.to_string()),
164 SourceSpan::new(start_offset, length),
165 )
166 },
167 )))
168 }
169}
170
171impl std::error::Error for Error {}
172
173impl fmt::Display for Error {
174 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175 self.syn_error.fmt(f)
176 }
177}
178
179#[derive(Debug, Clone, PartialEq, Eq)]
180struct MaybeNamedSource<T> {
181 file_name: Option<String>,
182 source_code: T,
183}
184
185impl<T> SourceCode for MaybeNamedSource<T>
186where
187 T: SourceCode,
188{
189 fn read_span<'a>(
190 &'a self,
191 span: &SourceSpan,
192 context_lines_before: usize,
193 context_lines_after: usize,
194 ) -> Result<Box<dyn SpanContents<'a> + 'a>, miette::MietteError> {
195 let contents =
196 self.source_code
197 .read_span(span, context_lines_before, context_lines_after)?;
198 let data = contents.data();
199 let span = *contents.span();
200 let line = contents.line();
201 let column = contents.column();
202 let line_count = contents.line_count();
203 match self.file_name.clone() {
204 Some(name) => Ok(Box::new(MietteSpanContents::new_named(
205 name, data, span, line, column, line_count,
206 ))),
207 None => Ok(Box::new(MietteSpanContents::new(
208 data, span, line, column, line_count,
209 ))),
210 }
211 }
212}
213
214#[cfg(all(test, feature = "render"))]
215mod tests {
216 use std::{collections::HashMap, fs, path::Path};
217
218 use proc_macro2::{Ident, Span};
219 use syn::{parse::Parse, parse::ParseStream, DeriveInput};
220
221 use super::*;
222
223 enum Behaviour {
224 IncludeSource,
225 IncludeFilename,
226 }
227
228 #[test]
229 fn basic_parse() {
230 insta::assert_snapshot!(test_parse::<DeriveInput>(
231 "struct Foo",
232 Behaviour::IncludeSource
233 ));
234 insta::assert_snapshot!(test_parse::<DeriveInput>(
235 "struct Foo",
236 Behaviour::IncludeFilename
237 ));
238 }
239
240 #[test]
241 fn call_site() {
242 insta::assert_snapshot!(Error::new(
243 syn::Error::new(Span::call_site(), "the whole thing is fucked"),
244 "this is the source code"
245 )
246 .render());
247 insta::assert_snapshot!(Error::new(
248 syn::Error::new(Span::call_site(), "the whole thing is fucked"),
249 "this is the source code\nand it's fucked on multiple\nlines"
250 )
251 .render());
252 }
253
254 #[test]
255 fn combined() {
256 insta::assert_snapshot!(test_parse::<UniqueDeriveInputs>(
257 "struct Foo;\nenum Bar {}\nunion Foo {}",
258 Behaviour::IncludeSource
259 )
260 .and_doc_snapshot("multiple"));
261 }
262
263 struct UniqueDeriveInputs {}
264
265 impl Parse for UniqueDeriveInputs {
266 fn parse(input: ParseStream) -> syn::Result<Self> {
267 let mut seen = HashMap::<Ident, DeriveInput>::new();
268 while !input.is_empty() {
269 let parsed = input.parse::<DeriveInput>()?;
270 match seen.remove(&parsed.ident) {
271 Some(duplicate) => {
272 let mut root = syn::Error::new(
273 parsed.ident.span(),
274 format!("duplicate definition of `{}`", parsed.ident),
275 );
276 root.combine(syn::Error::new(
277 duplicate.ident.span(),
278 "initial definition here",
279 ));
280 return Err(root);
281 }
282 None => {
283 seen.insert(parsed.ident.clone(), parsed);
284 }
285 }
286 }
287 Ok(Self {})
288 }
289 }
290
291 #[track_caller]
292 fn test_parse<T: Parse>(source_code: &str, behaviour: Behaviour) -> String {
293 let Err(error) = syn::parse_str::<T>(source_code) else {
294 panic!("parsing succeeded where it was expected to fail")
295 };
296 let error = match behaviour {
297 Behaviour::IncludeSource => Error::new(error, source_code),
298 Behaviour::IncludeFilename => Error::new_named(error, source_code, "/path/to/file"),
299 };
300 error.render()
301 }
302
303 trait AndDocSnapshot: AsRef<str> {
304 #[track_caller]
305 fn and_doc_snapshot(&self, name: &str) -> &Self {
306 let path = Path::new(env!("CARGO_MANIFEST_DIR"))
307 .join("src/doc-snapshots")
308 .join(name);
309 match fs::read_to_string(&path)
310 .map(|it| it == self.as_ref())
311 .unwrap_or(false)
312 {
313 true => {}
314 false => match fs::write(path, self.as_ref()) {
315 Ok(()) => panic!(
316 "doc snapshot {} was out of date - a new one has been written",
317 name
318 ),
319 Err(e) => panic!(
320 "doc snapshot {} was out of date, and a new one couldn't be written: {}",
321 name, e
322 ),
323 },
324 }
325
326 self
327 }
328 }
329 impl<T> AndDocSnapshot for T where T: AsRef<str> {}
330}