Skip to main content

debuggable_derive/
lib.rs

1//! Derive macro for [`debuggable`](https://docs.rs/debuggable). Use that crate instead of
2//! depending on this one directly.
3//!
4//! The derive parses `#[debuggable(...)]` attributes, validates them, and expands to one
5//! `::debuggable::__entry!` call carrying a JSON descriptor of the type
6//! (see `docs/internal/schema-v1.md`). All rendering happens in the debugger.
7
8mod emit;
9
10use emit::{Body, Field, Part, Ty, Variant};
11use proc_macro::TokenStream;
12use proc_macro2::Span;
13use syn::ext::IdentExt;
14use syn::spanned::Spanned;
15use syn::{Attribute, Data, DeriveInput, Error, Fields, GenericParam, LitStr, Result, Type};
16
17/// Derives debugger visualizers for a struct or enum. See the `debuggable` crate docs.
18#[proc_macro_derive(Debuggable, attributes(debuggable))]
19pub fn derive_debuggable(input: TokenStream) -> TokenStream {
20    let input = syn::parse_macro_input!(input as DeriveInput);
21    match expand(&input) {
22        Ok(tokens) => tokens.into(),
23        Err(e) => e.to_compile_error().into(),
24    }
25}
26
27fn expand(input: &DeriveInput) -> Result<proc_macro2::TokenStream> {
28    let ty = model(input)?;
29    Ok(emit::entry_call(&ty).parse().expect("generated tokens are valid"))
30}
31
32// ---- Attribute options --------------------------------------------------------------------
33
34/// Where an attribute appears; decides which options are allowed.
35#[derive(Clone, Copy, PartialEq)]
36enum Place {
37    Struct,
38    Enum,
39    Variant,
40    StructField,
41    VariantField,
42}
43
44#[derive(Default)]
45struct Opts {
46    summary: Option<LitStr>,
47    hide: Option<Span>,
48    rename: Option<LitStr>,
49    items: Option<Span>,
50    len: Option<LitStr>,
51}
52
53fn opts(attrs: &[Attribute], place: Place) -> Result<Opts> {
54    let mut o = Opts::default();
55    for attr in attrs.iter().filter(|a| a.path().is_ident("debuggable")) {
56        attr.parse_nested_meta(|m| {
57            let key = m.path.get_ident().map(|i| i.to_string()).unwrap_or_default();
58            let span = m.path.span();
59            let allowed = match key.as_str() {
60                "summary" => matches!(place, Place::Struct | Place::Variant),
61                "hide" | "rename" => matches!(place, Place::StructField | Place::VariantField),
62                "items" | "len" => place == Place::StructField,
63                _ => {
64                    const OPTIONS: [&str; 5] = ["summary", "hide", "rename", "items", "len"];
65                    return Err(m.error(match closest(&key, OPTIONS) {
66                        Some(best) => format!("unknown `debuggable` option `{key}`; did you mean `{best}`?"),
67                        None => "unknown `debuggable` option; expected one of: `summary`, `hide`, `rename`, `items`, `len`"
68                            .to_string(),
69                    }));
70                }
71            };
72            if !allowed {
73                return Err(Error::new(span, misplaced(&key, place)));
74            }
75            let flag = |m: &syn::meta::ParseNestedMeta, set: &mut Option<Span>| -> Result<()> {
76                if !m.input.is_empty() && !m.input.peek(syn::Token![,]) {
77                    return Err(m.error(format!("`{key}` takes no value")));
78                }
79                if set.replace(span).is_some() {
80                    return Err(Error::new(span, format!("duplicate `{key}`")));
81                }
82                Ok(())
83            };
84            let value = |m: &syn::meta::ParseNestedMeta, set: &mut Option<LitStr>| -> Result<()> {
85                let lit: LitStr = m.value()?.parse().map_err(|_| m.error(format!("expected `{key} = \"...\"`")))?;
86                if set.replace(lit).is_some() {
87                    return Err(Error::new(span, format!("duplicate `{key}`")));
88                }
89                Ok(())
90            };
91            match key.as_str() {
92                "summary" => value(&m, &mut o.summary),
93                "rename" => value(&m, &mut o.rename),
94                "len" => value(&m, &mut o.len),
95                "hide" => flag(&m, &mut o.hide),
96                _ => flag(&m, &mut o.items),
97            }
98        })?;
99    }
100    Ok(o)
101}
102
103fn misplaced(key: &str, place: Place) -> String {
104    match (key, place) {
105        ("summary", Place::Enum) => {
106            "`summary` on an enum goes on each variant: `#[debuggable(summary = \"...\")]` above the variant".into()
107        }
108        ("summary", _) => "`summary` is allowed on a struct or an enum variant, not on a field".into(),
109        ("items" | "len", Place::VariantField) => format!("`{key}` is not supported inside enum variants"),
110        (_, Place::Struct | Place::Enum | Place::Variant) => format!("`{key}` is allowed on fields only"),
111        _ => format!("`{key}` is not allowed here"),
112    }
113}
114
115// ---- Model building and validation ---------------------------------------------------------
116
117fn is_phantom(ty: &Type) -> bool {
118    matches!(ty, Type::Path(p) if p.qself.is_none() && p.path.segments.last().is_some_and(|s| s.ident == "PhantomData"))
119}
120
121/// Fields plus their options, validated as a group.
122fn fields(fields: &Fields, place: Place) -> Result<Vec<Field>> {
123    let mut out = Vec::new();
124    let mut items_seen = false;
125    for (i, f) in fields.iter().enumerate() {
126        let o = opts(&f.attrs, place)?;
127        if let (Some(_), Some(rename)) = (o.hide, &o.rename) {
128            return Err(Error::new(rename.span(), "a hidden field can't be renamed"));
129        }
130        if let (Some(_), Some(items)) = (o.hide, o.items) {
131            return Err(Error::new(items, "`items` already replaces the field with its elements; remove `hide`"));
132        }
133        if let (Some(len), None) = (&o.len, o.items) {
134            return Err(Error::new(len.span(), "`len` needs `items` on the same field"));
135        }
136        if let Some(items) = o.items {
137            if items_seen {
138                return Err(Error::new(items, "`items` may be used on only one field"));
139            }
140            items_seen = true;
141        }
142        if let Some(r) = &o.rename {
143            if r.value().is_empty() {
144                return Err(Error::new(r.span(), "`rename` needs a non-empty name"));
145            }
146        }
147        out.push(Field {
148            name: f.ident.as_ref().map(|id| id.unraw().to_string()).unwrap_or_else(|| i.to_string()),
149            hide: o.hide.is_some() || is_phantom(&f.ty),
150            rename: o.rename.map(|r| r.value()),
151            items: o.items.is_some(),
152            len: o.len.map(|l| (l.value(), l.span())),
153        });
154    }
155    for f in &out {
156        if let Some((len, span)) = &f.len {
157            if !out.iter().any(|g| &g.name == len) {
158                return Err(Error::new(*span, unknown_field(len, &out, "`len`")));
159            }
160        }
161    }
162    Ok(out)
163}
164
165/// The closest candidate for a "did you mean" hint. Like rustc, allow an edit distance of
166/// at most a third of the name's length, and never suggest a name with nothing in common.
167fn closest<'a>(name: &str, candidates: impl IntoIterator<Item = &'a str>) -> Option<&'a str> {
168    fn distance(a: &str, b: &str) -> usize {
169        let b: Vec<char> = b.chars().collect();
170        let mut row: Vec<usize> = (0..=b.len()).collect();
171        for (i, ca) in a.chars().enumerate() {
172            let mut prev = row[0];
173            row[0] = i + 1;
174            for j in 0..b.len() {
175                let cur = row[j + 1];
176                row[j + 1] = (prev + usize::from(ca != b[j])).min(row[j] + 1).min(cur + 1);
177                prev = cur;
178            }
179        }
180        row[b.len()]
181    }
182    let limit = (name.chars().count() / 3).max(1);
183    candidates
184        .into_iter()
185        .map(|c| (distance(name, c), c))
186        .filter(|&(d, _)| d <= limit && d < name.chars().count())
187        .min()
188        .map(|(_, c)| c)
189}
190
191fn unknown_field(name: &str, fields: &[Field], what: &str) -> String {
192    if fields.is_empty() {
193        return format!("{what} refers to `{name}`, but there are no fields");
194    }
195    if let Some(best) = closest(name, fields.iter().map(|f| f.name.as_str())) {
196        return format!("{what} refers to unknown field `{name}`; did you mean `{best}`?");
197    }
198    let names: Vec<String> = fields.iter().map(|f| format!("`{}`", f.name)).collect();
199    format!("{what} refers to unknown field `{name}`; available: {}", names.join(", "))
200}
201
202/// Parse a summary format string: `{field}` references, `{{`/`}}` escapes, literal text.
203fn summary(lit: &LitStr, fields: &[Field]) -> Result<Vec<Part>> {
204    let s = lit.value();
205    let err = |msg: String| Error::new(lit.span(), msg);
206    let mut parts = Vec::new();
207    let mut text = String::new();
208    let mut chars = s.chars().peekable();
209    while let Some(c) = chars.next() {
210        match c {
211            '{' if chars.peek() == Some(&'{') => {
212                chars.next();
213                text.push('{');
214            }
215            '}' if chars.peek() == Some(&'}') => {
216                chars.next();
217                text.push('}');
218            }
219            '{' => {
220                let mut name = String::new();
221                loop {
222                    match chars.next() {
223                        Some('}') => break,
224                        Some(c) if c.is_alphanumeric() || c == '_' => name.push(c),
225                        Some(':') => {
226                            return Err(err(format!(
227                                "format specs are not supported in summaries: use `{{{name}}}`"
228                            )))
229                        }
230                        Some(c) => {
231                            return Err(err(format!(
232                                "`{c}` in `{{{name}...}}`: summaries can only refer to fields by name, like `{{len}}` or `{{0}}`"
233                            )))
234                        }
235                        None => return Err(err("unclosed `{` in summary (write `{{` for a literal brace)".into())),
236                    }
237                }
238                if name.is_empty() {
239                    return Err(err("empty `{}` in summary: name a field, like `{len}`".into()));
240                }
241                let name = name.strip_prefix("r#").map(str::to_string).unwrap_or(name);
242                if !fields.iter().any(|f| f.name == name) {
243                    return Err(err(unknown_field(&name, fields, "summary")));
244                }
245                if !text.is_empty() {
246                    parts.push(Part::Lit(std::mem::take(&mut text)));
247                }
248                parts.push(Part::Field(name));
249            }
250            '}' => return Err(err("unmatched `}` in summary (write `}}` for a literal brace)".into())),
251            c => text.push(c),
252        }
253    }
254    if !text.is_empty() {
255        parts.push(Part::Lit(text));
256    }
257    Ok(parts)
258}
259
260fn model(input: &DeriveInput) -> Result<Ty> {
261    let generic = input.generics.params.iter().any(|p| !matches!(p, GenericParam::Lifetime(_)));
262    let (summary_lit, body) = match &input.data {
263        Data::Struct(s) => {
264            let o = opts(&input.attrs, Place::Struct)?;
265            let fs = fields(&s.fields, Place::StructField)?;
266            (o.summary, Body::Struct(fs))
267        }
268        Data::Enum(e) => {
269            opts(&input.attrs, Place::Enum)?;
270            let mut variants = Vec::new();
271            for v in &e.variants {
272                let o = opts(&v.attrs, Place::Variant)?;
273                let fs = fields(&v.fields, Place::VariantField)?;
274                let summary = o.summary.as_ref().map(|lit| summary(lit, &fs)).transpose()?;
275                variants.push(Variant { name: v.ident.unraw().to_string(), summary, fields: fs });
276            }
277            (None, Body::Enum(variants))
278        }
279        Data::Union(u) => {
280            return Err(Error::new(u.union_token.span, "`Debuggable` can't be derived for unions"));
281        }
282    };
283    let summary = match (&summary_lit, &body) {
284        (Some(lit), Body::Struct(fs)) => Some(summary(lit, fs)?),
285        _ => None,
286    };
287    Ok(Ty { name: input.ident.unraw().to_string(), generic, summary, body })
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    fn json(src: &str) -> String {
295        let input: DeriveInput = syn::parse_str(src).unwrap();
296        let call = emit::entry_call(&model(&input).unwrap());
297        // `::debuggable::__entry!("Name", "<json tail>");` -> the JSON tail, unescaped
298        let lit: LitStr = syn::parse_str(call.rsplit_once(", ").unwrap().1.trim_end_matches(");")).unwrap();
299        lit.value()
300    }
301
302    fn error(src: &str) -> String {
303        let input: DeriveInput = syn::parse_str(src).unwrap();
304        match model(&input) {
305            Ok(_) => panic!("expected an error for: {src}"),
306            Err(e) => e.to_string(),
307        }
308    }
309
310    #[test]
311    fn struct_with_everything() {
312        assert_eq!(
313            json(r#"#[debuggable(summary = "{len} items")] struct M<K, V> {
314                #[debuggable(items, len = "len")] slots: Vec<V>,
315                #[debuggable(hide)] free_head: u32,
316                len: u32,
317                _k: std::marker::PhantomData<K>,
318                #[debuggable(rename = "count")] n: u8,
319            }"#),
320            r#""generic":true,"kind":"struct","summary":[["field","len"],["lit"," items"]],"hide":["free_head","_k"],"rename":{"n":"count"},"items":{"field":"slots","len":"len"}}"#
321        );
322    }
323
324    #[test]
325    fn enum_variants_and_defaults() {
326        assert_eq!(
327            json(r#"enum Token { #[debuggable(summary = "Ident({name})")] Ident { name: String }, #[debuggable(summary = "Num({0})")] Num(i64), Eof }"#),
328            r#""generic":false,"kind":"enum","variants":{"Ident":{"summary":[["lit","Ident("],["field","name"],["lit",")"]]},"Num":{"summary":[["lit","Num("],["field","0"],["lit",")"]]}}}"#
329        );
330        assert_eq!(json("enum E { A, B }"), r#""generic":false,"kind":"enum","variants":{}}"#);
331    }
332
333    #[test]
334    fn lifetimes_alone_are_not_generic_and_raw_idents_are_unraw() {
335        assert_eq!(
336            json(r#"#[debuggable(summary = "{type}")] struct r#S<'a> { r#type: &'a str }"#),
337            r#""generic":false,"kind":"struct","summary":[["field","type"]]}"#
338        );
339    }
340
341    #[test]
342    fn json_is_ascii_and_quote_safe() {
343        // `bs` is a backslash, so the expected JSON escapes stay visible as escapes.
344        let bs = '\\';
345        assert_eq!(
346            json(r#"#[debuggable(summary = "{0}°C ''' \"q\" {{x}}")] struct C(f32);"#),
347            format!(r#""generic":false,"kind":"struct","summary":[["field","0"],["lit","{bs}u00b0C {bs}u0027{bs}u0027{bs}u0027 {bs}"q{bs}" {{x}}"]]}}"#)
348        );
349    }
350
351    #[test]
352    fn errors() {
353        let cases = [
354            (r#"#[debuggable(sumary = "x")] struct S;"#, "unknown `debuggable` option `sumary`; did you mean `summary`?"),
355            (r#"#[debuggable(colour = "x")] struct S;"#, "expected one of: `summary`"),
356            (r#"#[debuggable(summary = "{lenght}")] struct S { length: u8 }"#, "did you mean `length`?"),
357            (r#"#[debuggable(summary = "a", summary = "b")] struct S;"#, "duplicate `summary`"),
358            (r#"#[debuggable(summary = "x")] enum E { A }"#, "goes on each variant"),
359            (r#"#[debuggable(hide)] struct S { a: u8 }"#, "fields only"),
360            (r#"struct S { #[debuggable(summary = "x")] a: u8 }"#, "not on a field"),
361            (r#"enum E { A { #[debuggable(items)] v: Vec<u8> } }"#, "not supported inside enum variants"),
362            (r#"struct S { #[debuggable(len = "n")] v: Vec<u8>, n: usize }"#, "needs `items`"),
363            (r#"struct S { #[debuggable(items)] a: Vec<u8>, #[debuggable(items)] b: Vec<u8> }"#, "only one field"),
364            (r#"struct S { #[debuggable(items, len = "count")] v: Vec<u8>, n: usize }"#, "unknown field `count`; available: `v`, `n`"),
365            (r#"#[debuggable(summary = "{x}")] struct S { a: u8 }"#, "unknown field `x`; available: `a`"),
366            (r#"#[debuggable(summary = "{0}")] struct S { a: u8 }"#, "unknown field `0`"),
367            (r#"#[debuggable(summary = "{a:?}")] struct S { a: u8 }"#, "format specs are not supported"),
368            (r#"#[debuggable(summary = "{a.b}")] struct S { a: u8 }"#, "can only refer to fields by name"),
369            (r#"#[debuggable(summary = "{a")] struct S { a: u8 }"#, "unclosed `{`"),
370            (r#"#[debuggable(summary = "a}")] struct S { a: u8 }"#, "unmatched `}`"),
371            (r#"#[debuggable(summary = "{}")] struct S { a: u8 }"#, "empty `{}`"),
372            (r#"struct S { #[debuggable(hide, rename = "b")] a: u8 }"#, "hidden field can't be renamed"),
373            (r#"struct S { #[debuggable(hide, items)] a: Vec<u8> }"#, "remove `hide`"),
374            (r#"struct S { #[debuggable(hide = "yes")] a: u8 }"#, "takes no value"),
375            (r#"struct S { #[debuggable(rename = 3)] a: u8 }"#, "expected `rename = \"...\"`"),
376            (r#"struct S { #[debuggable(rename = "")] a: u8 }"#, "non-empty"),
377            (r#"union U { a: u8 }"#, "can't be derived for unions"),
378        ];
379        for (src, want) in cases {
380            let got = error(src);
381            assert!(got.contains(want), "for {src}\n  want: {want}\n  got:  {got}");
382        }
383    }
384}