Skip to main content

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