1use std::{fmt, iter};
25
26use miette::{
27 Diagnostic as MietteDiagnostic, LabeledSpan, MietteError, MietteSpanContents, SourceCode,
28 SourceSpan, SpanContents,
29};
30
31use orrery::RenderError;
32use orrery_parser::{
33 Span,
34 error::{Diagnostic, ParseError},
35 source_map::SourceMap,
36};
37
38#[derive(Debug, thiserror::Error)]
43pub enum Error<'a> {
44 #[error("{0}")]
46 Parse(ParseError<'a>),
47 #[error("{0}")]
49 Render(#[from] RenderError),
50}
51
52impl<'a> From<ParseError<'a>> for Error<'a> {
53 fn from(err: ParseError<'a>) -> Self {
54 Self::Parse(err)
55 }
56}
57
58impl From<std::io::Error> for Error<'_> {
59 fn from(err: std::io::Error) -> Self {
60 Self::Render(RenderError::Io(err))
61 }
62}
63
64impl<'a> Error<'a> {
65 pub fn reportables(&'a self) -> Vec<Box<dyn MietteDiagnostic + 'a>> {
73 match self {
74 Error::Parse(parse_err) => {
75 let source_map = parse_err.source_map();
76 parse_err
77 .diagnostics()
78 .iter()
79 .map(|d| {
80 Box::new(DiagnosticAdapter::new(d, source_map)) as Box<dyn MietteDiagnostic>
81 })
82 .collect()
83 }
84 Error::Render(render_err) => {
85 vec![Box::new(RenderErrorAdapter(render_err)) as Box<dyn MietteDiagnostic>]
86 }
87 }
88 }
89}
90
91#[derive(Debug, Clone, Copy)]
100struct SourceMapSource<'a>(&'a SourceMap<'a>);
101
102impl SourceCode for SourceMapSource<'_> {
103 fn read_span<'s>(
104 &'s self,
105 span: &SourceSpan,
106 context_lines_before: usize,
107 context_lines_after: usize,
108 ) -> Result<Box<dyn SpanContents<'s> + 's>, MietteError> {
109 let offset = span.offset();
110
111 let file = self.0.lookup_file(offset).ok_or(MietteError::OutOfBounds)?;
112
113 let local_offset = offset - file.start_offset();
115 let local_span = SourceSpan::new(local_offset.into(), span.len());
116
117 let contents =
119 file.source()
120 .read_span(&local_span, context_lines_before, context_lines_after)?;
121
122 let local = contents.span();
126 let virtual_span =
127 SourceSpan::new((local.offset() + file.start_offset()).into(), local.len());
128
129 Ok(Box::new(MietteSpanContents::new_named(
131 file.name().to_owned(),
132 contents.data(),
133 virtual_span,
134 contents.line(),
135 contents.column(),
136 contents.line_count(),
137 )))
138 }
139}
140
141#[derive(thiserror::Error)]
147#[error("{}", .diag.message())]
148struct DiagnosticAdapter<'a> {
149 diag: &'a Diagnostic,
150 source_code: SourceMapSource<'a>,
151 imports: Vec<ImportDiagnostic<'a>>,
152}
153
154impl<'a> DiagnosticAdapter<'a> {
155 fn new(diag: &'a Diagnostic, source_map: &'a SourceMap<'a>) -> Self {
156 let imports = Self::build_imports(diag, source_map);
157 Self {
158 diag,
159 source_code: SourceMapSource(source_map),
160 imports,
161 }
162 }
163
164 fn build_imports(
171 diag: &Diagnostic,
172 source_map: &'a SourceMap<'a>,
173 ) -> Vec<ImportDiagnostic<'a>> {
174 diag.labels()
175 .iter()
176 .filter(|l| l.is_primary())
177 .filter_map(|l| {
178 source_map
179 .lookup_file_by_span(l.span())
180 .and_then(|f| f.first_imported_at())
181 })
182 .map(|import_span| ImportDiagnostic::new(import_span, source_map))
183 .collect()
184 }
185}
186
187impl fmt::Debug for DiagnosticAdapter<'_> {
188 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189 f.debug_struct("DiagnosticAdapter")
190 .field("diag", &self.diag)
191 .finish()
192 }
193}
194
195impl MietteDiagnostic for DiagnosticAdapter<'_> {
196 fn code<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
197 self.diag
198 .code()
199 .map(|c| Box::new(c) as Box<dyn fmt::Display>)
200 }
201
202 fn help<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
203 self.diag
204 .help()
205 .map(|h| Box::new(h) as Box<dyn fmt::Display>)
206 }
207
208 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
209 Some(&self.source_code as &dyn miette::SourceCode)
210 }
211
212 fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
213 let labels = self.diag.labels();
214 if labels.is_empty() {
215 return None;
216 }
217
218 Some(Box::new(labels.iter().map(|label| {
219 let span = span_to_miette(label.span());
220 let message = Some(label.message().to_string());
221 if label.is_primary() {
222 LabeledSpan::new_primary_with_span(message, span)
223 } else {
224 LabeledSpan::new_with_span(message, span)
225 }
226 })))
227 }
228
229 fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn MietteDiagnostic> + 'a>> {
230 if self.imports.is_empty() {
231 return None;
232 }
233 Some(Box::new(
234 self.imports
235 .iter()
236 .map(|import| import as &dyn MietteDiagnostic),
237 ))
238 }
239}
240
241#[derive(Debug, thiserror::Error)]
242struct ImportDiagnostic<'a> {
243 span: Span,
244 source_code: SourceMapSource<'a>,
245 next: Option<Box<ImportDiagnostic<'a>>>,
246}
247
248impl<'a> ImportDiagnostic<'a> {
249 fn new(span: Span, source_map: &'a SourceMap<'a>) -> Self {
250 let next = source_map
251 .lookup_file_by_span(span)
252 .and_then(|f| f.first_imported_at())
253 .map(|import_span| Box::new(ImportDiagnostic::new(import_span, source_map)));
254 ImportDiagnostic {
255 span,
256 source_code: SourceMapSource(source_map),
257 next,
258 }
259 }
260}
261
262impl fmt::Display for ImportDiagnostic<'_> {
263 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
264 write!(f, "import trace")
265 }
266}
267
268impl MietteDiagnostic for ImportDiagnostic<'_> {
269 fn source_code(&self) -> Option<&dyn SourceCode> {
270 Some(&self.source_code as &dyn SourceCode)
271 }
272
273 fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
274 let span = span_to_miette(self.span);
275 Some(Box::new(iter::once(LabeledSpan::new_with_span(
276 Some("imported here".to_string()),
277 span,
278 ))))
279 }
280
281 fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn MietteDiagnostic> + 'a>> {
282 let next = self.next.as_ref()?;
283 Some(Box::new(iter::once(next.as_ref() as &dyn MietteDiagnostic)))
284 }
285}
286
287#[derive(Debug, thiserror::Error)]
289#[error(transparent)]
290struct RenderErrorAdapter<'a>(&'a RenderError);
291
292impl MietteDiagnostic for RenderErrorAdapter<'_> {
293 fn code<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
294 let code = match &self.0 {
295 RenderError::Io(_) => "orrery::io",
296 RenderError::Graph(_) => "orrery::graph",
297 RenderError::Layout(_) => "orrery::layout",
298 RenderError::Export(_) => "orrery::export",
299 };
300 Some(Box::new(code))
301 }
302}
303
304fn span_to_miette(span: Span) -> SourceSpan {
306 SourceSpan::new(span.start().into(), span.len())
307}
308
309#[cfg(test)]
310mod tests {
311 use orrery_parser::error::ErrorCode;
312
313 use super::*;
314
315 fn single_file_source_map<'a>(name: &str, source: &'a str) -> SourceMap<'a> {
317 let mut sm = SourceMap::new();
318 sm.add_file(name, source, None);
319 sm
320 }
321
322 #[test]
323 fn test_single_diagnostic_with_source_map() {
324 let source = "hello";
325 let sm = single_file_source_map("test.orr", source);
326 let diag = Diagnostic::error("test error")
327 .with_code(ErrorCode::E300)
328 .with_label(Span::new(0..5), "here")
329 .with_help("try this");
330 let parse_err = ParseError::new(vec![diag], sm);
331 let err = Error::Parse(parse_err);
332
333 let reportables = err.reportables();
334 assert_eq!(reportables.len(), 1);
335 assert_eq!(reportables[0].to_string(), "test error");
336 }
337
338 #[test]
339 fn test_multiple_diagnostics() {
340 let source = "source code that is long enough for spans";
341 let sm = single_file_source_map("test.orr", source);
342 let diags = vec![
343 Diagnostic::error("first error")
344 .with_code(ErrorCode::E300)
345 .with_label(Span::new(0..5), "first"),
346 Diagnostic::error("second error")
347 .with_code(ErrorCode::E301)
348 .with_label(Span::new(10..15), "second")
349 .with_help("help for second"),
350 Diagnostic::error("third error").with_label(Span::new(20..25), "third"),
351 ];
352 let parse_err = ParseError::new(diags, sm);
353 let err = Error::Parse(parse_err);
354
355 let reportables = err.reportables();
356
357 assert_eq!(reportables.len(), 3);
358 assert_eq!(reportables[0].to_string(), "first error");
359 assert_eq!(reportables[1].to_string(), "second error");
360 assert_eq!(reportables[2].to_string(), "third error");
361 }
362
363 #[test]
364 fn test_single_render_error() {
365 let err = Error::Render(RenderError::Graph("graph error".to_string()));
366
367 let reportables = err.reportables();
368
369 assert_eq!(reportables.len(), 1);
370 assert_eq!(reportables[0].to_string(), "Graph error: graph error");
371 }
372
373 #[test]
374 fn test_all_labels_returned() {
375 let source = "some source code text";
376 let sm = single_file_source_map("test.orr", source);
377 let diag = Diagnostic::error("error with labels")
378 .with_label(Span::new(0..5), "primary label")
379 .with_secondary_label(Span::new(10..15), "secondary label");
380
381 let adapter = DiagnosticAdapter::new(&diag, &sm);
382
383 let labels: Vec<_> = adapter.labels().unwrap().collect();
384 assert_eq!(labels.len(), 2);
385 assert_eq!(labels[0].label(), Some("primary label"));
386 assert_eq!(labels[1].label(), Some("secondary label"));
387 }
388
389 #[test]
390 fn test_primary_flag_on_labels() {
391 let source = "some source code text";
392 let sm = single_file_source_map("test.orr", source);
393 let diag = Diagnostic::error("error with labels")
394 .with_label(Span::new(0..5), "primary")
395 .with_secondary_label(Span::new(10..15), "secondary");
396
397 let adapter = DiagnosticAdapter::new(&diag, &sm);
398
399 let labels: Vec<_> = adapter.labels().unwrap().collect();
400 assert_eq!(labels.len(), 2);
401 assert!(labels[0].primary());
402 assert!(!labels[1].primary());
403 }
404
405 #[test]
406 fn test_source_map_as_source_code() {
407 use miette::SourceCode;
408
409 let source = "line one\nline two\nline three";
410 let sm = single_file_source_map("main.orr", source);
411 let src = SourceMapSource(&sm);
412
413 let span = SourceSpan::new(9.into(), 8); let contents = src
416 .read_span(&span, 0, 0)
417 .expect("read_span should succeed");
418 let text = std::str::from_utf8(contents.data()).unwrap();
419 assert!(text.contains("line two"));
420 }
421
422 #[test]
423 fn test_source_map_multi_file() {
424 use miette::SourceCode;
425
426 let mut sm = SourceMap::new();
427 let _base_a = sm.add_file("a.orr", "aaaa", None);
428 let base_b = sm.add_file("b.orr", "bbbb", Some(Span::new(0..4)));
429 let src = SourceMapSource(&sm);
430
431 let span = SourceSpan::new(base_b.into(), 4);
433 let contents = src
434 .read_span(&span, 0, 0)
435 .expect("read_span should succeed");
436 let name = contents.name().expect("should have a file name");
437 assert_eq!(name, "b.orr");
438 }
439
440 #[test]
441 fn test_import_trace_root_file_no_related() {
442 let source = "diagram component;\nbox: Rectangle;";
443 let sm = single_file_source_map("main.orr", source);
444 let diag = Diagnostic::error("error in root").with_label(Span::new(0..7), "here");
445
446 let adapter = DiagnosticAdapter::new(&diag, &sm);
447
448 assert!(adapter.related().is_none());
450 }
451
452 #[test]
453 fn test_import_trace_imported_file_has_related() {
454 let mut sm = SourceMap::new();
455 let _base_root = sm.add_file("main.orr", "import \"lib\";\n", None);
457 let base_lib = sm.add_file("lib.orr", "library;\ntype Bad;", Some(Span::new(0..13)));
459
460 let diag = Diagnostic::error("error in lib")
462 .with_label(Span::new(base_lib..(base_lib + 7)), "here");
463
464 let adapter = DiagnosticAdapter::new(&diag, &sm);
465
466 let related: Vec<_> = adapter.related().unwrap().collect();
468 assert_eq!(related.len(), 1);
469 assert!(related[0].labels().is_some());
470 }
471}