fea_rs/
diagnostic.rs

1//! Reporting errors, warnings, and other information to the user.
2use crate::{
3    parse::{FileId, SourceList},
4    ParseTree,
5};
6use std::{convert::TryInto, ops::Range, sync::Arc};
7
8/// A span of a source file.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct Span {
11    start: u32,
12    end: u32,
13}
14
15/// A diagnostic level
16#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
17#[repr(u8)]
18pub enum Level {
19    /// An unrecoverable error
20    Error,
21    /// A warning: something the user may want to address, but which is non-fatal
22    Warning,
23    /// Info. unused?
24    Info,
25}
26
27/// A message, associated with a location in a file.
28#[derive(Clone, Debug, PartialEq, Eq)]
29pub struct Message {
30    pub text: String,
31    pub file: FileId,
32    pub span: Span,
33}
34
35/// A diagnostic, including a message and additional annotations
36//TODO: would this be more useful with additional annotations or a help field?
37//some fancy error reporting crates have these.
38#[derive(Clone, Debug, PartialEq, Eq)]
39pub struct Diagnostic {
40    /// The main message for this diagnostic
41    pub message: Message,
42    /// The diagnostic level
43    pub level: Level,
44}
45
46/// A set of diagnostics with the associated source info
47#[derive(Clone)]
48pub struct DiagnosticSet {
49    pub(crate) messages: Vec<Diagnostic>,
50    pub(crate) sources: Arc<SourceList>,
51    pub(crate) max_to_print: usize,
52}
53
54impl Span {
55    /// Convert this span to a `Range<usize>`
56    pub fn range(&self) -> Range<usize> {
57        self.start as usize..self.end as usize
58    }
59}
60
61impl Diagnostic {
62    /// Create a new diagnostic
63    pub fn new(
64        level: Level,
65        file: FileId,
66        range: Range<usize>,
67        message: impl Into<String>,
68    ) -> Self {
69        Diagnostic {
70            message: Message {
71                text: message.into(),
72                span: Span {
73                    start: range.start.try_into().unwrap(),
74                    end: range.end.try_into().unwrap(),
75                },
76                file,
77            },
78            level,
79        }
80    }
81
82    /// Create a new error, at the provided location
83    pub fn error(file: FileId, span: Range<usize>, message: impl Into<String>) -> Self {
84        Diagnostic::new(Level::Error, file, span, message)
85    }
86
87    /// Create a new warning, at the provided location
88    pub fn warning(file: FileId, span: Range<usize>, message: impl Into<String>) -> Self {
89        Diagnostic::new(Level::Warning, file, span, message)
90    }
91
92    /// The diagnostic's message text
93    pub fn text(&self) -> &str {
94        &self.message.text
95    }
96
97    /// The location of the main span, as a `Range<usize>`
98    pub fn span(&self) -> Range<usize> {
99        self.message.span.range()
100    }
101
102    /// `true` if this diagnostic is an error
103    pub fn is_error(&self) -> bool {
104        matches!(self.level, Level::Error)
105    }
106}
107
108// we don't want diagnostic set to impl display itself, because we want to change
109// behaviour based on whether we think we're writing to a terminal, and that is
110// error prone.
111struct DiagnosticDisplayer<'a>(&'a DiagnosticSet);
112
113impl DiagnosticSet {
114    /// Create a new `DiagnosticSet`.
115    pub fn new(messages: Vec<Diagnostic>, tree: &ParseTree, max_to_print: usize) -> Self {
116        Self {
117            messages,
118            sources: tree.sources.clone(),
119            max_to_print,
120        }
121    }
122    /// The number of diagnostic messages
123    pub fn len(&self) -> usize {
124        self.messages.len()
125    }
126
127    /// `true` if there are no diagnostic messages
128    pub fn is_empty(&self) -> bool {
129        self.messages.is_empty()
130    }
131
132    /// `true` if any of the messages in this set indicate hard errors
133    pub fn has_errors(&self) -> bool {
134        self.messages.iter().any(|msg| msg.is_error())
135    }
136
137    /// Set the max number of messages to print.
138    pub fn set_max_to_print(&mut self, max_to_print: usize) {
139        self.max_to_print = max_to_print;
140    }
141
142    /// Discard any warnings, keeping only errors
143    pub fn discard_warnings(&mut self) {
144        self.messages.retain(|x| x.is_error());
145    }
146
147    /// Remove and return any warnings in this set.
148    pub fn split_off_warnings(&mut self) -> Option<DiagnosticSet> {
149        self.messages.sort_unstable_by_key(|d| d.level);
150        let split_at = self.messages.iter().position(|x| !x.is_error())?;
151        let warnings = self.messages.split_off(split_at);
152        Some(Self {
153            messages: warnings,
154            sources: self.sources.clone(),
155            max_to_print: self.max_to_print,
156        })
157    }
158
159    /// Return the underlying diagnostics, as a slice
160    pub fn diagnostics(&self) -> &[Diagnostic] {
161        &self.messages
162    }
163
164    /// Returns an opaque type that can pretty-print the diagnostics
165    pub fn display(&self) -> impl std::fmt::Display + '_ {
166        DiagnosticDisplayer(self)
167    }
168
169    pub(crate) fn write(&self, f: &mut impl std::fmt::Write, colorize: bool) -> std::fmt::Result {
170        let mut first = true;
171        for err in self.messages.iter().take(self.max_to_print) {
172            if !first {
173                writeln!(f)?;
174            }
175            write!(f, "{}", self.sources.format_diagnostic(err, colorize))?;
176            first = false;
177        }
178        if let Some(overflow) = self.messages.len().checked_sub(self.max_to_print) {
179            writeln!(f, "... and {overflow} more errors")?;
180        }
181        Ok(())
182    }
183
184    #[cfg(any(test, feature = "test"))]
185    pub(crate) fn to_string(&self, colorize: bool) -> String {
186        let mut out = String::new();
187        self.write(&mut out, colorize).unwrap();
188        out
189    }
190}
191
192impl std::fmt::Display for DiagnosticDisplayer<'_> {
193    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
194        use std::io::IsTerminal;
195        let colorize = std::io::stderr().is_terminal();
196        self.0.write(f, colorize)
197    }
198}
199
200impl std::fmt::Debug for DiagnosticSet {
201    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202        f.debug_struct("DiagnosticSet")
203            .field("messages", &self.messages)
204            .field("tree", &"ParseTree")
205            .finish()
206    }
207}