wdl_grammar/
diagnostic.rs

1//! Definition of diagnostics displayed to users.
2
3use std::cmp::Ordering;
4use std::fmt;
5
6use rowan::TextRange;
7
8/// Represents a span of source.
9#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
10pub struct Span {
11    /// The start of the span.
12    start: usize,
13    /// The end of the span.
14    end: usize,
15}
16
17impl Span {
18    /// Creates a new span from the given start and length.
19    pub const fn new(start: usize, len: usize) -> Self {
20        Self {
21            start,
22            end: start + len,
23        }
24    }
25
26    /// Gets the start of the span.
27    pub fn start(&self) -> usize {
28        self.start
29    }
30
31    /// Gets the noninclusive end of the span.
32    pub fn end(&self) -> usize {
33        self.end
34    }
35
36    /// Gets the length of the span.
37    pub fn len(&self) -> usize {
38        self.end - self.start
39    }
40
41    /// Determines if the span is empty.
42    pub fn is_empty(&self) -> bool {
43        self.start == self.end
44    }
45
46    /// Determines if the span contains the given offset.
47    pub fn contains(&self, offset: usize) -> bool {
48        offset >= self.start && offset < self.end
49    }
50
51    /// Calculates an intersection of two spans, if one exists.
52    ///
53    /// If spans are adjacent, a zero-length span is returned.
54    ///
55    /// Returns `None` if the two spans are disjoint.
56    ///
57    /// # Examples
58    ///
59    /// ```rust
60    /// # use wdl_grammar::Span;
61    /// assert_eq!(
62    ///     Span::intersect(Span::new(0, 10), Span::new(5, 10)),
63    ///     Some(Span::new(5, 5)),
64    /// );
65    /// ```
66    #[inline]
67    pub fn intersect(self, other: Self) -> Option<Self> {
68        let start = self.start.max(other.start);
69        let end = self.end.min(other.end);
70        if end < start {
71            return None;
72        }
73
74        Some(Self { start, end })
75    }
76}
77
78impl fmt::Display for Span {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        write!(f, "{start}..{end}", start = self.start, end = self.end)
81    }
82}
83
84impl From<logos::Span> for Span {
85    fn from(value: logos::Span) -> Self {
86        Self::new(value.start, value.len())
87    }
88}
89
90impl From<TextRange> for Span {
91    fn from(value: TextRange) -> Self {
92        let start = usize::from(value.start());
93        Self::new(start, usize::from(value.end()) - start)
94    }
95}
96
97/// Represents the severity of a diagnostic.
98#[derive(
99    Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, serde::Deserialize, serde::Serialize,
100)]
101pub enum Severity {
102    /// The diagnostic is displayed as an error.
103    Error,
104    /// The diagnostic is displayed as a warning.
105    Warning,
106    /// The diagnostic is displayed as a note.
107    Note,
108}
109
110impl Severity {
111    /// Returns `true` if the severity is [`Error`].
112    ///
113    /// [`Error`]: Severity::Error
114    #[must_use]
115    pub fn is_error(&self) -> bool {
116        matches!(self, Self::Error)
117    }
118
119    /// Returns `true` if the severity is [`Warning`].
120    ///
121    /// [`Warning`]: Severity::Warning
122    #[must_use]
123    pub fn is_warning(&self) -> bool {
124        matches!(self, Self::Warning)
125    }
126
127    /// Returns `true` if the severity is [`Note`].
128    ///
129    /// [`Note`]: Severity::Note
130    #[must_use]
131    pub fn is_note(&self) -> bool {
132        matches!(self, Self::Note)
133    }
134}
135
136/// Represents a diagnostic to display to the user.
137#[derive(Debug, Clone, Eq, PartialEq)]
138pub struct Diagnostic {
139    /// The optional rule associated with the diagnostic.
140    rule: Option<String>,
141    /// The default severity of the diagnostic.
142    severity: Severity,
143    /// The diagnostic message.
144    message: String,
145    /// The optional fix suggestion for the diagnostic.
146    fix: Option<String>,
147    /// The labels for the diagnostic.
148    ///
149    /// The first label in the collection is considered the primary label.
150    labels: Vec<Label>,
151}
152
153impl Ord for Diagnostic {
154    fn cmp(&self, other: &Self) -> Ordering {
155        match self.labels.cmp(&other.labels) {
156            Ordering::Equal => {}
157            ord => return ord,
158        }
159
160        match self.rule.cmp(&other.rule) {
161            Ordering::Equal => {}
162            ord => return ord,
163        }
164
165        match self.severity.cmp(&other.severity) {
166            Ordering::Equal => {}
167            ord => return ord,
168        }
169
170        match self.message.cmp(&other.message) {
171            Ordering::Equal => {}
172            ord => return ord,
173        }
174
175        self.fix.cmp(&other.fix)
176    }
177}
178
179impl PartialOrd for Diagnostic {
180    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
181        Some(self.cmp(other))
182    }
183}
184
185impl Diagnostic {
186    /// Creates a new diagnostic error with the given message.
187    pub fn error(message: impl Into<String>) -> Self {
188        Self {
189            rule: None,
190            severity: Severity::Error,
191            message: message.into(),
192            fix: None,
193            labels: Default::default(),
194        }
195    }
196
197    /// Creates a new diagnostic warning with the given message.
198    pub fn warning(message: impl Into<String>) -> Self {
199        Self {
200            rule: None,
201            severity: Severity::Warning,
202            message: message.into(),
203            fix: None,
204            labels: Default::default(),
205        }
206    }
207
208    /// Creates a new diagnostic node with the given message.
209    pub fn note(message: impl Into<String>) -> Self {
210        Self {
211            rule: None,
212            severity: Severity::Note,
213            message: message.into(),
214            fix: None,
215            labels: Default::default(),
216        }
217    }
218
219    /// Sets the rule for the diagnostic.
220    pub fn with_rule(mut self, rule: impl Into<String>) -> Self {
221        self.rule = Some(rule.into());
222        self
223    }
224
225    /// Sets the fix message for the diagnostic.
226    pub fn with_fix(mut self, fix: impl Into<String>) -> Self {
227        self.fix = Some(fix.into());
228        self
229    }
230
231    /// Adds a highlight to the diagnostic.
232    ///
233    /// This is equivalent to adding a label with an empty message.
234    ///
235    /// The span for the highlight is expected to be for the same file as the
236    /// diagnostic.
237    pub fn with_highlight(mut self, span: impl Into<Span>) -> Self {
238        self.labels.push(Label::new(String::new(), span.into()));
239        self
240    }
241
242    /// Adds a label to the diagnostic.
243    ///
244    /// The first label added is considered the primary label.
245    ///
246    /// The span for the label is expected to be for the same file as the
247    /// diagnostic.
248    pub fn with_label(mut self, message: impl Into<String>, span: impl Into<Span>) -> Self {
249        self.labels.push(Label::new(message, span.into()));
250        self
251    }
252
253    /// Sets the severity of the diagnostic.
254    pub fn with_severity(mut self, severity: Severity) -> Self {
255        self.severity = severity;
256        self
257    }
258
259    /// Gets the optional rule associated with the diagnostic.
260    pub fn rule(&self) -> Option<&str> {
261        self.rule.as_deref()
262    }
263
264    /// Gets the default severity level of the diagnostic.
265    ///
266    /// The severity level may be upgraded to error depending on configuration.
267    pub fn severity(&self) -> Severity {
268        self.severity
269    }
270
271    /// Gets the message of the diagnostic.
272    pub fn message(&self) -> &str {
273        &self.message
274    }
275
276    /// Gets the optional fix of the diagnostic.
277    pub fn fix(&self) -> Option<&str> {
278        self.fix.as_deref()
279    }
280
281    /// Gets the labels of the diagnostic.
282    pub fn labels(&self) -> impl Iterator<Item = &Label> {
283        self.labels.iter()
284    }
285
286    /// Gets the mutable labels of the diagnostic.
287    pub fn labels_mut(&mut self) -> impl Iterator<Item = &mut Label> {
288        self.labels.iter_mut()
289    }
290
291    /// Converts this diagnostic to a `codespan` [Diagnostic].
292    ///
293    /// The provided file identifier is used for the diagnostic.
294    ///
295    /// [Diagnostic]: codespan_reporting::diagnostic::Diagnostic
296    #[cfg(feature = "codespan")]
297    pub fn to_codespan<FileId: Copy>(
298        &self,
299        file_id: FileId,
300    ) -> codespan_reporting::diagnostic::Diagnostic<FileId> {
301        use codespan_reporting::diagnostic as codespan;
302
303        let mut diagnostic: codespan::Diagnostic<FileId> = match self.severity {
304            Severity::Error => codespan::Diagnostic::error(),
305            Severity::Warning => codespan::Diagnostic::warning(),
306            Severity::Note => codespan::Diagnostic::note(),
307        };
308
309        if let Some(rule) = &self.rule {
310            diagnostic.code = Some(rule.clone());
311        }
312
313        diagnostic.message.clone_from(&self.message);
314
315        if let Some(fix) = &self.fix {
316            diagnostic.notes.push(format!("fix: {fix}"));
317        }
318
319        if self.labels.is_empty() {
320            // Codespan will treat this as a label at the end of the file
321            // We add this so that every diagnostic has at least one label with the file
322            // printed.
323            diagnostic.labels.push(codespan::Label::new(
324                codespan::LabelStyle::Primary,
325                file_id,
326                usize::MAX - 1..usize::MAX,
327            ))
328        } else {
329            for (i, label) in self.labels.iter().enumerate() {
330                diagnostic.labels.push(
331                    codespan::Label::new(
332                        if i == 0 {
333                            codespan::LabelStyle::Primary
334                        } else {
335                            codespan::LabelStyle::Secondary
336                        },
337                        file_id,
338                        label.span.start..label.span.end,
339                    )
340                    .with_message(&label.message),
341                );
342            }
343        }
344
345        diagnostic
346    }
347}
348
349/// Represents a label that annotates the source code.
350#[derive(Debug, Clone, Eq, PartialEq)]
351pub struct Label {
352    /// The optional message of the label (may be empty).
353    message: String,
354    /// The span of the label.
355    span: Span,
356}
357
358impl Ord for Label {
359    fn cmp(&self, other: &Self) -> Ordering {
360        match self.span.cmp(&other.span) {
361            Ordering::Equal => {}
362            ord => return ord,
363        }
364
365        self.message.cmp(&other.message)
366    }
367}
368
369impl PartialOrd for Label {
370    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
371        Some(self.cmp(other))
372    }
373}
374
375impl Label {
376    /// Creates a new label with the given message and span.
377    pub fn new(message: impl Into<String>, span: impl Into<Span>) -> Self {
378        Self {
379            message: message.into(),
380            span: span.into(),
381        }
382    }
383
384    /// Gets the message of the label.
385    pub fn message(&self) -> &str {
386        &self.message
387    }
388
389    /// Gets the span of the label.
390    pub fn span(&self) -> Span {
391        self.span
392    }
393
394    /// Sets the span of the label.
395    pub fn set_span(&mut self, span: impl Into<Span>) {
396        self.span = span.into();
397    }
398}