codama_syn_helpers/
meta.rs

1use crate::extensions::*;
2use derive_more::From;
3use proc_macro2::{Delimiter, Group, TokenStream, TokenTree};
4use quote::{ToTokens, TokenStreamExt};
5use std::fmt::Display;
6use syn::{
7    ext::IdentExt,
8    parse::discouraged::Speculative,
9    token::{Brace, Bracket, Paren},
10    Expr, MacroDelimiter, MetaList, Path, Token,
11};
12
13#[derive(Debug, From)]
14pub enum Meta {
15    /// A path followed by an equal sign and a single Meta — e.g. `my_attribute = my_value`.
16    PathValue(PathValue),
17    /// A path followed by a wrapped list of Metas — e.g. `my_attribute(one, two, three)`.
18    /// This accepts an optional equal sign before the list — e.g. `my_attribute = (one, two, three)`.
19    PathList(PathList),
20    /// An expression — e.g. `my_attribute`, `42`, `"hello"` or `a + b`.
21    Expr(Expr),
22    /// A verbatim token stream — e.g. `[<=>]`.
23    /// This is the fallback for any other Meta that does not match the other variants.
24    Verbatim(TokenStream),
25}
26
27#[derive(Debug)]
28pub struct PathValue {
29    pub path: Path,
30    pub eq_token: Token![=],
31    pub value: Box<Meta>,
32}
33
34#[derive(Debug)]
35pub struct PathList {
36    pub path: Path,
37    pub eq_token: Option<Token![=]>,
38    pub delimiter: MacroDelimiter,
39    pub tokens: TokenStream,
40}
41
42impl Meta {
43    pub fn path(&self) -> syn::Result<&Path> {
44        match self {
45            Meta::PathList(pl) => Ok(&pl.path),
46            Meta::PathValue(pv) => Ok(&pv.path),
47            Meta::Expr(expr) => match expr {
48                Expr::Path(expr) => Ok(&expr.path),
49                _ => Err(expr.error("expected a path")),
50            },
51            Meta::Verbatim(tokens) => Err(tokens.error("expected a path")),
52        }
53    }
54
55    /// Returns the path as a string if it is a valid path
56    /// or an empty string if it is not a path.
57    pub fn path_str(&self) -> String {
58        self.path().map_or("".into(), |path| path.to_string())
59    }
60
61    pub fn is_path_or_list(&self) -> bool {
62        matches!(self, Meta::PathList(_) | Meta::Expr(Expr::Path(_)))
63    }
64
65    pub fn is_path_or_empty_list(&self) -> bool {
66        match self {
67            Meta::PathList(pl) if pl.tokens.is_empty() => true,
68            Meta::Expr(Expr::Path(_)) => true,
69            _ => false,
70        }
71    }
72
73    pub fn as_path(&self) -> syn::Result<&Path> {
74        match self {
75            Meta::Expr(Expr::Path(expr)) => Ok(&expr.path),
76            Meta::PathList(pl) => Err(pl
77                .tokens_after_path()
78                .error("unexpected tokens, expected a single path")),
79            Meta::PathValue(pv) => Err(pv
80                .tokens_after_path()
81                .error("unexpected tokens, expected a single path")),
82            meta => Err(meta.error("expected a path")),
83        }
84    }
85
86    pub fn as_path_list(&self) -> syn::Result<&PathList> {
87        match self {
88            Meta::PathList(pl) => Ok(pl),
89            Meta::PathValue(pv) => Err(pv.value.error(format!(
90                "expected a list: `{} = (...)`",
91                pv.path.to_string()
92            ))),
93            Meta::Expr(Expr::Path(expr)) => Err(expr.error(format!(
94                "expected a list: `{0}(...)` or `{0} = (...)`",
95                expr.path.to_string()
96            ))),
97            meta => Err(meta
98                .error("expected a path followed by a list: `my_path(...)` or `my_path = (...)`")),
99        }
100    }
101
102    pub fn as_path_value(&self) -> syn::Result<&PathValue> {
103        match self {
104            Meta::PathValue(pv) => Ok(pv),
105            Meta::PathList(pl) => match pl.eq_token {
106                Some(_) => Err(pl.tokens.error("expected a single value, found a list")),
107                None => Err(pl.tokens.error(format!(
108                    "expected `=` followed by a single value: `{} = ...`",
109                    pl.path.to_string(),
110                ))),
111            },
112            Meta::Expr(Expr::Path(expr)) => Err(expr.error(format!(
113                "expected a value for that path: `{} = ...`",
114                expr.path.to_string()
115            ))),
116            meta => Err(meta.error("expected a path followed by a value: `my_path = ...`")),
117        }
118    }
119
120    pub fn as_value(&self) -> syn::Result<&Meta> {
121        self.as_path_value().map(|pv| pv.value.as_ref())
122    }
123
124    pub fn as_expr_or_value_expr(&self) -> syn::Result<&Expr> {
125        self.as_expr().or_else(|_| self.as_value()?.as_expr())
126    }
127
128    pub fn as_expr(&self) -> syn::Result<&Expr> {
129        match self {
130            Meta::Expr(expr) => Ok(expr),
131            meta => Err(meta.error("expected a valid expression")),
132        }
133    }
134
135    pub fn as_verbatim(&self, msg: impl Display) -> syn::Result<&TokenStream> {
136        match self {
137            Meta::Verbatim(tokens) => Ok(tokens),
138            meta => Err(meta.error(msg)),
139        }
140    }
141
142    pub fn assert_directive(&self, directive: &str) -> syn::Result<&Self> {
143        let path = self.path()?;
144        if !path.is_strict(directive) {
145            return Err(path.error(format!("expected #[codama({directive})] attribute")));
146        };
147        Ok(self)
148    }
149}
150
151impl PathValue {
152    /// Get the equal sign and value tokens.
153    pub fn tokens_after_path(&self) -> TokenStream {
154        let mut tokens = TokenStream::new();
155        self.eq_token.to_tokens(&mut tokens);
156        self.value.to_tokens(&mut tokens);
157        tokens
158    }
159}
160
161impl PathList {
162    /// Get all tokens after the path, including the equal sign if present.
163    pub fn tokens_after_path(&self) -> TokenStream {
164        let mut tokens = TokenStream::new();
165        self.eq_token.to_tokens(&mut tokens);
166        delimiters_to_tokens(&self.delimiter, &self.tokens, &mut tokens);
167        tokens
168    }
169
170    /// Get an equivalent `MetaList` from the path list.
171    pub fn as_meta_list(&self) -> MetaList {
172        MetaList {
173            path: self.path.clone(),
174            delimiter: self.delimiter.clone(),
175            tokens: self.tokens.clone(),
176        }
177    }
178
179    /// Iterate over all metas in the list.
180    pub fn each(&self, logic: impl FnMut(Meta) -> syn::Result<()>) -> syn::Result<()> {
181        self.as_meta_list().each(logic)
182    }
183
184    /// Parse all metas in the list.
185    pub fn parse_metas(&self) -> syn::Result<Vec<Meta>> {
186        self.as_meta_list().parse_metas()
187    }
188
189    /// Parse all arguments as comma-separated types.
190    pub fn parse_comma_args<T: syn::parse::Parse>(&self) -> syn::Result<Vec<T>> {
191        self.as_meta_list().parse_comma_args()
192    }
193
194    pub fn as_expr_array(&self) -> syn::Result<syn::ExprArray> {
195        let syn::MacroDelimiter::Bracket(bracket_token) = self.delimiter else {
196            return Err(self.error("expected an array delimited with `[]`"));
197        };
198
199        Ok(syn::ExprArray {
200            attrs: vec![],
201            bracket_token,
202            elems: self.as_meta_list().parse_args_with(
203                syn::punctuated::Punctuated::<Expr, syn::Token![,]>::parse_terminated,
204            )?,
205        })
206    }
207}
208
209impl syn::parse::Parse for Meta {
210    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
211        let fork = input.fork();
212        match fork.call(parse_meta_path) {
213            Ok(path) => {
214                if fork.peek(Paren)
215                    || fork.peek(Bracket)
216                    || fork.peek(Brace)
217                    || (fork.peek(Token![=])
218                        && (fork.peek2(Paren) || fork.peek2(Bracket) || fork.peek2(Brace)))
219                {
220                    Ok(Self::PathList(input.parse()?))
221                } else if fork.peek(Token![=]) {
222                    Ok(Self::PathValue(input.parse()?))
223                } else {
224                    input.advance_to(&fork);
225                    Ok(Self::Expr(syn::Expr::Path(syn::ExprPath {
226                        attrs: Vec::new(),
227                        qself: None,
228                        path,
229                    })))
230                }
231            }
232            Err(_) => match fork.parse::<Expr>() {
233                Ok(expr) => {
234                    input.advance_to(&fork);
235                    Ok(Self::Expr(expr))
236                }
237                _ => Ok(Self::Verbatim(input.parse_arg()?)),
238            },
239        }
240    }
241}
242
243impl syn::parse::Parse for PathValue {
244    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
245        Ok(Self {
246            path: input.call(parse_meta_path)?,
247            eq_token: input.parse()?,
248            value: input.parse()?,
249        })
250    }
251}
252
253impl syn::parse::Parse for PathList {
254    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
255        let path = input.call(parse_meta_path)?;
256        let eq_token = input.parse()?;
257        let (delimiter, tokens) = input.call(parse_delimiters)?;
258        Ok(Self {
259            path,
260            eq_token,
261            delimiter,
262            tokens,
263        })
264    }
265}
266
267/// Parse a path without segment arguments and allowing any reserved keyword
268/// except `true` and `false` on the first segment.
269fn parse_meta_path(input: syn::parse::ParseStream) -> syn::Result<Path> {
270    let fork = input.fork();
271    let ident = syn::Ident::parse_any(&fork)?;
272    if ident == "true" || ident == "false" {
273        return Err(ident.error("unexpected reserved keyword"));
274    }
275    Ok(Path {
276        leading_colon: input.parse()?,
277        segments: {
278            let mut segments = syn::punctuated::Punctuated::new();
279            let ident = syn::Ident::parse_any(input)?;
280            segments.push_value(syn::PathSegment::from(ident));
281            while input.peek(Token![::]) {
282                let punct = input.parse()?;
283                segments.push_punct(punct);
284                let ident = syn::Ident::parse_any(input)?;
285                segments.push_value(syn::PathSegment::from(ident));
286            }
287            segments
288        },
289    })
290}
291
292/// Parses a custom token stream inside delimiters.
293fn parse_delimiters(input: syn::parse::ParseStream) -> syn::Result<(MacroDelimiter, TokenStream)> {
294    input.step(|cursor| match cursor.token_tree() {
295        Some((TokenTree::Group(g), rest)) => {
296            let span = g.delim_span();
297            let delimiter = match g.delimiter() {
298                Delimiter::Parenthesis => MacroDelimiter::Paren(Paren(span)),
299                Delimiter::Brace => MacroDelimiter::Brace(Brace(span)),
300                Delimiter::Bracket => MacroDelimiter::Bracket(Bracket(span)),
301                _ => return Err(cursor.error("expected delimiter")),
302            };
303            Ok(((delimiter, g.stream()), rest))
304        }
305        _ => Err(cursor.error("expected delimiter")),
306    })
307}
308
309fn delimiters_to_tokens(delimiter: &MacroDelimiter, inner: &TokenStream, tokens: &mut TokenStream) {
310    let (delim, span) = match delimiter {
311        MacroDelimiter::Paren(paren) => (Delimiter::Parenthesis, paren.span),
312        MacroDelimiter::Brace(brace) => (Delimiter::Brace, brace.span),
313        MacroDelimiter::Bracket(bracket) => (Delimiter::Bracket, bracket.span),
314    };
315    let mut group = Group::new(delim, inner.clone());
316    group.set_span(span.join());
317    tokens.append(group);
318}
319
320impl ToTokens for Meta {
321    fn to_tokens(&self, tokens: &mut TokenStream) {
322        match self {
323            Meta::PathList(m) => m.to_tokens(tokens),
324            Meta::PathValue(m) => m.to_tokens(tokens),
325            Meta::Expr(m) => m.to_tokens(tokens),
326            Meta::Verbatim(m) => m.to_tokens(tokens),
327        }
328    }
329}
330
331impl ToTokens for PathValue {
332    fn to_tokens(&self, tokens: &mut TokenStream) {
333        self.path.to_tokens(tokens);
334        self.eq_token.to_tokens(tokens);
335        self.value.to_tokens(tokens);
336    }
337}
338
339impl ToTokens for PathList {
340    fn to_tokens(&self, tokens: &mut TokenStream) {
341        self.path.to_tokens(tokens);
342        self.eq_token.to_tokens(tokens);
343        delimiters_to_tokens(&self.delimiter, &self.tokens, tokens);
344    }
345}
346
347#[cfg(test)]
348mod tests {
349    use super::*;
350
351    macro_rules! meta {
352        ($($attr:tt)*) => {{
353            syn::parse_str::<Meta>(stringify!($($attr)*)).unwrap()
354        }};
355    }
356
357    #[test]
358    fn parse_path() {
359        let meta = meta! { foo };
360        let Meta::Expr(syn::Expr::Path(syn::ExprPath { path, .. })) = meta else {
361            panic!("expected Meta::Path");
362        };
363        assert!(path.is_strict("foo"));
364    }
365
366    #[test]
367    fn parse_path_list() {
368        let meta = meta! { foo(1, 2, 3) };
369        let Meta::PathList(meta) = meta else {
370            panic!("expected Meta::List");
371        };
372        assert!(meta.path.is_strict("foo"));
373        assert!(meta.eq_token.is_none());
374        assert_eq!(meta.tokens.to_string(), "1 , 2 , 3");
375    }
376
377    #[test]
378    fn parse_path_list_with_equal_sign() {
379        let meta = meta! { foo = [1, 2, 3] };
380        let Meta::PathList(meta) = meta else {
381            panic!("expected Meta::List");
382        };
383        assert!(meta.path.is_strict("foo"));
384        assert!(meta.eq_token.is_some());
385        assert_eq!(meta.tokens.to_string(), "1 , 2 , 3");
386    }
387
388    #[test]
389    fn parse_path_value_with_expression() {
390        let meta: Meta = meta! { foo = 42 };
391        let Meta::PathValue(meta) = meta else {
392            panic!("expected Meta::PathValue");
393        };
394        assert!(meta.path.is_strict("foo"));
395        let expr = meta.value.as_expr().unwrap();
396        assert_eq!(expr.as_unsigned_integer::<usize>().unwrap(), 42);
397    }
398
399    #[test]
400    fn parse_path_value_with_boolean() {
401        let meta: Meta = meta! { foo = true };
402        let Meta::PathValue(meta) = meta else {
403            panic!("expected Meta::PathValue");
404        };
405        assert!(meta.path.is_strict("foo"));
406        let expr = meta.value.as_expr().unwrap();
407        assert!(expr.as_bool().unwrap());
408    }
409
410    #[test]
411    fn parse_path_value_with_verbatim() {
412        let meta = meta! { foo = ?what? };
413        let Meta::PathValue(meta) = meta else {
414            panic!("expected Meta::PathValue");
415        };
416        assert!(meta.path.is_strict("foo"));
417        let value = meta.value.as_verbatim("expected verbatim").unwrap();
418        assert_eq!(value.to_string(), "? what ?");
419    }
420
421    #[test]
422    fn parse_path_value_with_list() {
423        let meta = meta! { foo = bar(1, 2, 3) };
424        let Meta::PathValue(meta) = meta else {
425            panic!("expected Meta::PathValue");
426        };
427        assert!(meta.path.is_strict("foo"));
428        let value = meta.value.as_path_list().unwrap();
429        assert!(value.path.is_strict("bar"));
430        assert_eq!(value.tokens.to_string(), "1 , 2 , 3");
431    }
432
433    #[test]
434    fn parse_expr() {
435        let meta = meta! { "hello" };
436        let Meta::Expr(expr) = meta else {
437            panic!("expected Meta::Expr");
438        };
439        assert_eq!(expr.to_token_stream().to_string(), "\"hello\"");
440    }
441
442    #[test]
443    fn parse_verbatim() {
444        let meta = meta! { [==> 42 <==] };
445        let Meta::Verbatim(verbatim) = meta else {
446            panic!("expected Meta::Verbatim");
447        };
448        assert_eq!(verbatim.to_string(), "[==> 42 <==]");
449    }
450
451    #[test]
452    fn parse_verbatim_list() -> syn::Result<()> {
453        let meta = meta! { foo([==> 1 <==], [==> 2 <==]) };
454        let list = meta.as_path_list()?;
455        assert_eq!(list.path.to_string(), "foo");
456        let metas = list.parse_metas()?;
457        assert_eq!(metas.len(), 2);
458        assert_eq!(
459            metas[0].as_verbatim("expected [==> n <==]")?.to_string(),
460            "[==> 1 <==]"
461        );
462        assert_eq!(
463            metas[1].as_verbatim("expected [==> n <==]")?.to_string(),
464            "[==> 2 <==]"
465        );
466        Ok(())
467    }
468
469    #[test]
470    fn path() -> syn::Result<()> {
471        assert_eq!(meta! { foo }.path()?.to_string(), "foo");
472        assert_eq!(meta! { foo(42) }.path()?.to_string(), "foo");
473        assert_eq!(meta! { foo = 42 }.path()?.to_string(), "foo");
474        assert_eq!(meta! { foo = bar(42) }.path()?.to_string(), "foo");
475        assert_eq!(
476            meta! { [verbatim] }.path().unwrap_err().to_string(),
477            "expected a path"
478        );
479        Ok(())
480    }
481
482    #[test]
483    fn is_path_or_empty_list() {
484        assert!(meta! { foo }.is_path_or_empty_list());
485        assert!(meta! { some_node }.is_path_or_empty_list());
486        assert!(meta! { foo() }.is_path_or_empty_list());
487        assert!(meta! { some_node() }.is_path_or_empty_list());
488        assert!(!meta! { foo = 42 }.is_path_or_empty_list());
489        assert!(!meta! { foo = bar(1, 2, 3) }.is_path_or_empty_list());
490        assert!(!meta! { foo(answer = 42) }.is_path_or_empty_list());
491        assert!(!meta! { some_node(hello) }.is_path_or_empty_list());
492        assert!(!meta! { 42 }.is_path_or_empty_list());
493        assert!(!meta! { [verbatim] }.is_path_or_empty_list());
494    }
495
496    #[test]
497    fn as_path() {
498        assert_eq!(meta! { foo }.as_path().unwrap().to_string(), "foo");
499
500        assert_eq!(
501            meta! { foo(42) }.as_path().unwrap_err().to_string(),
502            "unexpected tokens, expected a single path"
503        );
504        assert_eq!(
505            meta! { foo = 42 }.as_path().unwrap_err().to_string(),
506            "unexpected tokens, expected a single path"
507        );
508        assert_eq!(
509            meta! { foo = bar(42) }.as_path().unwrap_err().to_string(),
510            "unexpected tokens, expected a single path"
511        );
512        assert_eq!(
513            meta! { [verbatim] }.as_path().unwrap_err().to_string(),
514            "expected a path"
515        );
516    }
517
518    #[test]
519    fn as_path_list() {
520        let meta = meta! { foo(1, 2, 3) };
521        let list = meta.as_path_list().unwrap();
522        assert!(list.path.is_strict("foo"));
523        assert_eq!(list.tokens.to_string(), "1 , 2 , 3");
524
525        assert_eq!(
526            meta! { foo }.as_path_list().unwrap_err().to_string(),
527            "expected a list: `foo(...)` or `foo = (...)`"
528        );
529        assert_eq!(
530            meta! { foo = 42 }.as_path_list().unwrap_err().to_string(),
531            "expected a list: `foo = (...)`"
532        );
533        assert_eq!(
534            meta! { foo = bar(42) }
535                .as_path_list()
536                .unwrap_err()
537                .to_string(),
538            "expected a list: `foo = (...)`"
539        );
540        assert_eq!(
541            meta! { [verbatim] }.as_path_list().unwrap_err().to_string(),
542            "expected a path followed by a list: `my_path(...)` or `my_path = (...)`"
543        );
544    }
545}