Skip to main content

dfir_lang/
diagnostic.rs

1//! Compatibility for `proc_macro` diagnostics, which are missing from [`proc_macro2`].
2
3extern crate proc_macro;
4
5use std::hash::{Hash, Hasher};
6
7use itertools::Itertools;
8use proc_macro2::{Ident, Literal, Span, TokenStream};
9use quote::quote_spanned;
10use serde::{Deserialize, Serialize};
11
12use crate::pretty_span::{PrettySpan, make_source_path_relative};
13
14/// Diagnostic reporting level.
15#[non_exhaustive]
16#[derive(Debug, Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Hash, Serialize, Deserialize)]
17pub enum Level {
18    /// An error.
19    ///
20    /// The most severe and important diagnostic. Errors will prevent compilation.
21    Error,
22    /// A warning.
23    ///
24    /// The second most severe diagnostic. Will not stop compilation.
25    Warning,
26    /// A note.
27    ///
28    /// The third most severe, or second least severe diagnostic.
29    Note,
30    /// A help message.
31    ///
32    /// The least severe and important diagnostic.
33    Help,
34}
35impl Level {
36    /// Iterator of all levels from most to least severe.
37    pub fn iter() -> std::array::IntoIter<Self, 4> {
38        [Self::Error, Self::Warning, Self::Note, Self::Help].into_iter()
39    }
40}
41
42/// Diagnostic. A warning or error (or lower [`Level`]) with a message and span. Shown by IDEs
43/// usually as a squiggly red or yellow underline.
44///
45/// Diagnostics must be emitted via [`Diagnostic::try_emit`], [`Diagnostic::to_tokens`], or
46/// [`Diagnostics::try_emit_all`] for diagnostics to show up.
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct Diagnostic<S = Span> {
49    /// Span (source code location).
50    pub span: S,
51    /// Severity level.
52    pub level: Level,
53    /// Human-readable message.
54    pub message: String,
55}
56impl Diagnostic {
57    /// Create a new diagnostic from the given span, level, and message.
58    pub fn spanned(span: Span, level: Level, message: impl Into<String>) -> Self {
59        let message = message.into();
60        Self {
61            span,
62            level,
63            message,
64        }
65    }
66
67    /// Emit if possible, otherwise return `Err` containing a [`TokenStream`] of a
68    /// `compile_error!(...)` call.
69    pub fn try_emit(&self) -> Result<(), TokenStream> {
70        #[cfg(nightly)]
71        if proc_macro::is_available() {
72            let pm_diag = match self.level {
73                Level::Error => self.span.unwrap().error(&*self.message),
74                Level::Warning => self.span.unwrap().warning(&*self.message),
75                Level::Note => self.span.unwrap().note(&*self.message),
76                Level::Help => self.span.unwrap().help(&*self.message),
77            };
78            pm_diag.emit();
79            return Ok(());
80        }
81        Err(self.to_tokens())
82    }
83
84    /// Used to emulate `proc_macro::Diagnostic::emit` by turning this diagnostic into a properly spanned [`TokenStream`]
85    /// that emits an error via `compile_error!(...)` with this diagnostic's message.
86    pub fn to_tokens(&self) -> TokenStream {
87        let msg_lit: Literal = Literal::string(&self.message);
88        let unique_ident = {
89            let mut hasher = std::collections::hash_map::DefaultHasher::new();
90            self.level.hash(&mut hasher);
91            self.message.hash(&mut hasher);
92            let hash = hasher.finish();
93            Ident::new(&format!("diagnostic_{}", hash), self.span)
94        };
95
96        if Level::Error == self.level {
97            quote_spanned! {self.span=>
98                {
99                    ::core::compile_error!(#msg_lit);
100                }
101            }
102        } else {
103            // Emit as a `#[deprecated]` warning message.
104            let level_ident = Ident::new(&format!("{:?}", self.level), self.span);
105            quote_spanned! {self.span=>
106                {
107                    #[allow(dead_code, non_snake_case)]
108                    fn #unique_ident() {
109                        #[deprecated = #msg_lit]
110                        struct #level_ident {}
111                        #[warn(deprecated)]
112                        #level_ident {};
113                    }
114                }
115            }
116        }
117    }
118
119    /// Converts this into a serializable and deserializable Diagnostic. Span information is
120    /// converted into [`SerdeSpan`] which keeps the span info but cannot be plugged into or
121    /// emitted through the Rust compiler's diagnostic system.
122    pub fn to_serde(&self) -> Diagnostic<SerdeSpan> {
123        let Self {
124            span,
125            level,
126            message,
127        } = self;
128        Diagnostic {
129            span: (*span).into(),
130            level: *level,
131            message: message.clone(),
132        }
133    }
134}
135impl From<syn::Error> for Diagnostic {
136    fn from(value: syn::Error) -> Self {
137        Self::spanned(value.span(), Level::Error, value.to_string())
138    }
139}
140impl std::fmt::Display for Diagnostic {
141    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142        writeln!(f, "{:?}: {}", self.level, self.message)?;
143        write!(f, "  --> {}", PrettySpan(self.span))?;
144        Ok(())
145    }
146}
147impl std::fmt::Display for Diagnostic<SerdeSpan> {
148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149        writeln!(f, "{:?}: {}", self.level, self.message)?;
150        write!(f, "  --> {}", self.span)?;
151        Ok(())
152    }
153}
154
155/// A serializable and deserializable version of [`Span`]. Cannot be plugged into the Rust
156/// compiler's diagnostic system.
157#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct SerdeSpan {
159    /// The source file path.
160    pub file: Option<String>,
161    /// Line number, one-indexed.
162    pub line: usize,
163    /// Column number, one-indexed.
164    pub column: usize,
165}
166impl From<Span> for SerdeSpan {
167    fn from(span: Span) -> Self {
168        let file = if proc_macro::is_available() {
169            Some(span.unwrap().file())
170        } else {
171            None
172        };
173
174        Self {
175            file,
176            line: span.start().line,
177            column: span.start().column,
178        }
179    }
180}
181impl std::fmt::Display for SerdeSpan {
182    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183        write!(
184            f,
185            "{}:{}:{}",
186            self.file
187                .as_ref()
188                .map(make_source_path_relative)
189                .map(|path| path.display().to_string())
190                .as_deref()
191                .unwrap_or("unknown"),
192            self.line,
193            self.column
194        )
195    }
196}
197
198/// A basic wrapper around [`Vec<Diagnostic>`] with pretty debug printing and utility methods.
199pub struct Diagnostics<S = Span> {
200    diagnostics: Vec<Diagnostic<S>>,
201}
202
203impl<S> Default for Diagnostics<S> {
204    fn default() -> Self {
205        Self::new()
206    }
207}
208
209impl<S> Diagnostics<S> {
210    /// Creates a new empty `Diagnostics`.
211    pub fn new() -> Self {
212        Self {
213            diagnostics: Vec::new(),
214        }
215    }
216
217    /// Retains only diagnostics as severe as `level` or more severe.
218    pub fn retain_level(&mut self, level: Level) {
219        self.diagnostics.retain(|d| d.level <= level);
220    }
221
222    /// Returns if any errors exist in this collection.
223    pub fn has_error(&self) -> bool {
224        self.diagnostics.iter().any(|d| Level::Error == d.level)
225    }
226
227    /// Adds a diagnostic to this collection.
228    pub fn push(&mut self, diagnostic: Diagnostic<S>) {
229        self.diagnostics.push(diagnostic);
230    }
231
232    /// Returns an iterator over the diagnostics.
233    pub fn iter(&self) -> std::slice::Iter<'_, Diagnostic<S>> {
234        self.diagnostics.iter()
235    }
236
237    /// Returns the number of diagnostics in this collection.
238    pub fn len(&self) -> usize {
239        self.diagnostics.len()
240    }
241
242    /// Returns if this collection is empty.
243    pub fn is_empty(&self) -> bool {
244        self.diagnostics.is_empty()
245    }
246}
247
248impl Diagnostics {
249    /// Emits all if possible, otherwise returns `Err` containing a [`TokenStream`] of code spanned to emit each error
250    /// and warning indirectly.
251    pub fn try_emit_all(&self) -> Result<(), TokenStream> {
252        if let Some(tokens) = self
253            .diagnostics
254            .iter()
255            .filter_map(|diag| diag.try_emit().err())
256            .reduce(|mut tokens, next| {
257                tokens.extend(next);
258                tokens
259            })
260        {
261            Err(tokens)
262        } else {
263            Ok(())
264        }
265    }
266}
267
268impl<S> Extend<Diagnostic<S>> for Diagnostics<S> {
269    fn extend<T: IntoIterator<Item = Diagnostic<S>>>(&mut self, iter: T) {
270        self.diagnostics.extend(iter);
271    }
272}
273
274impl<S> std::fmt::Debug for Diagnostics<S>
275where
276    Diagnostic<S>: std::fmt::Display,
277{
278    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
279        if self.diagnostics.is_empty() {
280            write!(f, "Diagnostics (empty)")?;
281        } else {
282            write!(f, "Diagnostics (")?;
283            let groups = self.diagnostics.iter().into_group_map_by(|d| d.level);
284            for (level, count) in
285                Level::iter().filter_map(|level| groups.get(&level).map(|vec| (level, vec.len())))
286            {
287                write!(f, "{level:?}: {count}, ")?;
288            }
289            writeln!(f, "):")?;
290            for diagnostic in Level::iter()
291                .filter_map(|level| groups.get(&level))
292                .flatten()
293            {
294                writeln!(f, "{diagnostic}")?;
295            }
296        }
297        Ok(())
298    }
299}
300
301impl<S> FromIterator<Diagnostic<S>> for Diagnostics<S> {
302    fn from_iter<T: IntoIterator<Item = Diagnostic<S>>>(iter: T) -> Self {
303        Self {
304            diagnostics: Vec::from_iter(iter),
305        }
306    }
307}
308
309impl<S> IntoIterator for Diagnostics<S> {
310    type Item = Diagnostic<S>;
311    type IntoIter = std::vec::IntoIter<Self::Item>;
312
313    fn into_iter(self) -> Self::IntoIter {
314        self.diagnostics.into_iter()
315    }
316}