rtlola_reporting/
reporting.rs

1//! This module contains helper to report messages (warnings/errors)
2use std::error::Error;
3use std::fmt::Debug;
4use std::iter::{self, FromIterator};
5use std::ops::Range;
6use std::path::Path;
7use std::sync::RwLock;
8
9use codespan_reporting::diagnostic::{Diagnostic as RawDiagnostic, Label, Severity};
10use codespan_reporting::files::SimpleFile;
11use codespan_reporting::term;
12use codespan_reporting::term::termcolor::{ColorChoice, StandardStream, WriteColor};
13use codespan_reporting::term::Config;
14use serde::{Deserialize, Serialize};
15
16/// Represents a location in the source
17#[derive(
18    Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, Default, PartialOrd, Ord,
19)]
20pub enum Span {
21    /// Direct code reference through byte offset
22    Direct {
23        /// The start of the span in characters absolute to the beginning of the specification.
24        start: usize,
25        /// The end of the span in characters absolute to the beginning of the specification.
26        end: usize,
27    },
28    /// Indirect code reference created through ast refactoring
29    Indirect {
30        /// The start of the span in characters absolute to the beginning of the specification.
31        start: usize,
32        /// The end of the span in characters absolute to the beginning of the specification.
33        end: usize,
34    },
35    /// An unknown code reference
36    #[default]
37    Unknown,
38}
39impl<'a> From<pest::Span<'a>> for Span {
40    fn from(span: pest::Span<'a>) -> Self {
41        Span::Direct {
42            start: span.start(),
43            end: span.end(),
44        }
45    }
46}
47
48impl From<Span> for Range<usize> {
49    fn from(s: Span) -> Range<usize> {
50        let (s, e) = s.get_bounds();
51        Range { start: s, end: e }
52    }
53}
54
55impl Span {
56    /// Return true if the span is indirect.
57    pub fn is_indirect(&self) -> bool {
58        match self {
59            Span::Direct { .. } => false,
60            Span::Indirect { .. } => true,
61            Span::Unknown => false,
62        }
63    }
64
65    /// Returns true if the span is unknown.
66    pub fn is_unknown(&self) -> bool {
67        match self {
68            Span::Direct { .. } => false,
69            Span::Indirect { .. } => false,
70            Span::Unknown => true,
71        }
72    }
73
74    /// Returns the start and end position of the span.
75    /// Note: If the span is unknown returns (usize::min, usize::max)
76    pub fn get_bounds(&self) -> (usize, usize) {
77        match self {
78            Span::Indirect { start, end } | Span::Direct { start, end } => (*start, *end),
79            Span::Unknown => (usize::MIN, usize::MAX),
80        }
81    }
82
83    /// Combines two spans to their union
84    pub fn union(&self, other: &Self) -> Self {
85        if self.is_unknown() {
86            return *other;
87        }
88        if other.is_unknown() {
89            return *self;
90        }
91        let (start1, end1) = self.get_bounds();
92        let (start2, end2) = other.get_bounds();
93        if self.is_indirect() || other.is_indirect() {
94            Span::Indirect {
95                start: start1.min(start2),
96                end: end1.max(end2),
97            }
98        } else {
99            Span::Direct {
100                start: start1.min(start2),
101                end: end1.max(end2),
102            }
103        }
104    }
105
106    /// Converts a direct span to an indirect one.
107    pub fn to_indirect(self) -> Self {
108        match self {
109            Span::Direct { start, end } => Span::Indirect { start, end },
110            Span::Indirect { .. } => self,
111            Span::Unknown => self,
112        }
113    }
114}
115
116/// A handler is responsible for emitting warnings and errors
117pub struct Handler<'a> {
118    /// The number of errors that have already occurred
119    error_count: RwLock<usize>,
120    /// The number of warnings that have already occurred
121    warning_count: RwLock<usize>,
122    /// The input file the handler refers to given by a path and its content
123    input: SimpleFile<&'a str, &'a str>,
124    /// The output the handler is emitting to
125    output: RwLock<Box<dyn WriteColor>>,
126    /// The config for the error formatting
127    config: Config,
128}
129impl Debug for Handler<'_> {
130    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
131        f.debug_struct("Handler")
132            .field("error_count", &self.error_count)
133            .field("warning_count", &self.warning_count)
134            .field("input", &self.input)
135            .field("config", &self.config)
136            .finish()
137    }
138}
139
140impl<'a> Handler<'a> {
141    /// Creates a new Handler
142    /// `input_path` refers to the path of the input file
143    /// `input_content` refers to the content of the input file
144    pub fn new(input_path: &'a Path, input_content: &'a str) -> Self {
145        Handler {
146            error_count: RwLock::new(0),
147            warning_count: RwLock::new(0),
148            input: SimpleFile::new(input_path.to_str().unwrap_or("unknown file"), input_content),
149            output: RwLock::new(Box::new(StandardStream::stderr(ColorChoice::Always))),
150            config: Config::default(),
151        }
152    }
153
154    /// Creates a new handler without a path.
155    pub fn without_file(input_content: &'a str) -> Self {
156        Handler {
157            error_count: RwLock::new(0),
158            warning_count: RwLock::new(0),
159            input: SimpleFile::new("unknown file", input_content),
160            output: RwLock::new(Box::new(StandardStream::stderr(ColorChoice::Always))),
161            config: Config::default(),
162        }
163    }
164
165    /// Emits a single [Diagnostic] to the terminal
166    pub fn emit(&self, diag: &Diagnostic) {
167        let mut diag = diag.clone();
168        if diag.has_indirect_span {
169            diag.inner
170                .notes
171                .push("Warning was caused indirectly by transformations.".into());
172        }
173        self.emit_raw(diag.inner);
174    }
175
176    /// Emits a [RtLolaError] to the console
177    pub fn emit_error(&self, err: &RtLolaError) {
178        err.iter().for_each(|diag| self.emit(diag));
179    }
180
181    fn emit_raw(&self, diag: RawDiagnostic<()>) {
182        match diag.severity {
183            Severity::Error => *self.error_count.write().unwrap() += 1,
184            Severity::Warning => *self.warning_count.write().unwrap() += 1,
185            _ => {}
186        }
187        term::emit(
188            (*self.output.write().unwrap()).as_mut(),
189            &self.config,
190            &self.input,
191            &diag,
192        )
193        .expect("Could not write diagnostic.");
194    }
195
196    /// Returns true if an error has occurred
197    pub fn contains_error(&self) -> bool {
198        self.emitted_errors() > 0
199    }
200
201    /// Returns the number of emitted errors
202    pub fn emitted_errors(&self) -> usize {
203        *self.error_count.read().unwrap()
204    }
205
206    /// Returns the number of emitted warnings
207    pub fn emitted_warnings(&self) -> usize {
208        *self.warning_count.read().unwrap()
209    }
210
211    /// Emits a simple warning with a message
212    pub fn warn(&self, message: &str) {
213        self.emit_raw(RawDiagnostic::warning().with_message(message))
214    }
215
216    /// Emits a warning referring to the code span `span` with and optional label `span_label`
217    /// that is printed next to the code fragment
218    pub fn warn_with_span(&self, message: &str, span: Span, span_label: Option<&str>) {
219        let mut diag = RawDiagnostic::warning().with_message(message);
220        if !span.is_unknown() {
221            let mut label = Label::primary((), span);
222            if let Some(l) = span_label {
223                label.message = l.into();
224            }
225            diag.labels = vec![label];
226        }
227        if span.is_indirect() {
228            diag.notes = vec!["Warning was caused indirectly by transformations.".into()];
229        }
230        self.emit_raw(diag)
231    }
232
233    /// Emits a simple error with a message
234    pub fn error(&self, message: &str) {
235        self.emit_raw(RawDiagnostic::error().with_message(message))
236    }
237
238    /// Emits an error referring to the code span `span` with and optional label `span_label`
239    /// that is printed next to the code fragment
240    pub fn error_with_span(&self, message: &str, span: Span, span_label: Option<&str>) {
241        let mut diag = RawDiagnostic::error().with_message(message);
242        if !span.is_unknown() {
243            let mut label = Label::primary((), span);
244            if let Some(l) = span_label {
245                label.message = l.into();
246            }
247            diag.labels = vec![label];
248        }
249        if span.is_indirect() {
250            diag.notes = vec!["Error was caused indirectly by transformations.".into()];
251        }
252        self.emit_raw(diag)
253    }
254}
255
256/// A [Diagnostic] is more flexible way to build and output errors and warnings.
257#[derive(Debug, Clone, Serialize, Deserialize)]
258pub struct Diagnostic {
259    /// The internal representation of the diagnostic
260    pub(crate) inner: RawDiagnostic<()>,
261    /// True if the diagnostic refers to at least one indirect span
262    pub(crate) has_indirect_span: bool,
263}
264
265impl Diagnostic {
266    /// Creates a new warning with the message `message`
267    pub fn warning(message: &str) -> Self {
268        Diagnostic {
269            inner: RawDiagnostic::warning().with_message(message),
270            has_indirect_span: false,
271        }
272    }
273
274    /// Creates a new error with the message `message`
275    pub fn error(message: &str) -> Self {
276        Diagnostic {
277            inner: RawDiagnostic::error().with_message(message),
278            has_indirect_span: false,
279        }
280    }
281
282    /// Adds a code span to the diagnostic.
283    /// The `label` is printed next to the code fragment the span refers to.
284    /// If `primary` is set to true the span is treated as the primary code fragment.
285    pub fn add_span_with_label(mut self, span: Span, label: Option<&str>, primary: bool) -> Self {
286        if span.is_unknown() {
287            return self;
288        }
289        self.has_indirect_span |= span.is_indirect();
290        let mut rep_label = if primary {
291            Label::primary((), span)
292        } else {
293            Label::secondary((), span)
294        };
295        if let Some(l) = label {
296            rep_label.message = l.into();
297        }
298        self.inner.labels.push(rep_label);
299        self
300    }
301
302    /// Adds a code span to the diagnostic if the span is available.
303    /// The `label` is printed next to the code fragment the span refers to.
304    /// If `primary` is set to true the span is treated as the primary code fragment.
305    pub fn maybe_add_span_with_label(
306        mut self,
307        span: Option<Span>,
308        label: Option<&str>,
309        primary: bool,
310    ) -> Self {
311        let span = match span {
312            None | Some(Span::Unknown) => return self,
313            Some(s) => s,
314        };
315        self.has_indirect_span |= span.is_indirect();
316        let mut rep_label = if primary {
317            Label::primary((), span)
318        } else {
319            Label::secondary((), span)
320        };
321        if let Some(l) = label {
322            rep_label.message = l.into();
323        }
324        self.inner.labels.push(rep_label);
325        self
326    }
327
328    /// Adds a note to the bottom of the diagnostic.
329    pub fn add_note(mut self, note: &str) -> Self {
330        self.inner.notes.push(note.into());
331        self
332    }
333}
334
335impl From<Diagnostic> for RawDiagnostic<()> {
336    fn from(diag: Diagnostic) -> Self {
337        diag.inner
338    }
339}
340
341#[derive(Debug, Clone, Serialize, Deserialize)]
342/// An error type to collect diagnostics throughout the frontend.
343pub struct RtLolaError {
344    errors: Vec<Diagnostic>,
345}
346
347impl Error for RtLolaError {}
348
349impl std::fmt::Display for RtLolaError {
350    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
351        writeln!(
352            f,
353            "RTLola Error: {} errors, {} warnings:",
354            self.num_errors(),
355            self.num_warnings()
356        )?;
357        for msg in self.iter() {
358            let severity = match msg.inner.severity {
359                Severity::Warning => "[WARNING]",
360                Severity::Error => "[ERROR]",
361                _ => unreachable!(),
362            };
363            writeln!(f, "- {severity} {}", msg.inner.message)?;
364        }
365        Ok(())
366    }
367}
368
369impl RtLolaError {
370    /// Creates a new error
371    pub fn new() -> Self {
372        RtLolaError { errors: vec![] }
373    }
374
375    /// Adds a [Diagnostic] to the error
376    pub fn add(&mut self, diag: Diagnostic) {
377        self.errors.push(diag)
378    }
379
380    /// Returns a slice of all [Diagnostic]s of the error
381    pub fn as_slice(&self) -> &[Diagnostic] {
382        self.errors.as_slice()
383    }
384
385    /// Returns the number of Diagnostics with the severity error
386    pub fn num_errors(&self) -> usize {
387        self.errors
388            .iter()
389            .filter(|e| matches!(e.inner.severity, Severity::Error))
390            .count()
391    }
392
393    /// Returns the number of Diagnostics with the severity warning
394    pub fn num_warnings(&self) -> usize {
395        self.errors
396            .iter()
397            .filter(|e| matches!(e.inner.severity, Severity::Warning))
398            .count()
399    }
400
401    /// Merges to errors into one by combining the internal collections
402    pub fn join(&mut self, mut other: RtLolaError) {
403        self.errors.append(&mut other.errors)
404    }
405
406    /// Returns an iterator over the [Diagnostic]s of the error
407    pub fn iter(&self) -> impl Iterator<Item = &Diagnostic> {
408        self.errors.iter()
409    }
410
411    /// Combines to Results with an RtLolaError as the Error type into a single Result.
412    /// If both results are Ok then `op` is applied to these values to construct the new Ok value.
413    /// If one of the errors is Err then the Err is returned
414    /// If both Results are errors then the RtLolaErrors are merged using [RtLolaError::join] and returned.
415    pub fn combine<L, R, U, F: FnOnce(L, R) -> U>(
416        left: Result<L, RtLolaError>,
417        right: Result<R, RtLolaError>,
418        op: F,
419    ) -> Result<U, RtLolaError> {
420        match (left, right) {
421            (Ok(l), Ok(r)) => Ok(op(l, r)),
422            (Ok(_), Err(e)) | (Err(e), Ok(_)) => Err(e),
423            (Err(mut l), Err(r)) => {
424                l.join(r);
425                Err(l)
426            }
427        }
428    }
429}
430
431impl Default for RtLolaError {
432    fn default() -> Self {
433        Self::new()
434    }
435}
436
437impl IntoIterator for RtLolaError {
438    type IntoIter = std::vec::IntoIter<Self::Item>;
439    type Item = Diagnostic;
440
441    fn into_iter(self) -> Self::IntoIter {
442        self.errors.into_iter()
443    }
444}
445
446impl FromIterator<Diagnostic> for RtLolaError {
447    fn from_iter<T: IntoIterator<Item = Diagnostic>>(iter: T) -> Self {
448        RtLolaError {
449            errors: iter.into_iter().collect(),
450        }
451    }
452}
453
454impl From<Diagnostic> for RtLolaError {
455    fn from(diag: Diagnostic) -> Self {
456        RtLolaError { errors: vec![diag] }
457    }
458}
459
460impl From<Result<(), RtLolaError>> for RtLolaError {
461    fn from(res: Result<(), RtLolaError>) -> Self {
462        match res {
463            Ok(()) => RtLolaError::new(),
464            Err(e) => e,
465        }
466    }
467}
468
469impl RtLolaError {
470    /// Collects the iterator of Result's into a Result of a collection, while
471    /// concatenating all RTLola errors together
472    #[allow(clippy::manual_try_fold)]
473    pub fn collect<T, Q: FromIterator<T> + Extend<T>>(
474        iter: impl IntoIterator<Item = Result<T, Self>>,
475    ) -> Result<Q, RtLolaError> {
476        iter.into_iter()
477            .fold(Ok(Q::from_iter(iter::empty())), |e, item| match (e, item) {
478                (Ok(mut e), Ok(item)) => {
479                    e.extend(iter::once(item));
480                    Ok(e)
481                }
482                (Err(e), Ok(_)) | (Ok(_), Err(e)) => Err(e),
483                (Err(mut e1), Err(e2)) => {
484                    e1.join(e2);
485                    Err(e1)
486                }
487            })
488    }
489}
490
491impl From<RtLolaError> for Result<(), RtLolaError> {
492    fn from(e: RtLolaError) -> Self {
493        if e.errors
494            .iter()
495            .filter(|e| matches!(e.inner.severity, Severity::Error))
496            .count()
497            == 0
498        {
499            Ok(())
500        } else {
501            Err(e)
502        }
503    }
504}
505
506#[cfg(test)]
507mod tests {
508    use std::path::PathBuf;
509
510    use super::*;
511
512    #[test]
513    fn error_span() {
514        let path = PathBuf::from("stdin");
515        let content = "input i: Int\noutput x = 5";
516        let handler = Handler::new(&path, &content);
517        let span = Span::Direct { start: 9, end: 12 };
518        handler.error_with_span("Unknown Type", span, Some("here".into()));
519        assert_eq!(handler.emitted_errors(), 1);
520    }
521
522    #[test]
523    fn warning_span() {
524        let path = PathBuf::from("stdin");
525        let content = "input i: Int\noutput x = 5";
526        let handler = Handler::new(&path, &content);
527        let span = Span::Direct { start: 9, end: 12 };
528        handler.warn_with_span("Unknown Type", span, Some("here".into()));
529        assert_eq!(handler.emitted_warnings(), 1);
530    }
531
532    #[test]
533    fn error() {
534        let path = PathBuf::from("stdin");
535        let content = "input i: Int\noutput x = 5";
536        let handler = Handler::new(&path, &content);
537        handler.error("Unknown Type");
538        assert_eq!(handler.emitted_errors(), 1);
539    }
540
541    #[test]
542    fn warning() {
543        let path = PathBuf::from("stdin");
544        let content = "input i: Int\noutput x = 5";
545        let handler = Handler::new(&path, &content);
546        handler.warn("Unknown Type");
547        assert_eq!(handler.emitted_warnings(), 1);
548    }
549
550    #[test]
551    fn error_span_no_label() {
552        let path = PathBuf::from("stdin");
553        let content = "input i: Int\noutput x = 5";
554        let handler = Handler::new(&path, &content);
555        let span = Span::Direct { start: 9, end: 12 };
556        handler.error_with_span("Unknown Type", span, None);
557        assert_eq!(handler.emitted_errors(), 1);
558    }
559
560    #[test]
561    fn custom() {
562        let path = PathBuf::from("stdin");
563        let content = "input i: Int\noutput x = 5";
564        let handler = Handler::new(&path, &content);
565        let span1 = Span::Direct { start: 9, end: 12 };
566        let span2 = Span::Indirect { start: 20, end: 21 };
567        let span3 = Span::Direct { start: 24, end: 25 };
568        handler.emit(
569            &Diagnostic::error("Failed with love")
570                .add_span_with_label(span1, Some("here"), true)
571                .add_span_with_label(span2, Some("and here"), false)
572                .maybe_add_span_with_label(None, Some("Maybe there is no span"), false)
573                .maybe_add_span_with_label(Some(span3), None, false)
574                .add_note("This is a note"),
575        );
576        assert_eq!(handler.emitted_errors(), 1);
577    }
578}