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, Only, 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    text: Option<Span>,
51    len: Option<LitStr>,
52    only: Option<LitStr>,
53    value: Option<LitStr>,
54}
55
56fn opts(attrs: &[Attribute], place: Place) -> Result<Opts> {
57    let mut o = Opts::default();
58    for attr in attrs.iter().filter(|a| a.path().is_ident("debuggable")) {
59        attr.parse_nested_meta(|m| {
60            let key = m.path.get_ident().map(|i| i.to_string()).unwrap_or_default();
61            let span = m.path.span();
62            let allowed = match key.as_str() {
63                "summary" => matches!(place, Place::Struct | Place::Variant),
64                "hide" | "rename" => matches!(place, Place::StructField | Place::VariantField),
65                "items" | "text" | "len" | "only" | "value" => place == Place::StructField,
66                _ => {
67                    const OPTIONS: [&str; 8] = ["summary", "hide", "rename", "items", "text", "len", "only", "value"];
68                    return Err(m.error(match closest(&key, OPTIONS) {
69                        Some(best) => format!("unknown `debuggable` option `{key}`; did you mean `{best}`?"),
70                        None => "unknown `debuggable` option; expected one of: `summary`, `hide`, `rename`, `items`, `text`, `len`, `only`, `value`"
71                            .to_string(),
72                    }));
73                }
74            };
75            if !allowed {
76                return Err(Error::new(span, misplaced(&key, place)));
77            }
78            let flag = |m: &syn::meta::ParseNestedMeta, set: &mut Option<Span>| -> Result<()> {
79                if !m.input.is_empty() && !m.input.peek(syn::Token![,]) {
80                    return Err(m.error(format!("`{key}` takes no value")));
81                }
82                if set.replace(span).is_some() {
83                    return Err(Error::new(span, format!("duplicate `{key}`")));
84                }
85                Ok(())
86            };
87            let value = |m: &syn::meta::ParseNestedMeta, set: &mut Option<LitStr>| -> Result<()> {
88                let lit: LitStr = m.value()?.parse().map_err(|_| m.error(format!("expected `{key} = \"...\"`")))?;
89                if set.replace(lit).is_some() {
90                    return Err(Error::new(span, format!("duplicate `{key}`")));
91                }
92                Ok(())
93            };
94            match key.as_str() {
95                "summary" => value(&m, &mut o.summary),
96                "rename" => value(&m, &mut o.rename),
97                "len" => value(&m, &mut o.len),
98                "only" => value(&m, &mut o.only),
99                "value" => value(&m, &mut o.value),
100                "hide" => flag(&m, &mut o.hide),
101                "text" => flag(&m, &mut o.text),
102                _ => flag(&m, &mut o.items),
103            }
104        })?;
105    }
106    Ok(o)
107}
108
109fn misplaced(key: &str, place: Place) -> String {
110    match (key, place) {
111        ("summary", Place::Enum) => {
112            "`summary` on an enum goes on each variant: `#[debuggable(summary = \"...\")]` above the variant".into()
113        }
114        ("summary", _) => "`summary` is allowed on a struct or an enum variant, not on a field".into(),
115        ("items" | "text" | "len" | "only" | "value", Place::VariantField) => format!("`{key}` is not supported inside enum variants"),
116        (_, Place::Struct | Place::Enum | Place::Variant) => format!("`{key}` is allowed on fields only"),
117        _ => format!("`{key}` is not allowed here"),
118    }
119}
120
121// ---- Model building and validation ---------------------------------------------------------
122
123fn is_phantom(ty: &Type) -> bool {
124    matches!(ty, Type::Path(p) if p.qself.is_none() && p.path.segments.last().is_some_and(|s| s.ident == "PhantomData"))
125}
126
127/// Fields plus their options, validated as a group.
128fn fields(fields: &Fields, place: Place) -> Result<Vec<Field>> {
129    let mut out = Vec::new();
130    let mut items_seen = false;
131    for (i, f) in fields.iter().enumerate() {
132        let o = opts(&f.attrs, place)?;
133        if let (Some(_), Some(rename)) = (o.hide, &o.rename) {
134            return Err(Error::new(rename.span(), "a hidden field can't be renamed"));
135        }
136        if let (Some(_), Some(items)) = (o.hide, o.items) {
137            return Err(Error::new(items, "`items` already replaces the field with its elements; remove `hide`"));
138        }
139        if let (Some(_), Some(text)) = (o.items, o.text) {
140            return Err(Error::new(text, "a field can't have both `items` and `text`"));
141        }
142        for (lit, key) in [(&o.only, "only"), (&o.value, "value")] {
143            if let (Some(lit), None) = (lit, o.items) {
144                return Err(Error::new(lit.span(), format!("`{key}` needs `items` on the same field")));
145            }
146        }
147        if let (Some(len), None, None) = (&o.len, o.items, o.text) {
148            return Err(Error::new(len.span(), "`len` needs `items` or `text` on the same field"));
149        }
150        if let Some(items) = o.items {
151            if items_seen {
152                return Err(Error::new(items, "`items` may be used on only one field"));
153            }
154            items_seen = true;
155        }
156        if let Some(r) = &o.rename {
157            if r.value().is_empty() {
158                return Err(Error::new(r.span(), "`rename` needs a non-empty name"));
159            }
160        }
161        out.push(Field {
162            name: f.ident.as_ref().map(|id| id.unraw().to_string()).unwrap_or_else(|| i.to_string()),
163            hide: o.hide.is_some() || is_phantom(&f.ty),
164            rename: o.rename.map(|r| r.value()),
165            items: o.items.is_some(),
166            text: o.text.is_some(),
167            only: o.only.as_ref().map(parse_only).transpose()?,
168            value: o.value.as_ref().map(|v| parse_path(v, "value")).transpose()?,
169            len: o.len.map(|l| (l.value(), l.span())),
170        });
171    }
172    for f in &out {
173        if let Some((len, span)) = &f.len {
174            if !out.iter().any(|g| &g.name == len) {
175                return Err(Error::new(*span, unknown_field(len, &out, "`len`")));
176            }
177        }
178    }
179    Ok(out)
180}
181
182/// `a.b.0`: field names (or tuple indices) separated by dots (design 0002 §2.1).
183fn parse_path(lit: &LitStr, what: &str) -> Result<Vec<String>> {
184    parse_path_str(&lit.value(), lit, what)
185}
186
187fn parse_path_str(s: &str, lit: &LitStr, what: &str) -> Result<Vec<String>> {
188    let err = |msg: String| Error::new(lit.span(), msg);
189    if s.trim().is_empty() {
190        return Err(err(format!("`{what}` expects a field or variant name, like `\"value\"` or `\"u.value\"`")));
191    }
192    let mut out = Vec::new();
193    for seg in s.split('.') {
194        let seg = seg.trim();
195        if seg.is_empty() {
196            return Err(err(format!("empty segment in `{what}` path `{s}`")));
197        }
198        if let Some((first, rest)) = seg.split_once(char::is_whitespace) {
199            let _ = first;
200            return Err(err(format!("unexpected `{}` in `{what}`", rest.trim())));
201        }
202        let seg = seg.strip_prefix("r#").unwrap_or(seg);
203        let is_ident = syn::parse::Parser::parse_str(syn::Ident::parse_any, seg).is_ok();
204        if !(is_ident || seg.bytes().all(|b| b.is_ascii_digit())) {
205            return Err(err(format!("`{seg}` in `{what}` is not a field name")));
206        }
207        out.push(seg.to_string());
208    }
209    Ok(out)
210}
211
212/// `path` or `path & mask` (design 0002 §2.1).
213fn parse_only(lit: &LitStr) -> Result<Only> {
214    let s = lit.value();
215    let err = |msg: String| Error::new(lit.span(), msg);
216    let (path, mask) = match s.split_once('&') {
217        None => (s.as_str(), None),
218        Some((path, mask)) => {
219            let mask = mask.trim();
220            if mask.is_empty() {
221                return Err(err("`only` expects a mask after `&`, like `\"version & 1\"`".into()));
222            }
223            let parsed = match mask.strip_prefix("0x").or_else(|| mask.strip_prefix("0X")) {
224                Some(hex) => u64::from_str_radix(&hex.replace('_', ""), 16),
225                None => mask.replace('_', "").parse::<u64>(),
226            };
227            let n = parsed.map_err(|_| err(format!("`only` mask `{mask}` is not an integer")))?;
228            if n == 0 {
229                return Err(err("`only` mask must be non-zero".into()));
230            }
231            if n > (1 << 53) - 1 {
232                return Err(err("`only` mask must be at most 2^53 - 1".into()));
233            }
234            (path, Some(n))
235        }
236    };
237    if path.trim().is_empty() {
238        return Err(err("`only` expects a field or variant name, optionally `& mask`: `\"version & 1\"`".into()));
239    }
240    Ok(Only { path: parse_path_str(path, lit, "only")?, mask })
241}
242
243/// The closest candidate for a "did you mean" hint. Like rustc, allow an edit distance of
244/// at most a third of the name's length, and never suggest a name with nothing in common.
245fn closest<'a>(name: &str, candidates: impl IntoIterator<Item = &'a str>) -> Option<&'a str> {
246    /// Edit distance where swapping two adjacent characters counts as one edit, as in rustc
247    /// (optimal string alignment).
248    fn distance(a: &str, b: &str) -> usize {
249        let (a, b): (Vec<char>, Vec<char>) = (a.chars().collect(), b.chars().collect());
250        let mut d: Vec<Vec<usize>> = (0..=a.len()).map(|i| vec![i; b.len() + 1]).collect();
251        d[0] = (0..=b.len()).collect();
252        for i in 1..=a.len() {
253            for j in 1..=b.len() {
254                let cost = usize::from(a[i - 1] != b[j - 1]);
255                d[i][j] = (d[i - 1][j - 1] + cost).min(d[i - 1][j] + 1).min(d[i][j - 1] + 1);
256                if i > 1 && j > 1 && a[i - 1] == b[j - 2] && a[i - 2] == b[j - 1] {
257                    d[i][j] = d[i][j].min(d[i - 2][j - 2] + 1);
258                }
259            }
260        }
261        d[a.len()][b.len()]
262    }
263    let limit = (name.chars().count() / 3).max(1);
264    candidates
265        .into_iter()
266        .map(|c| (distance(name, c), c))
267        .filter(|&(d, _)| d <= limit && d < name.chars().count())
268        .min()
269        .map(|(_, c)| c)
270}
271
272fn unknown_field(name: &str, fields: &[Field], what: &str) -> String {
273    if fields.is_empty() {
274        return format!("{what} refers to `{name}`, but there are no fields");
275    }
276    if let Some(best) = closest(name, fields.iter().map(|f| f.name.as_str())) {
277        return format!("{what} refers to unknown field `{name}`; did you mean `{best}`?");
278    }
279    let names: Vec<String> = fields.iter().map(|f| format!("`{}`", f.name)).collect();
280    format!("{what} refers to unknown field `{name}`; available: {}", names.join(", "))
281}
282
283/// Parse a summary format string: `{field}` references, `{{`/`}}` escapes, literal text.
284fn summary(lit: &LitStr, fields: &[Field]) -> Result<Vec<Part>> {
285    let s = lit.value();
286    let err = |msg: String| Error::new(lit.span(), msg);
287    let mut parts = Vec::new();
288    let mut text = String::new();
289    let mut chars = s.chars().peekable();
290    while let Some(c) = chars.next() {
291        match c {
292            '{' if chars.peek() == Some(&'{') => {
293                chars.next();
294                text.push('{');
295            }
296            '}' if chars.peek() == Some(&'}') => {
297                chars.next();
298                text.push('}');
299            }
300            '{' => {
301                let mut name = String::new();
302                loop {
303                    match chars.next() {
304                        Some('}') => break,
305                        Some(c) if c.is_alphanumeric() || c == '_' => name.push(c),
306                        Some(':') => {
307                            return Err(err(format!(
308                                "format specs are not supported in summaries: use `{{{name}}}`"
309                            )))
310                        }
311                        Some(c) => {
312                            return Err(err(format!(
313                                "`{c}` in `{{{name}...}}`: summaries can only refer to fields by name, like `{{len}}` or `{{0}}`"
314                            )))
315                        }
316                        None => return Err(err("unclosed `{` in summary (write `{{` for a literal brace)".into())),
317                    }
318                }
319                if name.is_empty() {
320                    return Err(err("empty `{}` in summary: name a field, like `{len}`".into()));
321                }
322                let name = name.strip_prefix("r#").map(str::to_string).unwrap_or(name);
323                if !fields.iter().any(|f| f.name == name) {
324                    return Err(err(unknown_field(&name, fields, "summary")));
325                }
326                if !text.is_empty() {
327                    parts.push(Part::Lit(std::mem::take(&mut text)));
328                }
329                parts.push(Part::Field(name));
330            }
331            '}' => return Err(err("unmatched `}` in summary (write `}}` for a literal brace)".into())),
332            c => text.push(c),
333        }
334    }
335    if !text.is_empty() {
336        parts.push(Part::Lit(text));
337    }
338    Ok(parts)
339}
340
341fn model(input: &DeriveInput) -> Result<Ty> {
342    let generic = input.generics.params.iter().any(|p| !matches!(p, GenericParam::Lifetime(_)));
343    let (summary_lit, body) = match &input.data {
344        Data::Struct(s) => {
345            let o = opts(&input.attrs, Place::Struct)?;
346            let fs = fields(&s.fields, Place::StructField)?;
347            (o.summary, Body::Struct(fs))
348        }
349        Data::Enum(e) => {
350            opts(&input.attrs, Place::Enum)?;
351            let mut variants = Vec::new();
352            for v in &e.variants {
353                let o = opts(&v.attrs, Place::Variant)?;
354                let fs = fields(&v.fields, Place::VariantField)?;
355                let summary = o.summary.as_ref().map(|lit| summary(lit, &fs)).transpose()?;
356                variants.push(Variant { name: v.ident.unraw().to_string(), summary, fields: fs });
357            }
358            (None, Body::Enum(variants))
359        }
360        Data::Union(u) => {
361            return Err(Error::new(u.union_token.span, "`Debuggable` can't be derived for unions"));
362        }
363    };
364    let summary = match (&summary_lit, &body) {
365        (Some(lit), Body::Struct(fs)) => Some(summary(lit, fs)?),
366        _ => None,
367    };
368    Ok(Ty { name: input.ident.unraw().to_string(), generic, summary, body })
369}
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374
375    fn json(src: &str) -> String {
376        let input: DeriveInput = syn::parse_str(src).unwrap();
377        let call = emit::entry_call(&model(&input).unwrap());
378        // `::debuggable::__entry!("Name", "<json tail>");` -> the JSON tail, unescaped
379        let lit: LitStr = syn::parse_str(call.rsplit_once(", ").unwrap().1.trim_end_matches(");")).unwrap();
380        lit.value()
381    }
382
383    fn error(src: &str) -> String {
384        let input: DeriveInput = syn::parse_str(src).unwrap();
385        match model(&input) {
386            Ok(_) => panic!("expected an error for: {src}"),
387            Err(e) => e.to_string(),
388        }
389    }
390
391    #[test]
392    fn struct_with_everything() {
393        assert_eq!(
394            json(r#"#[debuggable(summary = "{len} items")] struct M<K, V> {
395                #[debuggable(items, len = "len")] slots: Vec<V>,
396                #[debuggable(hide)] free_head: u32,
397                len: u32,
398                _k: std::marker::PhantomData<K>,
399                #[debuggable(rename = "count")] n: u8,
400            }"#),
401            r#""generic":true,"kind":"struct","summary":[["field","len"],["lit"," items"]],"hide":["free_head","_k"],"rename":{"n":"count"},"items":{"field":"slots","len":"len"}}"#
402        );
403    }
404
405    #[test]
406    fn text_fields() {
407        assert_eq!(
408            json(r#"#[debuggable(summary = "{xs}")] struct S<const N: usize> {
409                #[debuggable(hide)] len: u8,
410                #[debuggable(text, len = "len", hide)] xs: [u8; N],
411                #[debuggable(text)] tag: [u8; 4],
412            }"#),
413            r#""generic":true,"kind":"struct","summary":[["field","xs"]],"hide":["len","xs"],"text":[{"field":"xs","len":"len"},{"field":"tag"}]}"#
414        );
415    }
416
417    #[test]
418    fn slots_fields() {
419        assert_eq!(
420            json(r#"#[debuggable(summary = "{len} items")] struct S<T> {
421                #[debuggable(items, len = "n", only = "version & 0x1", value = "u.r#value")] slots: Vec<T>,
422                n: usize, len: u32,
423            }"#),
424            r#""generic":true,"kind":"struct","summary":[["field","len"],["lit"," items"]],"slots":{"field":"slots","len":"n","only":{"path":["version"],"mask":1},"value":["u","value"]}}"#
425        );
426        assert_eq!(
427            json(r#"struct S { #[debuggable(items, only = "Occupied", value = "0")] e: Vec<u8> }"#),
428            r#""generic":false,"kind":"struct","slots":{"field":"e","only":{"path":["Occupied"]},"value":["0"]}}"#
429        );
430        assert_eq!(
431            // plain items: unchanged
432            json(r#"struct S { #[debuggable(items)] e: Vec<u8> }"#),
433            r#""generic":false,"kind":"struct","items":{"field":"e"}}"#
434        );
435    }
436
437    #[test]
438    fn enum_variants_and_defaults() {
439        assert_eq!(
440            json(r#"enum Token { #[debuggable(summary = "Ident({name})")] Ident { name: String }, #[debuggable(summary = "Num({0})")] Num(i64), Eof }"#),
441            r#""generic":false,"kind":"enum","variants":{"Ident":{"summary":[["lit","Ident("],["field","name"],["lit",")"]]},"Num":{"summary":[["lit","Num("],["field","0"],["lit",")"]]}}}"#
442        );
443        assert_eq!(json("enum E { A, B }"), r#""generic":false,"kind":"enum","variants":{}}"#);
444    }
445
446    #[test]
447    fn lifetimes_alone_are_not_generic_and_raw_idents_are_unraw() {
448        assert_eq!(
449            json(r#"#[debuggable(summary = "{type}")] struct r#S<'a> { r#type: &'a str }"#),
450            r#""generic":false,"kind":"struct","summary":[["field","type"]]}"#
451        );
452    }
453
454    #[test]
455    fn json_is_ascii_and_quote_safe() {
456        // `bs` is a backslash, so the expected JSON escapes stay visible as escapes.
457        let bs = '\\';
458        assert_eq!(
459            json(r#"#[debuggable(summary = "{0}°C ''' \"q\" {{x}}")] struct C(f32);"#),
460            format!(r#""generic":false,"kind":"struct","summary":[["field","0"],["lit","{bs}u00b0C {bs}u0027{bs}u0027{bs}u0027 {bs}"q{bs}" {{x}}"]]}}"#)
461        );
462    }
463
464    #[test]
465    fn errors() {
466        let cases = [
467            (r#"#[debuggable(sumary = "x")] struct S;"#, "unknown `debuggable` option `sumary`; did you mean `summary`?"),
468            (r#"#[debuggable(colour = "x")] struct S;"#, "expected one of: `summary`"),
469            (r#"#[debuggable(summary = "{lenght}")] struct S { length: u8 }"#, "did you mean `length`?"),
470            (r#"#[debuggable(summary = "a", summary = "b")] struct S;"#, "duplicate `summary`"),
471            (r#"#[debuggable(summary = "x")] enum E { A }"#, "goes on each variant"),
472            (r#"#[debuggable(hide)] struct S { a: u8 }"#, "fields only"),
473            (r#"struct S { #[debuggable(summary = "x")] a: u8 }"#, "not on a field"),
474            (r#"enum E { A { #[debuggable(items)] v: Vec<u8> } }"#, "not supported inside enum variants"),
475            (r#"struct S { #[debuggable(len = "n")] v: Vec<u8>, n: usize }"#, "`len` needs `items` or `text`"),
476            (r#"struct S { #[debuggable(items, text)] v: Vec<u8> }"#, "both `items` and `text`"),
477            (r#"struct S { #[debuggable(text, text)] v: Vec<u8> }"#, "duplicate `text`"),
478            (r#"struct S { #[debuggable(txt)] v: Vec<u8> }"#, "did you mean `text`?"),
479            (r#"enum E { A { #[debuggable(text)] v: Vec<u8> } }"#, "`text` is not supported inside enum variants"),
480            (r#"struct S { #[debuggable(items)] a: Vec<u8>, #[debuggable(items)] b: Vec<u8> }"#, "only one field"),
481            (r#"struct S { #[debuggable(items, len = "count")] v: Vec<u8>, n: usize }"#, "unknown field `count`; available: `v`, `n`"),
482            (r#"#[debuggable(summary = "{x}")] struct S { a: u8 }"#, "unknown field `x`; available: `a`"),
483            (r#"#[debuggable(summary = "{0}")] struct S { a: u8 }"#, "unknown field `0`"),
484            (r#"#[debuggable(summary = "{a:?}")] struct S { a: u8 }"#, "format specs are not supported"),
485            (r#"#[debuggable(summary = "{a.b}")] struct S { a: u8 }"#, "can only refer to fields by name"),
486            (r#"#[debuggable(summary = "{a")] struct S { a: u8 }"#, "unclosed `{`"),
487            (r#"#[debuggable(summary = "a}")] struct S { a: u8 }"#, "unmatched `}`"),
488            (r#"#[debuggable(summary = "{}")] struct S { a: u8 }"#, "empty `{}`"),
489            (r#"struct S { #[debuggable(hide, rename = "b")] a: u8 }"#, "hidden field can't be renamed"),
490            (r#"struct S { #[debuggable(hide, items)] a: Vec<u8> }"#, "remove `hide`"),
491            (r#"struct S { #[debuggable(hide = "yes")] a: u8 }"#, "takes no value"),
492            (r#"struct S { #[debuggable(rename = 3)] a: u8 }"#, "expected `rename = \"...\"`"),
493            (r#"struct S { #[debuggable(rename = "")] a: u8 }"#, "non-empty"),
494            (r#"union U { a: u8 }"#, "can't be derived for unions"),
495            (r#"struct S { #[debuggable(only = "x")] v: Vec<u8> }"#, "`only` needs `items`"),
496            (r#"struct S { #[debuggable(value = "x")] v: Vec<u8> }"#, "`value` needs `items`"),
497            (r#"struct S { #[debuggable(items, only = "")] v: Vec<u8> }"#, "`only` expects a field or variant name"),
498            (r#"struct S { #[debuggable(items, only = "a..b")] v: Vec<u8> }"#, "empty segment"),
499            (r#"struct S { #[debuggable(items, only = "a & 0")] v: Vec<u8> }"#, "mask must be non-zero"),
500            (r#"struct S { #[debuggable(items, only = "a & 9007199254740992")] v: Vec<u8> }"#, "at most 2^53 - 1"),
501            (r#"struct S { #[debuggable(items, only = "a &")] v: Vec<u8> }"#, "expects a mask"),
502            (r#"struct S { #[debuggable(items, only = "a b")] v: Vec<u8> }"#, "unexpected `b`"),
503            (r#"struct S { #[debuggable(items, value = "a.-b")] v: Vec<u8> }"#, "not a field name"),
504            (r#"struct S { #[debuggable(items, only = "a", only = "b")] v: Vec<u8> }"#, "duplicate `only`"),
505            (r#"struct S { #[debuggable(items, onyl = "x")] v: Vec<u8> }"#, "did you mean `only`?"),
506        ];
507        for (src, want) in cases {
508            let got = error(src);
509            assert!(got.contains(want), "for {src}\n  want: {want}\n  got:  {got}");
510        }
511    }
512}