1mod 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#[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#[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}
53
54fn opts(attrs: &[Attribute], place: Place) -> Result<Opts> {
55 let mut o = Opts::default();
56 for attr in attrs.iter().filter(|a| a.path().is_ident("debuggable")) {
57 attr.parse_nested_meta(|m| {
58 let key = m.path.get_ident().map(|i| i.to_string()).unwrap_or_default();
59 let span = m.path.span();
60 let allowed = match key.as_str() {
61 "summary" => matches!(place, Place::Struct | Place::Variant),
62 "hide" | "rename" => matches!(place, Place::StructField | Place::VariantField),
63 "items" | "text" | "len" => place == Place::StructField,
64 _ => {
65 const OPTIONS: [&str; 6] = ["summary", "hide", "rename", "items", "text", "len"];
66 return Err(m.error(match closest(&key, OPTIONS) {
67 Some(best) => format!("unknown `debuggable` option `{key}`; did you mean `{best}`?"),
68 None => "unknown `debuggable` option; expected one of: `summary`, `hide`, `rename`, `items`, `text`, `len`"
69 .to_string(),
70 }));
71 }
72 };
73 if !allowed {
74 return Err(Error::new(span, misplaced(&key, place)));
75 }
76 let flag = |m: &syn::meta::ParseNestedMeta, set: &mut Option<Span>| -> Result<()> {
77 if !m.input.is_empty() && !m.input.peek(syn::Token![,]) {
78 return Err(m.error(format!("`{key}` takes no value")));
79 }
80 if set.replace(span).is_some() {
81 return Err(Error::new(span, format!("duplicate `{key}`")));
82 }
83 Ok(())
84 };
85 let value = |m: &syn::meta::ParseNestedMeta, set: &mut Option<LitStr>| -> Result<()> {
86 let lit: LitStr = m.value()?.parse().map_err(|_| m.error(format!("expected `{key} = \"...\"`")))?;
87 if set.replace(lit).is_some() {
88 return Err(Error::new(span, format!("duplicate `{key}`")));
89 }
90 Ok(())
91 };
92 match key.as_str() {
93 "summary" => value(&m, &mut o.summary),
94 "rename" => value(&m, &mut o.rename),
95 "len" => value(&m, &mut o.len),
96 "hide" => flag(&m, &mut o.hide),
97 "text" => flag(&m, &mut o.text),
98 _ => flag(&m, &mut o.items),
99 }
100 })?;
101 }
102 Ok(o)
103}
104
105fn misplaced(key: &str, place: Place) -> String {
106 match (key, place) {
107 ("summary", Place::Enum) => {
108 "`summary` on an enum goes on each variant: `#[debuggable(summary = \"...\")]` above the variant".into()
109 }
110 ("summary", _) => "`summary` is allowed on a struct or an enum variant, not on a field".into(),
111 ("items" | "text" | "len", Place::VariantField) => format!("`{key}` is not supported inside enum variants"),
112 (_, Place::Struct | Place::Enum | Place::Variant) => format!("`{key}` is allowed on fields only"),
113 _ => format!("`{key}` is not allowed here"),
114 }
115}
116
117fn is_phantom(ty: &Type) -> bool {
120 matches!(ty, Type::Path(p) if p.qself.is_none() && p.path.segments.last().is_some_and(|s| s.ident == "PhantomData"))
121}
122
123fn fields(fields: &Fields, place: Place) -> Result<Vec<Field>> {
125 let mut out = Vec::new();
126 let mut items_seen = false;
127 for (i, f) in fields.iter().enumerate() {
128 let o = opts(&f.attrs, place)?;
129 if let (Some(_), Some(rename)) = (o.hide, &o.rename) {
130 return Err(Error::new(rename.span(), "a hidden field can't be renamed"));
131 }
132 if let (Some(_), Some(items)) = (o.hide, o.items) {
133 return Err(Error::new(items, "`items` already replaces the field with its elements; remove `hide`"));
134 }
135 if let (Some(_), Some(text)) = (o.items, o.text) {
136 return Err(Error::new(text, "a field can't have both `items` and `text`"));
137 }
138 if let (Some(len), None, None) = (&o.len, o.items, o.text) {
139 return Err(Error::new(len.span(), "`len` needs `items` or `text` on the same field"));
140 }
141 if let Some(items) = o.items {
142 if items_seen {
143 return Err(Error::new(items, "`items` may be used on only one field"));
144 }
145 items_seen = true;
146 }
147 if let Some(r) = &o.rename {
148 if r.value().is_empty() {
149 return Err(Error::new(r.span(), "`rename` needs a non-empty name"));
150 }
151 }
152 out.push(Field {
153 name: f.ident.as_ref().map(|id| id.unraw().to_string()).unwrap_or_else(|| i.to_string()),
154 hide: o.hide.is_some() || is_phantom(&f.ty),
155 rename: o.rename.map(|r| r.value()),
156 items: o.items.is_some(),
157 text: o.text.is_some(),
158 len: o.len.map(|l| (l.value(), l.span())),
159 });
160 }
161 for f in &out {
162 if let Some((len, span)) = &f.len {
163 if !out.iter().any(|g| &g.name == len) {
164 return Err(Error::new(*span, unknown_field(len, &out, "`len`")));
165 }
166 }
167 }
168 Ok(out)
169}
170
171fn closest<'a>(name: &str, candidates: impl IntoIterator<Item = &'a str>) -> Option<&'a str> {
174 fn distance(a: &str, b: &str) -> usize {
175 let b: Vec<char> = b.chars().collect();
176 let mut row: Vec<usize> = (0..=b.len()).collect();
177 for (i, ca) in a.chars().enumerate() {
178 let mut prev = row[0];
179 row[0] = i + 1;
180 for j in 0..b.len() {
181 let cur = row[j + 1];
182 row[j + 1] = (prev + usize::from(ca != b[j])).min(row[j] + 1).min(cur + 1);
183 prev = cur;
184 }
185 }
186 row[b.len()]
187 }
188 let limit = (name.chars().count() / 3).max(1);
189 candidates
190 .into_iter()
191 .map(|c| (distance(name, c), c))
192 .filter(|&(d, _)| d <= limit && d < name.chars().count())
193 .min()
194 .map(|(_, c)| c)
195}
196
197fn unknown_field(name: &str, fields: &[Field], what: &str) -> String {
198 if fields.is_empty() {
199 return format!("{what} refers to `{name}`, but there are no fields");
200 }
201 if let Some(best) = closest(name, fields.iter().map(|f| f.name.as_str())) {
202 return format!("{what} refers to unknown field `{name}`; did you mean `{best}`?");
203 }
204 let names: Vec<String> = fields.iter().map(|f| format!("`{}`", f.name)).collect();
205 format!("{what} refers to unknown field `{name}`; available: {}", names.join(", "))
206}
207
208fn summary(lit: &LitStr, fields: &[Field]) -> Result<Vec<Part>> {
210 let s = lit.value();
211 let err = |msg: String| Error::new(lit.span(), msg);
212 let mut parts = Vec::new();
213 let mut text = String::new();
214 let mut chars = s.chars().peekable();
215 while let Some(c) = chars.next() {
216 match c {
217 '{' if chars.peek() == Some(&'{') => {
218 chars.next();
219 text.push('{');
220 }
221 '}' if chars.peek() == Some(&'}') => {
222 chars.next();
223 text.push('}');
224 }
225 '{' => {
226 let mut name = String::new();
227 loop {
228 match chars.next() {
229 Some('}') => break,
230 Some(c) if c.is_alphanumeric() || c == '_' => name.push(c),
231 Some(':') => {
232 return Err(err(format!(
233 "format specs are not supported in summaries: use `{{{name}}}`"
234 )))
235 }
236 Some(c) => {
237 return Err(err(format!(
238 "`{c}` in `{{{name}...}}`: summaries can only refer to fields by name, like `{{len}}` or `{{0}}`"
239 )))
240 }
241 None => return Err(err("unclosed `{` in summary (write `{{` for a literal brace)".into())),
242 }
243 }
244 if name.is_empty() {
245 return Err(err("empty `{}` in summary: name a field, like `{len}`".into()));
246 }
247 let name = name.strip_prefix("r#").map(str::to_string).unwrap_or(name);
248 if !fields.iter().any(|f| f.name == name) {
249 return Err(err(unknown_field(&name, fields, "summary")));
250 }
251 if !text.is_empty() {
252 parts.push(Part::Lit(std::mem::take(&mut text)));
253 }
254 parts.push(Part::Field(name));
255 }
256 '}' => return Err(err("unmatched `}` in summary (write `}}` for a literal brace)".into())),
257 c => text.push(c),
258 }
259 }
260 if !text.is_empty() {
261 parts.push(Part::Lit(text));
262 }
263 Ok(parts)
264}
265
266fn model(input: &DeriveInput) -> Result<Ty> {
267 let generic = input.generics.params.iter().any(|p| !matches!(p, GenericParam::Lifetime(_)));
268 let (summary_lit, body) = match &input.data {
269 Data::Struct(s) => {
270 let o = opts(&input.attrs, Place::Struct)?;
271 let fs = fields(&s.fields, Place::StructField)?;
272 (o.summary, Body::Struct(fs))
273 }
274 Data::Enum(e) => {
275 opts(&input.attrs, Place::Enum)?;
276 let mut variants = Vec::new();
277 for v in &e.variants {
278 let o = opts(&v.attrs, Place::Variant)?;
279 let fs = fields(&v.fields, Place::VariantField)?;
280 let summary = o.summary.as_ref().map(|lit| summary(lit, &fs)).transpose()?;
281 variants.push(Variant { name: v.ident.unraw().to_string(), summary, fields: fs });
282 }
283 (None, Body::Enum(variants))
284 }
285 Data::Union(u) => {
286 return Err(Error::new(u.union_token.span, "`Debuggable` can't be derived for unions"));
287 }
288 };
289 let summary = match (&summary_lit, &body) {
290 (Some(lit), Body::Struct(fs)) => Some(summary(lit, fs)?),
291 _ => None,
292 };
293 Ok(Ty { name: input.ident.unraw().to_string(), generic, summary, body })
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299
300 fn json(src: &str) -> String {
301 let input: DeriveInput = syn::parse_str(src).unwrap();
302 let call = emit::entry_call(&model(&input).unwrap());
303 let lit: LitStr = syn::parse_str(call.rsplit_once(", ").unwrap().1.trim_end_matches(");")).unwrap();
305 lit.value()
306 }
307
308 fn error(src: &str) -> String {
309 let input: DeriveInput = syn::parse_str(src).unwrap();
310 match model(&input) {
311 Ok(_) => panic!("expected an error for: {src}"),
312 Err(e) => e.to_string(),
313 }
314 }
315
316 #[test]
317 fn struct_with_everything() {
318 assert_eq!(
319 json(r#"#[debuggable(summary = "{len} items")] struct M<K, V> {
320 #[debuggable(items, len = "len")] slots: Vec<V>,
321 #[debuggable(hide)] free_head: u32,
322 len: u32,
323 _k: std::marker::PhantomData<K>,
324 #[debuggable(rename = "count")] n: u8,
325 }"#),
326 r#""generic":true,"kind":"struct","summary":[["field","len"],["lit"," items"]],"hide":["free_head","_k"],"rename":{"n":"count"},"items":{"field":"slots","len":"len"}}"#
327 );
328 }
329
330 #[test]
331 fn text_fields() {
332 assert_eq!(
333 json(r#"#[debuggable(summary = "{xs}")] struct S<const N: usize> {
334 #[debuggable(hide)] len: u8,
335 #[debuggable(text, len = "len", hide)] xs: [u8; N],
336 #[debuggable(text)] tag: [u8; 4],
337 }"#),
338 r#""generic":true,"kind":"struct","summary":[["field","xs"]],"hide":["len","xs"],"text":[{"field":"xs","len":"len"},{"field":"tag"}]}"#
339 );
340 }
341
342 #[test]
343 fn enum_variants_and_defaults() {
344 assert_eq!(
345 json(r#"enum Token { #[debuggable(summary = "Ident({name})")] Ident { name: String }, #[debuggable(summary = "Num({0})")] Num(i64), Eof }"#),
346 r#""generic":false,"kind":"enum","variants":{"Ident":{"summary":[["lit","Ident("],["field","name"],["lit",")"]]},"Num":{"summary":[["lit","Num("],["field","0"],["lit",")"]]}}}"#
347 );
348 assert_eq!(json("enum E { A, B }"), r#""generic":false,"kind":"enum","variants":{}}"#);
349 }
350
351 #[test]
352 fn lifetimes_alone_are_not_generic_and_raw_idents_are_unraw() {
353 assert_eq!(
354 json(r#"#[debuggable(summary = "{type}")] struct r#S<'a> { r#type: &'a str }"#),
355 r#""generic":false,"kind":"struct","summary":[["field","type"]]}"#
356 );
357 }
358
359 #[test]
360 fn json_is_ascii_and_quote_safe() {
361 let bs = '\\';
363 assert_eq!(
364 json(r#"#[debuggable(summary = "{0}°C ''' \"q\" {{x}}")] struct C(f32);"#),
365 format!(r#""generic":false,"kind":"struct","summary":[["field","0"],["lit","{bs}u00b0C {bs}u0027{bs}u0027{bs}u0027 {bs}"q{bs}" {{x}}"]]}}"#)
366 );
367 }
368
369 #[test]
370 fn errors() {
371 let cases = [
372 (r#"#[debuggable(sumary = "x")] struct S;"#, "unknown `debuggable` option `sumary`; did you mean `summary`?"),
373 (r#"#[debuggable(colour = "x")] struct S;"#, "expected one of: `summary`"),
374 (r#"#[debuggable(summary = "{lenght}")] struct S { length: u8 }"#, "did you mean `length`?"),
375 (r#"#[debuggable(summary = "a", summary = "b")] struct S;"#, "duplicate `summary`"),
376 (r#"#[debuggable(summary = "x")] enum E { A }"#, "goes on each variant"),
377 (r#"#[debuggable(hide)] struct S { a: u8 }"#, "fields only"),
378 (r#"struct S { #[debuggable(summary = "x")] a: u8 }"#, "not on a field"),
379 (r#"enum E { A { #[debuggable(items)] v: Vec<u8> } }"#, "not supported inside enum variants"),
380 (r#"struct S { #[debuggable(len = "n")] v: Vec<u8>, n: usize }"#, "`len` needs `items` or `text`"),
381 (r#"struct S { #[debuggable(items, text)] v: Vec<u8> }"#, "both `items` and `text`"),
382 (r#"struct S { #[debuggable(text, text)] v: Vec<u8> }"#, "duplicate `text`"),
383 (r#"struct S { #[debuggable(txt)] v: Vec<u8> }"#, "did you mean `text`?"),
384 (r#"enum E { A { #[debuggable(text)] v: Vec<u8> } }"#, "`text` is not supported inside enum variants"),
385 (r#"struct S { #[debuggable(items)] a: Vec<u8>, #[debuggable(items)] b: Vec<u8> }"#, "only one field"),
386 (r#"struct S { #[debuggable(items, len = "count")] v: Vec<u8>, n: usize }"#, "unknown field `count`; available: `v`, `n`"),
387 (r#"#[debuggable(summary = "{x}")] struct S { a: u8 }"#, "unknown field `x`; available: `a`"),
388 (r#"#[debuggable(summary = "{0}")] struct S { a: u8 }"#, "unknown field `0`"),
389 (r#"#[debuggable(summary = "{a:?}")] struct S { a: u8 }"#, "format specs are not supported"),
390 (r#"#[debuggable(summary = "{a.b}")] struct S { a: u8 }"#, "can only refer to fields by name"),
391 (r#"#[debuggable(summary = "{a")] struct S { a: u8 }"#, "unclosed `{`"),
392 (r#"#[debuggable(summary = "a}")] struct S { a: u8 }"#, "unmatched `}`"),
393 (r#"#[debuggable(summary = "{}")] struct S { a: u8 }"#, "empty `{}`"),
394 (r#"struct S { #[debuggable(hide, rename = "b")] a: u8 }"#, "hidden field can't be renamed"),
395 (r#"struct S { #[debuggable(hide, items)] a: Vec<u8> }"#, "remove `hide`"),
396 (r#"struct S { #[debuggable(hide = "yes")] a: u8 }"#, "takes no value"),
397 (r#"struct S { #[debuggable(rename = 3)] a: u8 }"#, "expected `rename = \"...\"`"),
398 (r#"struct S { #[debuggable(rename = "")] a: u8 }"#, "non-empty"),
399 (r#"union U { a: u8 }"#, "can't be derived for unions"),
400 ];
401 for (src, want) in cases {
402 let got = error(src);
403 assert!(got.contains(want), "for {src}\n want: {want}\n got: {got}");
404 }
405 }
406}