Skip to main content

zyn_core/mark/
diagnostic.rs

1use proc_macro2::Span;
2use proc_macro2::TokenStream;
3
4use crate::mark::Level;
5use crate::mark::MultiSpan;
6
7/// A compiler diagnostic (error, warning, note, or help message).
8///
9/// Immutable once built. Create instances via [`Builder`] or the
10/// free functions in [`crate::mark`].
11#[derive(Debug, Clone, Default)]
12pub struct Diagnostic {
13    level: Level,
14    message: String,
15    spans: Vec<Span>,
16    children: Vec<Diagnostic>,
17}
18
19/// A specialized [`Result`](std::result::Result) type for zyn diagnostics.
20pub type Result<T> = std::result::Result<T, Diagnostic>;
21
22impl Diagnostic {
23    /// Returns the joined span of this diagnostic, or `call_site` if no spans are attached.
24    pub fn span(&self) -> Span {
25        let mut value = self.spans.first().copied().unwrap_or_else(Span::call_site);
26
27        for item in &self.spans {
28            value = value.join(*item).unwrap_or_else(Span::call_site);
29        }
30
31        value
32    }
33
34    /// Returns the highest severity level among this diagnostic and its children.
35    pub fn level(&self) -> Level {
36        self.children
37            .iter()
38            .map(|d| d.level)
39            .max()
40            .unwrap_or(self.level)
41            .max(self.level)
42    }
43
44    pub fn iter(&self) -> impl Iterator<Item = &Self> {
45        self.children.iter()
46    }
47
48    pub fn walk(&self) -> Walk<'_> {
49        Walk {
50            stack: self.children.iter().rev().collect(),
51        }
52    }
53
54    /// Returns `true` if the highest severity level is [`Level::Error`] or above.
55    pub fn is_error(&self) -> bool {
56        self.level() >= Level::Error
57    }
58
59    /// Returns `true` if no level and no children are set.
60    pub fn is_empty(&self) -> bool {
61        self.level == Level::None && self.children.is_empty()
62    }
63
64    /// Returns the number of direct children.
65    pub fn len(&self) -> usize {
66        self.children.len()
67    }
68
69    // Emission
70
71    #[cfg(feature = "diagnostics")]
72    pub fn emit_as_item_tokens(self) -> TokenStream {
73        let diag: proc_macro2_diagnostics::Diagnostic = self.into();
74        diag.emit_as_item_tokens()
75    }
76
77    #[cfg(not(feature = "diagnostics"))]
78    pub fn emit_as_item_tokens(self) -> TokenStream {
79        self.emit_fallback()
80    }
81
82    #[cfg(feature = "diagnostics")]
83    pub fn emit_as_expr_tokens(self) -> TokenStream {
84        let diag: proc_macro2_diagnostics::Diagnostic = self.into();
85        diag.emit_as_expr_tokens()
86    }
87
88    #[cfg(not(feature = "diagnostics"))]
89    pub fn emit_as_expr_tokens(self) -> TokenStream {
90        self.emit_fallback()
91    }
92
93    /// Emits all accumulated diagnostics as compiler messages.
94    pub fn emit(self) -> TokenStream {
95        let mut tokens = TokenStream::new();
96
97        if self.level != Level::None {
98            tokens.extend(self.clone().emit_as_item_tokens());
99        }
100
101        for child in self.children {
102            tokens.extend(child.emit());
103        }
104
105        tokens
106    }
107
108    #[cfg(not(feature = "diagnostics"))]
109    fn emit_fallback(self) -> TokenStream {
110        let span = self.span();
111        let msg = &self.message;
112        let mut tokens = if self.level != Level::None && !msg.is_empty() {
113            let prefix = match self.level {
114                Level::Warning => "warning: ",
115                Level::Note => "note: ",
116                Level::Help => "help: ",
117                _ => "",
118            };
119            let full_msg = format!("{prefix}{msg}");
120            quote::quote_spanned! { span => compile_error!(#full_msg); }
121        } else {
122            TokenStream::new()
123        };
124
125        for child in self.children {
126            tokens.extend(child.emit_fallback());
127        }
128
129        tokens
130    }
131}
132
133impl std::fmt::Display for Diagnostic {
134    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135        if !self.message.is_empty() {
136            write!(f, "{}: {}", self.level, self.message)?;
137        }
138
139        for (i, child) in self.children.iter().enumerate() {
140            if !self.message.is_empty() || i > 0 {
141                writeln!(f)?;
142            }
143
144            write!(f, "{child}")?;
145        }
146
147        Ok(())
148    }
149}
150
151impl From<syn::Error> for Diagnostic {
152    fn from(error: syn::Error) -> Self {
153        let children: Vec<_> = error
154            .into_iter()
155            .map(|e| Diagnostic {
156                level: Level::Error,
157                message: e.to_string(),
158                spans: e.span().into_spans(),
159                children: Vec::new(),
160            })
161            .collect();
162
163        Self {
164            children,
165            ..Default::default()
166        }
167    }
168}
169
170#[cfg(feature = "diagnostics")]
171impl From<Diagnostic> for proc_macro2_diagnostics::Diagnostic {
172    fn from(value: Diagnostic) -> Self {
173        let span = value.span();
174        let mut diag = Self::spanned(span, value.level.into(), value.message);
175
176        for child in value.children {
177            diag = diag.spanned_child(child.span(), child.level.into(), child.message);
178        }
179
180        diag
181    }
182}
183
184impl IntoIterator for Diagnostic {
185    type Item = Diagnostic;
186    type IntoIter = std::vec::IntoIter<Diagnostic>;
187
188    fn into_iter(self) -> Self::IntoIter {
189        self.children.into_iter()
190    }
191}
192
193impl<'a> IntoIterator for &'a Diagnostic {
194    type Item = &'a Diagnostic;
195    type IntoIter = std::slice::Iter<'a, Diagnostic>;
196
197    fn into_iter(self) -> Self::IntoIter {
198        self.children.iter()
199    }
200}
201
202// ── Builder ──────────────────────────────────────────────────
203
204/// Builder for constructing [`Diagnostic`] instances.
205///
206/// All methods are builder-pattern (consume and return `Self`).
207/// Call [`build`](Self::build) to finalize into an immutable `Diagnostic`.
208#[derive(Debug, Clone, Default)]
209pub struct Builder {
210    level: Level,
211    message: String,
212    spans: Vec<Span>,
213    children: Vec<Diagnostic>,
214}
215
216impl Builder {
217    /// Sets the severity level.
218    pub fn level(mut self, level: Level) -> Self {
219        self.level = level;
220        self
221    }
222
223    /// Sets the diagnostic message.
224    pub fn message(mut self, msg: impl Into<String>) -> Self {
225        self.message = msg.into();
226        self
227    }
228
229    /// Sets the source span(s). Accepts a [`Span`], `Vec<Span>`, or `&[Span]`.
230    pub fn span(mut self, spans: impl MultiSpan) -> Self {
231        self.spans = spans.into_spans();
232        self
233    }
234
235    /// Adds a child diagnostic. Accepts a `Builder` (built automatically)
236    /// or a `Diagnostic` (via `Into<Builder>`).
237    #[allow(clippy::should_implement_trait)]
238    pub fn add(mut self, child: impl Into<Builder>) -> Self {
239        self.children.push(child.into().build());
240        self
241    }
242
243    /// Finalizes the builder into an immutable [`Diagnostic`].
244    pub fn build(self) -> Diagnostic {
245        Diagnostic {
246            level: self.level,
247            message: self.message,
248            spans: self.spans,
249            children: self.children,
250        }
251    }
252}
253
254impl From<Diagnostic> for Builder {
255    fn from(d: Diagnostic) -> Self {
256        Self {
257            level: d.level,
258            message: d.message,
259            spans: d.spans,
260            children: d.children,
261        }
262    }
263}
264
265// ── Walk ─────────────────────────────────────────────────────────────
266
267pub struct Walk<'a> {
268    stack: Vec<&'a Diagnostic>,
269}
270
271impl<'a> Iterator for Walk<'a> {
272    type Item = &'a Diagnostic;
273
274    fn next(&mut self) -> Option<Self::Item> {
275        let diag = self.stack.pop()?;
276        self.stack.extend(diag.children.iter().rev());
277        Some(diag)
278    }
279}