Skip to main content

cratestack_parser/
diagnostics_related.rs

1use std::ops::Range;
2use std::sync::Arc;
3
4use ariadne::{Color, Label, Report, ReportKind};
5use cratestack_core::schema::attribute_text::substitute_for_display;
6
7use crate::diagnostics::{SchemaError, char_span};
8
9/// A second location an error points at, in its own file: for example the
10/// earlier declaration of a name that two files both declare
11/// (docs/design/multi-file-schemas.md section 9).
12///
13/// Like the primary location of a `SchemaError`, `file` and `source_text`
14/// start out empty and are attached once, where the loader knows which file
15/// a global offset belongs to.
16#[derive(Clone)]
17pub struct RelatedSpan {
18    pub(crate) message: String,
19    pub(crate) span: Range<usize>,
20    pub(crate) line: usize,
21    pub(crate) file: Arc<str>,
22    pub(crate) source_text: Arc<str>,
23}
24
25impl RelatedSpan {
26    pub(crate) fn new(message: impl Into<String>, span: Range<usize>, line: usize) -> Self {
27        Self {
28            message: message.into(),
29            span,
30            line,
31            file: Arc::from(""),
32            source_text: Arc::from(""),
33        }
34    }
35
36    pub fn message(&self) -> &str {
37        &self.message
38    }
39
40    /// Byte span within [`Self::file`]'s text.
41    pub fn span(&self) -> Range<usize> {
42        self.span.clone()
43    }
44
45    pub fn line(&self) -> usize {
46        self.line
47    }
48
49    pub fn file(&self) -> &str {
50        &self.file
51    }
52}
53
54impl std::fmt::Debug for RelatedSpan {
55    /// Omits `source_text`, like `SchemaError`'s `Debug`.
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        f.debug_struct("RelatedSpan")
58            .field("message", &self.message)
59            .field("span", &self.span)
60            .field("line", &self.line)
61            .field("file", &self.file)
62            .finish()
63    }
64}
65
66impl SchemaError {
67    /// The multi-location form of [`Self::render`]: one label per location,
68    /// each in its own file's code frame. Only reached when `related` is
69    /// non-empty, so every other error renders exactly as before.
70    pub(crate) fn render_with_related(&self) -> String {
71        let mut files: Vec<(String, String)> = Vec::new();
72        let mut register = |file: &str, text: &str| {
73            if !files.iter().any(|(name, _)| name == file) {
74                files.push((file.to_owned(), substitute_for_display(text)));
75            }
76        };
77        register(&self.file, &self.source_text);
78        for related in &self.related {
79            register(&related.file, &related.source_text);
80        }
81        let primary = (
82            self.file.to_string(),
83            char_span(&self.source_text, &self.span),
84        );
85        let mut report = Report::build(ReportKind::Error, primary.clone())
86            .with_message(&self.message)
87            .with_label(
88                Label::new(primary)
89                    .with_message(&self.message)
90                    .with_color(Color::Red),
91            );
92        for related in &self.related {
93            let span = char_span(&related.source_text, &related.span);
94            report = report.with_label(
95                Label::new((related.file.to_string(), span))
96                    .with_message(&related.message)
97                    .with_color(Color::Blue),
98            );
99        }
100        let mut output = Vec::new();
101        report
102            .finish()
103            .write(ariadne::sources(files), &mut output)
104            .expect("diagnostic rendering should succeed");
105        String::from_utf8(output).expect("ariadne should emit utf-8")
106    }
107}