dfir_lang/
diagnostic.rs

1//! Compatibility for `proc_macro` diagnostics, which are missing from [`proc_macro2`].
2
3extern crate proc_macro;
4
5use std::borrow::Cow;
6use std::hash::{Hash, Hasher};
7
8use proc_macro2::{Ident, Literal, Span, TokenStream};
9use quote::quote_spanned;
10use serde::{Deserialize, Serialize};
11
12use crate::pretty_span::PrettySpan;
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    /// If this level is [`Level::Error`].
37    pub fn is_error(&self) -> bool {
38        self <= &Self::Error
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/// [`Diagnostic::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<S> Diagnostic<S> {
57    /// If this diagnostic's level is [`Level::Error`].
58    pub fn is_error(&self) -> bool {
59        self.level.is_error()
60    }
61}
62impl Diagnostic {
63    /// Create a new diagnostic from the given span, level, and message.
64    pub fn spanned(span: Span, level: Level, message: impl Into<String>) -> Self {
65        let message = message.into();
66        Self {
67            span,
68            level,
69            message,
70        }
71    }
72
73    /// Emit if possible, otherwise return `Err` containing a [`TokenStream`] of a
74    /// `compile_error!(...)` call.
75    pub fn try_emit(&self) -> Result<(), TokenStream> {
76        #[cfg(nightly)]
77        if proc_macro::is_available() {
78            let pm_diag = match self.level {
79                Level::Error => self.span.unwrap().error(&*self.message),
80                Level::Warning => self.span.unwrap().warning(&*self.message),
81                Level::Note => self.span.unwrap().note(&*self.message),
82                Level::Help => self.span.unwrap().help(&*self.message),
83            };
84            pm_diag.emit();
85            return Ok(());
86        }
87        Err(self.to_tokens())
88    }
89
90    /// Emits all if possible, otherwise returns `Err` containing a [`TokenStream`] of
91    /// `compile_error!(...)` calls.
92    pub fn try_emit_all<'a>(
93        diagnostics: impl IntoIterator<Item = &'a Self>,
94    ) -> Result<(), TokenStream> {
95        if let Some(tokens) = diagnostics
96            .into_iter()
97            .filter_map(|diag| diag.try_emit().err())
98            .reduce(|mut tokens, next| {
99                tokens.extend(next);
100                tokens
101            })
102        {
103            Err(tokens)
104        } else {
105            Ok(())
106        }
107    }
108
109    /// Used to emulate `proc_macro::Diagnostic::emit` by turning this diagnostic into a properly spanned [`TokenStream`]
110    /// that emits an error via `compile_error!(...)` with this diagnostic's message.
111    pub fn to_tokens(&self) -> TokenStream {
112        let msg_lit: Literal = Literal::string(&self.message);
113        let unique_ident = {
114            let mut hasher = std::collections::hash_map::DefaultHasher::new();
115            self.level.hash(&mut hasher);
116            self.message.hash(&mut hasher);
117            let hash = hasher.finish();
118            Ident::new(&format!("diagnostic_{}", hash), self.span)
119        };
120
121        if Level::Error == self.level {
122            quote_spanned! {self.span=>
123                {
124                    ::core::compile_error!(#msg_lit);
125                }
126            }
127        } else {
128            // Emit as a `#[deprecated]` warning message.
129            let level_ident = Ident::new(&format!("{:?}", self.level), self.span);
130            quote_spanned! {self.span=>
131                {
132                    #[allow(dead_code, non_snake_case)]
133                    fn #unique_ident() {
134                        #[deprecated = #msg_lit]
135                        struct #level_ident {}
136                        #[warn(deprecated)]
137                        #level_ident {};
138                    }
139                }
140            }
141        }
142    }
143
144    /// Converts this into a serializable and deserializable Diagnostic. Span information is
145    /// converted into [`SerdeSpan`] which keeps the span info but cannot be plugged into or
146    /// emitted through the Rust compiler's diagnostic system.
147    pub fn to_serde(&self) -> Diagnostic<SerdeSpan> {
148        let Self {
149            span,
150            level,
151            message,
152        } = self;
153        Diagnostic {
154            span: (*span).into(),
155            level: *level,
156            message: message.clone(),
157        }
158    }
159}
160impl From<syn::Error> for Diagnostic {
161    fn from(value: syn::Error) -> Self {
162        Self::spanned(value.span(), Level::Error, value.to_string())
163    }
164}
165impl std::fmt::Display for Diagnostic {
166    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167        writeln!(f, "{:?}: {}", self.level, self.message)?;
168        write!(f, "  --> {}", PrettySpan(self.span))?;
169        Ok(())
170    }
171}
172impl std::fmt::Display for Diagnostic<SerdeSpan> {
173    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174        writeln!(f, "{:?}: {}", self.level, self.message)?;
175        write!(f, "  --> {}", self.span)?;
176        Ok(())
177    }
178}
179
180/// A serializable and deserializable version of [`Span`]. Cannot be plugged into the Rust
181/// compiler's diagnostic system.
182#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct SerdeSpan {
184    /// The source file path.
185    // https://github.com/serde-rs/serde/issues/1852#issuecomment-904840811
186    #[serde(borrow)]
187    pub path: Cow<'static, str>,
188    /// Line number, one-indexed.
189    pub line: usize,
190    /// Column number, one-indexed.
191    pub column: usize,
192}
193impl From<Span> for SerdeSpan {
194    fn from(span: Span) -> Self {
195        #[cfg_attr(
196            not(nightly),
197            expect(unused_labels, reason = "conditional compilation")
198        )]
199        let path = 'a: {
200            #[cfg(nightly)]
201            if proc_macro::is_available() {
202                break 'a span
203                    .unwrap()
204                    .source_file()
205                    .path()
206                    .display()
207                    .to_string()
208                    .into();
209            }
210
211            "unknown".into()
212        };
213
214        Self {
215            path,
216            line: span.start().line,
217            column: span.start().column,
218        }
219    }
220}
221impl std::fmt::Display for SerdeSpan {
222    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
223        write!(f, "{}:{}:{}", self.path, self.line, self.column)
224    }
225}