Skip to main content

svugy_parse/
diagnostic.rs

1//! Report what a source language rejects or ignores, and gate the model on it, the same way for every language the crate reads.
2
3use std::error::Error;
4use std::fmt;
5use std::marker::PhantomData;
6use std::ops::Range;
7
8use miette::{Diagnostic, LabeledSpan, Severity, SourceSpan};
9use svugy_model::model::Model;
10use svugy_model::problem::Suggestion;
11use svugy_model::validate::{ModelAdmission, ModelReport};
12
13/// Name a source language by the code of its errors and the type of its warnings, and import its text.
14pub trait SourceLanguage: fmt::Debug + Clone + PartialEq + Eq + Sized {
15    /// Spell the stable diagnostic code of the language's errors.
16    const ERROR_CODE: &'static str;
17    /// Type the warnings the language raises beside its errors.
18    type Warning: Diagnostic + Clone + PartialEq;
19
20    /// Import `source_text` into its model admission, keeping every error and warning on the way.
21    fn import(source_text: &str) -> Import<Self>;
22}
23
24/// Report a source error: text of the language `L` that cannot be read, left out of the model.
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct SourceError<L> {
27    pub message: String,
28    pub span: SourceSpan,
29    language: PhantomData<L>,
30}
31
32/// Stand for the warnings of a language that raises none of its own.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum NoWarning {}
35
36/// Record the errors and warnings a language's layers raise, handed from one layer to the next.
37#[derive(Debug)]
38pub(crate) struct Diagnostics<L: SourceLanguage> {
39    pub(crate) errors: Vec<SourceError<L>>,
40    pub(crate) warnings: Vec<L::Warning>,
41}
42
43/// Collect the import of a source file: the model's admission, every error and every warning of the language.
44#[derive(Debug)]
45pub struct Import<L: SourceLanguage> {
46    pub admission: ModelAdmission,
47    pub errors: Vec<SourceError<L>>,
48    pub warnings: Vec<L::Warning>,
49}
50
51/// Split an import into its model, withheld while any diagnostic is an error, its errors, its warnings and the model's report.
52#[derive(Debug)]
53pub struct ImportParts<L: SourceLanguage> {
54    pub model: Option<Model>,
55    pub errors: Vec<SourceError<L>>,
56    pub warnings: Vec<L::Warning>,
57    pub report: ModelReport,
58}
59
60/// Locate a labelled diagnostic by where its first label starts in the source.
61pub trait LabelledDiagnostic {
62    /// Return where the diagnostic's first label starts, or `None` when it has no label.
63    fn first_label_offset(&self) -> Option<usize>;
64}
65
66impl<L> SourceError<L> {
67    /// Create an error of `message` labelled at the byte range `span`.
68    pub(crate) fn new(message: String, span: Range<usize>) -> Self {
69        let span = SourceSpan::from(span);
70        Self {
71            message,
72            span,
73            language: PhantomData,
74        }
75    }
76}
77
78impl<L> fmt::Display for SourceError<L> {
79    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
80        formatter.write_str(&self.message)
81    }
82}
83
84impl<L: fmt::Debug> Error for SourceError<L> {}
85
86impl<L: SourceLanguage> Diagnostic for SourceError<L> {
87    fn code<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
88        let code = Box::new(L::ERROR_CODE);
89        Some(code)
90    }
91
92    fn severity(&self) -> Option<Severity> {
93        Some(Severity::Error)
94    }
95
96    fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
97        let label_text = Some("here".to_owned());
98        let label = LabeledSpan::new_with_span(label_text, self.span);
99        let labels = Box::new(std::iter::once(label));
100        Some(labels)
101    }
102}
103
104impl fmt::Display for NoWarning {
105    fn fmt(&self, _formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
106        match *self {}
107    }
108}
109
110impl Error for NoWarning {}
111
112impl Diagnostic for NoWarning {}
113
114impl<L: SourceLanguage> Default for Diagnostics<L> {
115    fn default() -> Self {
116        Self::from_parts(Vec::new(), Vec::new())
117    }
118}
119
120impl<L: SourceLanguage> Diagnostics<L> {
121    /// Create diagnostics carrying on the `errors` and `warnings` an earlier layer recorded.
122    pub(crate) fn from_parts(errors: Vec<SourceError<L>>, warnings: Vec<L::Warning>) -> Self {
123        Self { errors, warnings }
124    }
125
126    /// Record an error of `message` at `span`.
127    pub(crate) fn record_error(
128        &mut self,
129        message: impl Into<String>,
130        span: impl Into<Range<usize>>,
131    ) {
132        let message = message.into();
133        let byte_range = span.into();
134        let error = SourceError::new(message, byte_range);
135        self.errors.push(error);
136    }
137
138    /// Record an unknown name after `prefix` at `span`, suggesting the closest of `candidates`.
139    pub(crate) fn record_unknown<T: fmt::Display>(
140        &mut self,
141        prefix: &str,
142        unknown: &str,
143        candidates: impl IntoIterator<Item = T>,
144        span: impl Into<Range<usize>>,
145    ) {
146        let suggestion = Suggestion::closest_to(unknown, candidates);
147        let message = match suggestion {
148            Some(suggestion) => format!("{prefix} `{unknown}`; {suggestion}"),
149            None => format!("{prefix} `{unknown}`"),
150        };
151        self.record_error(message, span);
152    }
153
154    /// Split the diagnostics into their errors and their warnings, each sorted by where it starts in the source, ties kept in recording order.
155    pub(crate) fn into_sorted_parts(self) -> (Vec<SourceError<L>>, Vec<L::Warning>) {
156        let Self {
157            mut errors,
158            mut warnings,
159        } = self;
160        errors.sort_by_key(|error| error.span.offset());
161        warnings.sort_by_key(|warning| {
162            let first_label_offset = warning.first_label_offset();
163            first_label_offset.unwrap_or(usize::MAX)
164        });
165        (errors, warnings)
166    }
167}
168
169impl<L: SourceLanguage> Import<L> {
170    /// List every diagnostic in source order: errors, warnings and model problems alike, unlabelled ones last.
171    pub fn diagnostics(&self) -> Vec<&dyn Diagnostic> {
172        let errors = self.errors.iter().map(|error| error as &dyn Diagnostic);
173        let warnings = self
174            .warnings
175            .iter()
176            .map(|warning| warning as &dyn Diagnostic);
177        let problems = self
178            .admission
179            .report
180            .problems()
181            .iter()
182            .map(|problem| problem as &dyn Diagnostic);
183        let mut diagnostics: Vec<&dyn Diagnostic> =
184            errors.chain(warnings).chain(problems).collect();
185        diagnostics.sort_by_key(|diagnostic| {
186            let first_label_offset = diagnostic.first_label_offset();
187            first_label_offset.unwrap_or(usize::MAX)
188        });
189        diagnostics
190    }
191
192    /// Return the admitted model only when no diagnostic is an error, so a diagram the author has yet to fix is never drawn.
193    pub fn admitted_model(&self) -> Option<&Model> {
194        let has_errors = self.has_errors();
195        if has_errors {
196            return None;
197        }
198        self.admission.model.as_ref()
199    }
200
201    /// Split the import into its parts, its model withheld as `admitted_model` withholds it.
202    pub fn into_admitted_parts(self) -> ImportParts<L> {
203        let has_errors = self.has_errors();
204        let Self {
205            admission,
206            errors,
207            warnings,
208        } = self;
209        let ModelAdmission { model, report } = admission;
210        let admitted_model = if has_errors { None } else { model };
211        ImportParts {
212            model: admitted_model,
213            errors,
214            warnings,
215            report,
216        }
217    }
218
219    /// Tell whether any diagnostic is an error: an error of the language or of the model's checks.
220    fn has_errors(&self) -> bool {
221        let has_language_errors = !self.errors.is_empty();
222        let has_model_errors = self.admission.report.has_errors();
223        has_language_errors || has_model_errors
224    }
225}
226
227impl<D: Diagnostic + ?Sized> LabelledDiagnostic for D {
228    fn first_label_offset(&self) -> Option<usize> {
229        let mut labels = self.labels().into_iter().flatten();
230        let first_label = labels.next();
231        first_label.map(|label| label.offset())
232    }
233}