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 proc_macro2::{Ident, Literal, Span, TokenStream};
8use quote::quote_spanned;
9use serde::{Deserialize, Serialize};
10
11use crate::pretty_span::{PrettySpan, make_source_path_relative};
12
13/// Diagnostic reporting level.
14#[non_exhaustive]
15#[derive(Debug, Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Hash, Serialize, Deserialize)]
16pub enum Level {
17    /// An error.
18    ///
19    /// The most severe and important diagnostic. Errors will prevent compilation.
20    Error,
21    /// A warning.
22    ///
23    /// The second most severe diagnostic. Will not stop compilation.
24    Warning,
25    /// A note.
26    ///
27    /// The third most severe, or second least severe diagnostic.
28    Note,
29    /// A help message.
30    ///
31    /// The least severe and important diagnostic.
32    Help,
33}
34impl Level {
35    /// If this level is [`Level::Error`].
36    pub fn is_error(&self) -> bool {
37        self <= &Self::Error
38    }
39}
40
41/// Diagnostic. A warning or error (or lower [`Level`]) with a message and span. Shown by IDEs
42/// usually as a squiggly red or yellow underline.
43///
44/// Diagnostics must be emitted via [`Diagnostic::try_emit`], [`Diagnostic::to_tokens`], or
45/// [`Diagnostic::try_emit_all`] for diagnostics to show up.
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct Diagnostic<S = Span> {
48    /// Span (source code location).
49    pub span: S,
50    /// Severity level.
51    pub level: Level,
52    /// Human-readable message.
53    pub message: String,
54}
55impl<S> Diagnostic<S> {
56    /// If this diagnostic's level is [`Level::Error`].
57    pub fn is_error(&self) -> bool {
58        self.level.is_error()
59    }
60}
61impl Diagnostic {
62    /// Create a new diagnostic from the given span, level, and message.
63    pub fn spanned(span: Span, level: Level, message: impl Into<String>) -> Self {
64        let message = message.into();
65        Self {
66            span,
67            level,
68            message,
69        }
70    }
71
72    /// Emit if possible, otherwise return `Err` containing a [`TokenStream`] of a
73    /// `compile_error!(...)` call.
74    pub fn try_emit(&self) -> Result<(), TokenStream> {
75        #[cfg(nightly)]
76        if proc_macro::is_available() {
77            let pm_diag = match self.level {
78                Level::Error => self.span.unwrap().error(&*self.message),
79                Level::Warning => self.span.unwrap().warning(&*self.message),
80                Level::Note => self.span.unwrap().note(&*self.message),
81                Level::Help => self.span.unwrap().help(&*self.message),
82            };
83            pm_diag.emit();
84            return Ok(());
85        }
86        Err(self.to_tokens())
87    }
88
89    /// Emits all if possible, otherwise returns `Err` containing a [`TokenStream`] of
90    /// `compile_error!(...)` calls.
91    pub fn try_emit_all<'a>(
92        diagnostics: impl IntoIterator<Item = &'a Self>,
93    ) -> Result<(), TokenStream> {
94        if let Some(tokens) = diagnostics
95            .into_iter()
96            .filter_map(|diag| diag.try_emit().err())
97            .reduce(|mut tokens, next| {
98                tokens.extend(next);
99                tokens
100            })
101        {
102            Err(tokens)
103        } else {
104            Ok(())
105        }
106    }
107
108    /// Used to emulate `proc_macro::Diagnostic::emit` by turning this diagnostic into a properly spanned [`TokenStream`]
109    /// that emits an error via `compile_error!(...)` with this diagnostic's message.
110    pub fn to_tokens(&self) -> TokenStream {
111        let msg_lit: Literal = Literal::string(&self.message);
112        let unique_ident = {
113            let mut hasher = std::collections::hash_map::DefaultHasher::new();
114            self.level.hash(&mut hasher);
115            self.message.hash(&mut hasher);
116            let hash = hasher.finish();
117            Ident::new(&format!("diagnostic_{}", hash), self.span)
118        };
119
120        if Level::Error == self.level {
121            quote_spanned! {self.span=>
122                {
123                    ::core::compile_error!(#msg_lit);
124                }
125            }
126        } else {
127            // Emit as a `#[deprecated]` warning message.
128            let level_ident = Ident::new(&format!("{:?}", self.level), self.span);
129            quote_spanned! {self.span=>
130                {
131                    #[allow(dead_code, non_snake_case)]
132                    fn #unique_ident() {
133                        #[deprecated = #msg_lit]
134                        struct #level_ident {}
135                        #[warn(deprecated)]
136                        #level_ident {};
137                    }
138                }
139            }
140        }
141    }
142
143    /// Converts this into a serializable and deserializable Diagnostic. Span information is
144    /// converted into [`SerdeSpan`] which keeps the span info but cannot be plugged into or
145    /// emitted through the Rust compiler's diagnostic system.
146    pub fn to_serde(&self) -> Diagnostic<SerdeSpan> {
147        let Self {
148            span,
149            level,
150            message,
151        } = self;
152        Diagnostic {
153            span: (*span).into(),
154            level: *level,
155            message: message.clone(),
156        }
157    }
158}
159impl From<syn::Error> for Diagnostic {
160    fn from(value: syn::Error) -> Self {
161        Self::spanned(value.span(), Level::Error, value.to_string())
162    }
163}
164impl std::fmt::Display for Diagnostic {
165    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166        writeln!(f, "{:?}: {}", self.level, self.message)?;
167        write!(f, "  --> {}", PrettySpan(self.span))?;
168        Ok(())
169    }
170}
171impl std::fmt::Display for Diagnostic<SerdeSpan> {
172    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173        writeln!(f, "{:?}: {}", self.level, self.message)?;
174        write!(f, "  --> {}", self.span)?;
175        Ok(())
176    }
177}
178
179/// A serializable and deserializable version of [`Span`]. Cannot be plugged into the Rust
180/// compiler's diagnostic system.
181#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct SerdeSpan {
183    /// The source file path.
184    pub file: Option<String>,
185    /// Line number, one-indexed.
186    pub line: usize,
187    /// Column number, one-indexed.
188    pub column: usize,
189}
190impl From<Span> for SerdeSpan {
191    fn from(span: Span) -> Self {
192        #[cfg_attr(
193            not(nightly),
194            expect(unused_labels, reason = "conditional compilation")
195        )]
196        let file = 'a: {
197            #[cfg(nightly)]
198            if proc_macro::is_available() {
199                break 'a Some(span.unwrap().file());
200            }
201
202            None
203        };
204
205        Self {
206            file,
207            line: span.start().line,
208            column: span.start().column,
209        }
210    }
211}
212impl std::fmt::Display for SerdeSpan {
213    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
214        write!(
215            f,
216            "{}:{}:{}",
217            self.file
218                .as_ref()
219                .map(make_source_path_relative)
220                .map(|path| path.display().to_string())
221                .as_deref()
222                .unwrap_or("unknown"),
223            self.line,
224            self.column
225        )
226    }
227}