codespan_derive_proc/
lib.rs

1use std::collections::{BTreeSet, HashMap};
2
3use proc_macro2::TokenStream;
4use quote::{format_ident, quote, ToTokens};
5use syn::{
6    spanned::Spanned, Attribute, Error, Ident, Index, Lit, Meta, MetaNameValue, Path, Result, Type,
7};
8use synstructure::{decl_derive, Structure};
9
10decl_derive!([IntoDiagnostic, attributes(file_id, message, render, note, primary, secondary)] => diagnostic_derive);
11
12#[derive(PartialEq, Eq, PartialOrd, Ord, Hash)]
13enum FieldName {
14    Named(Ident),
15    Numbered(u32),
16}
17
18fn diagnostic_derive(s: Structure) -> Result<TokenStream> {
19    let file_id_attr = syn::parse_str("file_id")?;
20    let message_attr = syn::parse_str("message")?;
21    let render_attr = syn::parse_str("render")?;
22    let note_attr = syn::parse_str("note")?;
23    let primary_attr = syn::parse_str("primary")?;
24    let secondary_attr = syn::parse_str("secondary")?;
25    let primary_style: Path = syn::parse_str("Primary")?;
26    let secondary_style: Path = syn::parse_str("Secondary")?;
27
28    let struct_span = s.ast().span();
29
30    let mut file_id = None;
31
32    for attr in &s.ast().attrs {
33        if attr.path == file_id_attr {
34            if let Some((_, other_span)) = &file_id {
35                let mut err = Error::new(*other_span, "Duplicated #[file_id(...)] attribute");
36                err.combine(Error::new(attr.span(), "Second occurrence is here"));
37                return Err(err);
38            }
39
40            file_id = Some((attr.parse_args::<Type>()?, attr.span()));
41        } else if attr.path == message_attr
42            || attr.path == render_attr
43            || attr.path == note_attr
44            || attr.path == primary_attr
45            || attr.path == secondary_attr
46        {
47            return Err(Error::new(
48                attr.span(),
49                format!("Unexpected attribute `{}`", attr.path.to_token_stream()),
50            ));
51        }
52    }
53
54    let file_id = file_id
55        .ok_or_else(|| Error::new(struct_span, "Expected `#[file_id(Type)]` attribute"))?
56        .0;
57
58    let mut branches = vec![];
59
60    for v in s.variants() {
61        // Create a mapping from field name to pattern binding name
62        // Binding names (according to synstructure) are always `__binding_#`
63        let members = match &v.ast().fields {
64            syn::Fields::Unit => HashMap::new(),
65            syn::Fields::Named(f) => f
66                .named
67                .iter()
68                .enumerate()
69                .map(|(i, field)| {
70                    (
71                        FieldName::Named(field.ident.as_ref().unwrap().clone()),
72                        format_ident!("__binding_{}", i),
73                    )
74                })
75                .collect(),
76            syn::Fields::Unnamed(f) => f
77                .unnamed
78                .iter()
79                .enumerate()
80                .map(|(i, _)| {
81                    (
82                        FieldName::Numbered(i as u32),
83                        format_ident!("__binding_{}", i),
84                    )
85                })
86                .collect(),
87        };
88
89        let members_ordered: Vec<_> = (0..members.len())
90            .map(|i| format_ident!("__binding_{}", i))
91            .collect();
92
93        // TokenStream of the `format!` error message, plus Span of occurrence of
94        // attribute in case it's duplicated and we need to error out.
95        let mut why = None;
96        // TokenStream of the render function call, plus Span of occurrence of
97        // attribute in case it's duplicated and we need to error out.
98        let mut render = None;
99        // Vector of Label creations, corresponding to `#[span]`.
100        let mut labels = vec![];
101        // Vector of TokenStream of `format!` generated for `#[note]`.
102        let mut notes = vec![];
103
104        for attr in v.ast().attrs.iter() {
105            if attr.path == message_attr {
106                if let Some((_, other_span)) = &why {
107                    let mut err = Error::new(*other_span, "Duplicated #[message = ...] attribute");
108                    err.combine(Error::new(attr.span(), "Second occurrence is here"));
109                    return Err(err);
110                } else if let Some((_, other_span)) = &render {
111                    let mut err = Error::new(
112                        *other_span,
113                        "#[message = ...] attribute not compatible with #[render(...)] attribute",
114                    );
115                    err.combine(Error::new(
116                        attr.span(),
117                        "#[render(...)] attribute occurs here",
118                    ));
119                    return Err(err);
120                }
121
122                why = Some((attr_to_format(attr, &members)?, attr.span()));
123            } else if attr.path == render_attr {
124                if let Some((_, other_span)) = &why {
125                    let mut err = Error::new(
126                        *other_span,
127                        "#[message = ...] attribute not compatible with #[render(...)] attribute",
128                    );
129                    err.combine(Error::new(
130                        attr.span(),
131                        "#[message = ...] attribute occurs here",
132                    ));
133                    return Err(err);
134                } else if let Some((_, other_span)) = &render {
135                    let mut err = Error::new(*other_span, "Duplicated #[render(...)] attribute");
136                    err.combine(Error::new(attr.span(), "Second occurrence is here"));
137                    return Err(err);
138                }
139
140                render = Some((attr_to_render(attr, &members_ordered)?, attr.span()));
141            } else if attr.path == note_attr {
142                let note = attr_to_format(attr, &members)?;
143                notes.push(note);
144            } else if attr.path == primary_attr
145                || attr.path == secondary_attr
146                || attr.path == file_id_attr
147            {
148                return Err(Error::new(
149                    attr.span(),
150                    format!("Unexpected attribute `{}`", attr.path.to_token_stream()),
151                ));
152            }
153        }
154
155        for b in v.bindings() {
156            let binding = &b.binding;
157
158            for attr in &b.ast().attrs {
159                if attr.path == primary_attr || attr.path == secondary_attr {
160                    let style = if attr.path == primary_attr {
161                        &primary_style
162                    } else {
163                        &secondary_style
164                    };
165
166                    let label = match attr.parse_meta()? {
167                        Meta::Path(_) => {
168                            quote! {
169                                ::codespan_derive::IntoLabel::into_label( #binding, ::codespan_derive::LabelStyle::#style )
170                            }
171                        }
172                        Meta::NameValue(MetaNameValue { .. }) => {
173                            let message = attr_to_format(&attr, &members)?;
174
175                            quote! {
176                                ::codespan_derive::IntoLabel::into_label( #binding, ::codespan_derive::LabelStyle::#style )
177                                    .with_message( #message )
178                            }
179                        }
180                        _ => return Err(Error::new(attr.span(),
181                        format!("Expected `span` attribute to be of the form: `#[span]` or `#[span = \"Message...\"]`"))),
182                    };
183
184                    labels.push(label);
185                } else if attr.path == message_attr
186                    || attr.path == render_attr
187                    || attr.path == note_attr
188                    || attr.path == file_id_attr
189                {
190                    return Err(Error::new(
191                        attr.span(),
192                        format!("Unexpected attribute `{}`", attr.path.to_token_stream()),
193                    ));
194                }
195            }
196        }
197
198        let pat = v.pat();
199
200        if let Some((why, _)) = why {
201            branches.push(quote! {
202                #pat => {
203                    ::codespan_derive::Diagnostic::< #file_id >::error()
204                        .with_message( #why )
205                        .with_labels(vec![ #(#labels),* ])
206                        .with_notes(vec![ #(#notes),* ])
207                }
208            });
209        } else if let Some((render, _)) = render {
210            branches.push(quote! {
211                #pat => {
212                    #render
213                        .with_labels(vec![ #(#labels),* ])
214                        .with_notes(vec![ #(#notes),* ])
215                }
216            });
217        } else {
218            return Err(Error::new(
219                v.ast().ident.span(),
220                "Expected `#[message = \"Message...\"]` or `#[render(function)]` attribute",
221            ));
222        }
223    }
224
225    Ok(s.gen_impl(quote! {
226        gen impl ::codespan_derive::IntoDiagnostic for @Self {
227            type FileId = #file_id ;
228
229            #[allow(dead_code)]
230            fn into_diagnostic(&self) -> ::codespan_derive::Diagnostic::< #file_id > {
231                match self {
232                    #(#branches),*
233                    _ => { panic!("Uninhabited type cannot be turned into a Diagnostic") }
234                }
235            }
236        }
237    }))
238}
239
240/// Turns an `#[... = "format string"]` into a `format!()` invocation
241fn attr_to_format(attr: &Attribute, members: &HashMap<FieldName, Ident>) -> Result<TokenStream> {
242    match attr.parse_meta()? {
243        Meta::NameValue(MetaNameValue {
244            lit: Lit::Str(msg), ..
245        }) => {
246            let msg_span = msg.span();
247            let mut msg: &str = &msg.value();
248
249            let mut idents = BTreeSet::new();
250            let mut out = String::new();
251
252            while !msg.is_empty() {
253                if let Some(i) = msg.find(&['{', '}'][..]) {
254                    out += &msg[..i];
255
256                    if msg[i..].starts_with("{{") {
257                        out += "{{";
258                        msg = &msg[i + 2..];
259                    } else if msg[i..].starts_with("}}") {
260                        out += "}}";
261                        msg = &msg[i + 2..];
262                    } else if msg[i..].starts_with('}') {
263                        return Err(Error::new(msg_span, "Unterminated `}` in format string"));
264                    } else {
265                        msg = &msg[i + 1..];
266
267                        if let Some(j) = msg.find('}') {
268                            let (field, rest) = if let Some(k) = msg[0..j].find(":") {
269                                (&msg[0..k], Some(&msg[k..j]))
270                            } else {
271                                (&msg[0..j], None)
272                            };
273
274                            // Now reset msg
275                            msg = &msg[j + 1..];
276
277                            let member = if let Ok(ident) = syn::parse_str::<Ident>(field) {
278                                FieldName::Named(ident)
279                            } else if let Ok(num) = syn::parse_str::<Index>(field) {
280                                FieldName::Numbered(num.index)
281                            } else {
282                                return Err(Error::new(
283                                    msg_span,
284                                    format!(
285                                        "Expected either a struct member name or index, got `{}`",
286                                        field
287                                    ),
288                                ));
289                            };
290
291                            out += "{";
292
293                            if let Some(ident) = members.get(&member) {
294                                out += &ident.to_string();
295                                idents.insert(ident.clone());
296                            } else {
297                                return Err(Error::new(
298                                    msg_span,
299                                    format!(
300                                        "Struct member name or index `{}` is not a valid field",
301                                        field
302                                    ),
303                                ));
304                            }
305
306                            if let Some(rest) = rest {
307                                out += rest;
308                            }
309
310                            out += "}";
311                        } else {
312                            return Err(Error::new(msg_span, "Unterminated `{` in format string"));
313                        }
314                    }
315                } else {
316                    out += msg;
317                    msg = "";
318                }
319            }
320
321            Ok(quote! {
322                format!(#out, #(#idents = #idents),*)
323            })
324        }
325        _ => Err(Error::new(
326            attr.span(),
327            format!(
328                "Expected {name} attribute to be of the form: `#[{name} = \"FormatString\"]`",
329                name = attr.path.to_token_stream()
330            ),
331        )),
332    }
333}
334
335fn attr_to_render(attr: &Attribute, members: &Vec<Ident>) -> Result<TokenStream> {
336    let path: Path = attr.parse_args()?;
337
338    Ok(quote! {
339        #path (#(#members),*)
340    })
341}