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        #[cfg_attr(
169            not(nightly),
170            expect(unused_labels, reason = "conditional compilation")
171        )]
172        let file = 'a: {
173            #[cfg(nightly)]
174            if proc_macro::is_available() {
175                break 'a Some(span.unwrap().file());
176            }
177
178            None
179        };
180
181        Self {
182            file,
183            line: span.start().line,
184            column: span.start().column,
185        }
186    }
187}
188impl std::fmt::Display for SerdeSpan {
189    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190        write!(
191            f,
192            "{}:{}:{}",
193            self.file
194                .as_ref()
195                .map(make_source_path_relative)
196                .map(|path| path.display().to_string())
197                .as_deref()
198                .unwrap_or("unknown"),
199            self.line,
200            self.column
201        )
202    }
203}
204
205/// A basic wrapper around [`Vec<Diagnostic>`] with pretty debug printing and utility methods.
206pub struct Diagnostics<S = Span> {
207    diagnostics: Vec<Diagnostic<S>>,
208}
209
210impl<S> Default for Diagnostics<S> {
211    fn default() -> Self {
212        Self::new()
213    }
214}
215
216impl<S> Diagnostics<S> {
217    /// Creates a new empty `Diagnostics`.
218    pub fn new() -> Self {
219        Self {
220            diagnostics: Vec::new(),
221        }
222    }
223
224    /// Retains only diagnostics as severe as `level` or more severe.
225    pub fn retain_level(&mut self, level: Level) {
226        self.diagnostics.retain(|d| d.level <= level);
227    }
228
229    /// Returns if any errors exist in this collection.
230    pub fn has_error(&self) -> bool {
231        self.diagnostics.iter().any(|d| Level::Error == d.level)
232    }
233
234    /// Adds a diagnostic to this collection.
235    pub fn push(&mut self, diagnostic: Diagnostic<S>) {
236        self.diagnostics.push(diagnostic);
237    }
238
239    /// Returns an iterator over the diagnostics.
240    pub fn iter(&self) -> std::slice::Iter<'_, Diagnostic<S>> {
241        self.diagnostics.iter()
242    }
243
244    /// Returns the number of diagnostics in this collection.
245    pub fn len(&self) -> usize {
246        self.diagnostics.len()
247    }
248
249    /// Returns if this collection is empty.
250    pub fn is_empty(&self) -> bool {
251        self.diagnostics.is_empty()
252    }
253}
254
255impl Diagnostics {
256    /// Emits all if possible, otherwise returns `Err` containing a [`TokenStream`] of code spanned to emit each error
257    /// and warning indirectly.
258    pub fn try_emit_all(&self) -> Result<(), TokenStream> {
259        if let Some(tokens) = self
260            .diagnostics
261            .iter()
262            .filter_map(|diag| diag.try_emit().err())
263            .reduce(|mut tokens, next| {
264                tokens.extend(next);
265                tokens
266            })
267        {
268            Err(tokens)
269        } else {
270            Ok(())
271        }
272    }
273}
274
275impl<S> Extend<Diagnostic<S>> for Diagnostics<S> {
276    fn extend<T: IntoIterator<Item = Diagnostic<S>>>(&mut self, iter: T) {
277        self.diagnostics.extend(iter);
278    }
279}
280
281impl<S> std::fmt::Debug for Diagnostics<S>
282where
283    Diagnostic<S>: std::fmt::Display,
284{
285    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286        if self.diagnostics.is_empty() {
287            write!(f, "Diagnostics (empty)")?;
288        } else {
289            write!(f, "Diagnostics (")?;
290            let groups = self.diagnostics.iter().into_group_map_by(|d| d.level);
291            for (level, count) in
292                Level::iter().filter_map(|level| groups.get(&level).map(|vec| (level, vec.len())))
293            {
294                write!(f, "{level:?}: {count}, ")?;
295            }
296            writeln!(f, "):")?;
297            for diagnostic in Level::iter()
298                .filter_map(|level| groups.get(&level))
299                .flatten()
300            {
301                writeln!(f, "{diagnostic}")?;
302            }
303        }
304        Ok(())
305    }
306}
307
308impl<S> FromIterator<Diagnostic<S>> for Diagnostics<S> {
309    fn from_iter<T: IntoIterator<Item = Diagnostic<S>>>(iter: T) -> Self {
310        Self {
311            diagnostics: Vec::from_iter(iter),
312        }
313    }
314}
315
316impl<S> IntoIterator for Diagnostics<S> {
317    type Item = Diagnostic<S>;
318    type IntoIter = std::vec::IntoIter<Self::Item>;
319
320    fn into_iter(self) -> Self::IntoIter {
321        self.diagnostics.into_iter()
322    }
323}