Skip to main content

syn/
meta.rs

1// SPDX-License-Identifier: Apache-2.0 OR MIT
2
3//! Facility for interpreting structured content inside of an `Attribute`.
4
5use crate::error::{Error, Result};
6use crate::ext::IdentExt as _;
7use crate::lit::Lit;
8use crate::parse::{ParseStream, Parser};
9use crate::path::{Path, PathSegment};
10use crate::punctuated::Punctuated;
11use proc_macro2::Ident;
12use std::fmt::Display;
13
14/// Make a parser that is usable with `parse_macro_input!` in a
15/// `#[proc_macro_attribute]` macro.
16///
17/// *Warning:* When parsing attribute args **other than** the
18/// `proc_macro::TokenStream` input of a `proc_macro_attribute`, you do **not**
19/// need this function. In several cases your callers will get worse error
20/// messages if you use this function, because the surrounding delimiter's span
21/// is concealed from attribute macros by rustc. Use
22/// [`Attribute::parse_nested_meta`] instead.
23///
24/// [`Attribute::parse_nested_meta`]: crate::Attribute::parse_nested_meta
25///
26/// # Example
27///
28/// This example implements an attribute macro whose invocations look like this:
29///
30/// ```
31/// # const IGNORE: &str = stringify! {
32/// #[tea(kind = "EarlGrey", hot)]
33/// struct Picard {...}
34/// # };
35/// ```
36///
37/// The "parameters" supported by the attribute are:
38///
39/// - `kind = "..."`
40/// - `hot`
41/// - `with(sugar, milk, ...)`, a comma-separated list of ingredients
42///
43/// ```
44/// # extern crate proc_macro;
45/// #
46/// use proc_macro::TokenStream;
47/// use syn::{parse_macro_input, LitStr, Path};
48///
49/// # const IGNORE: &str = stringify! {
50/// #[proc_macro_attribute]
51/// # };
52/// pub fn tea(args: TokenStream, input: TokenStream) -> TokenStream {
53///     let mut kind: Option<LitStr> = None;
54///     let mut hot: bool = false;
55///     let mut with: Vec<Path> = Vec::new();
56///     let tea_parser = syn::meta::parser(|meta| {
57///         if meta.path.is_ident("kind") {
58///             kind = Some(meta.value()?.parse()?);
59///             Ok(())
60///         } else if meta.path.is_ident("hot") {
61///             hot = true;
62///             Ok(())
63///         } else if meta.path.is_ident("with") {
64///             meta.parse_nested_meta(|meta| {
65///                 with.push(meta.path);
66///                 Ok(())
67///             })
68///         } else {
69///             Err(meta.error("unsupported tea property"))
70///         }
71///     });
72///
73///     parse_macro_input!(args with tea_parser);
74///     eprintln!("kind={kind:?} hot={hot} with={with:?}");
75///
76///     /* ... */
77/// #   TokenStream::new()
78/// }
79/// ```
80///
81/// The `syn::meta` library will take care of dealing with the commas including
82/// trailing commas, and producing sensible error messages on unexpected input.
83///
84/// ```console
85/// error: expected `,`
86///  --> src/main.rs:3:37
87///   |
88/// 3 | #[tea(kind = "EarlGrey", with(sugar = "lol", milk))]
89///   |                                     ^
90/// ```
91///
92/// # Example
93///
94/// Same as above but we factor out most of the logic into a separate function.
95///
96/// ```
97/// # extern crate proc_macro;
98/// #
99/// use proc_macro::TokenStream;
100/// use syn::meta::ParseNestedMeta;
101/// use syn::parse::{Parser, Result};
102/// use syn::{parse_macro_input, LitStr, Path};
103///
104/// # const IGNORE: &str = stringify! {
105/// #[proc_macro_attribute]
106/// # };
107/// pub fn tea(args: TokenStream, input: TokenStream) -> TokenStream {
108///     let mut attrs = TeaAttributes::default();
109///     let tea_parser = syn::meta::parser(|meta| attrs.parse(meta));
110///     parse_macro_input!(args with tea_parser);
111///
112///     /* ... */
113/// #   TokenStream::new()
114/// }
115///
116/// #[derive(Default)]
117/// struct TeaAttributes {
118///     kind: Option<LitStr>,
119///     hot: bool,
120///     with: Vec<Path>,
121/// }
122///
123/// impl TeaAttributes {
124///     fn parse(&mut self, meta: ParseNestedMeta) -> Result<()> {
125///         if meta.path.is_ident("kind") {
126///             self.kind = Some(meta.value()?.parse()?);
127///             Ok(())
128///         } else /* just like in last example */
129/// #           { unimplemented!() }
130///
131///     }
132/// }
133/// ```
134pub fn parser(logic: impl FnMut(ParseNestedMeta) -> Result<()>) -> impl Parser<Output = ()> {
135    |input: ParseStream| {
136        if input.is_empty() {
137            Ok(())
138        } else {
139            parse_nested_meta(input, logic)
140        }
141    }
142}
143
144/// Context for parsing a single property in the conventional syntax for
145/// structured attributes.
146///
147/// # Examples
148///
149/// Refer to usage examples on the following two entry-points:
150///
151/// - [`Attribute::parse_nested_meta`] if you have an entire `Attribute` to
152///   parse. Always use this if possible. Generally this is able to produce
153///   better error messages because `Attribute` holds span information for all
154///   of the delimiters therein.
155///
156/// - [`syn::meta::parser`] if you are implementing a `proc_macro_attribute`
157///   macro and parsing the arguments to the attribute macro, i.e. the ones
158///   written in the same attribute that dispatched the macro invocation. Rustc
159///   does not pass span information for the surrounding delimiters into the
160///   attribute macro invocation in this situation, so error messages might be
161///   less precise.
162///
163/// [`Attribute::parse_nested_meta`]: crate::Attribute::parse_nested_meta
164/// [`syn::meta::parser`]: crate::meta::parser
165#[non_exhaustive]
166pub struct ParseNestedMeta<'a> {
167    pub path: Path,
168    pub input: ParseStream<'a>,
169}
170
171impl<'a> ParseNestedMeta<'a> {
172    /// Used when parsing `key = "value"` syntax.
173    ///
174    /// All it does is advance `meta.input` past the `=` sign in the input. You
175    /// could accomplish the same effect by writing
176    /// `meta.parse::<Token![=]>()?`, so at most it is a minor convenience to
177    /// use `meta.value()?`.
178    ///
179    /// # Example
180    ///
181    /// ```
182    /// use syn::{parse_quote, Attribute, LitStr};
183    ///
184    /// let attr: Attribute = parse_quote! {
185    ///     #[tea(kind = "EarlGrey")]
186    /// };
187    ///                                          // conceptually:
188    /// if attr.path().is_ident("tea") {         // this parses the `tea`
189    ///     attr.parse_nested_meta(|meta| {      // this parses the `(`
190    ///         if meta.path.is_ident("kind") {  // this parses the `kind`
191    ///             let value = meta.value()?;   // this parses the `=`
192    ///             let s: LitStr = value.parse()?;  // this parses `"EarlGrey"`
193    ///             if s.value() == "EarlGrey" {
194    ///                 // ...
195    ///             }
196    ///             Ok(())
197    ///         } else {
198    ///             Err(meta.error("unsupported attribute"))
199    ///         }
200    ///     })?;
201    /// }
202    /// # anyhow::Ok(())
203    /// ```
204    pub fn value(&self) -> Result<ParseStream<'a>> {
205        self.input.parse::<Token![=]>()?;
206        Ok(self.input)
207    }
208
209    /// Used when parsing `list(...)` syntax **if** the content inside the
210    /// nested parentheses is also expected to conform to Rust's structured
211    /// attribute convention.
212    ///
213    /// # Example
214    ///
215    /// ```
216    /// use syn::{parse_quote, Attribute};
217    ///
218    /// let attr: Attribute = parse_quote! {
219    ///     #[tea(with(sugar, milk))]
220    /// };
221    ///
222    /// if attr.path().is_ident("tea") {
223    ///     attr.parse_nested_meta(|meta| {
224    ///         if meta.path.is_ident("with") {
225    ///             meta.parse_nested_meta(|meta| {  // <---
226    ///                 if meta.path.is_ident("sugar") {
227    ///                     // Here we can go even deeper if needed.
228    ///                     Ok(())
229    ///                 } else if meta.path.is_ident("milk") {
230    ///                     Ok(())
231    ///                 } else {
232    ///                     Err(meta.error("unsupported ingredient"))
233    ///                 }
234    ///             })
235    ///         } else {
236    ///             Err(meta.error("unsupported tea property"))
237    ///         }
238    ///     })?;
239    /// }
240    /// # anyhow::Ok(())
241    /// ```
242    ///
243    /// # Counterexample
244    ///
245    /// If you don't need `parse_nested_meta`'s help in parsing the content
246    /// written within the nested parentheses, keep in mind that you can always
247    /// just parse it yourself from the exposed ParseStream. Rust syntax permits
248    /// arbitrary tokens within those parentheses so for the crazier stuff,
249    /// `parse_nested_meta` is not what you want.
250    ///
251    /// ```
252    /// use syn::{parenthesized, parse_quote, Attribute, LitInt};
253    ///
254    /// let attr: Attribute = parse_quote! {
255    ///     #[repr(align(32))]
256    /// };
257    ///
258    /// let mut align: Option<LitInt> = None;
259    /// if attr.path().is_ident("repr") {
260    ///     attr.parse_nested_meta(|meta| {
261    ///         if meta.path.is_ident("align") {
262    ///             let content;
263    ///             parenthesized!(content in meta.input);
264    ///             align = Some(content.parse()?);
265    ///             Ok(())
266    ///         } else {
267    ///             Err(meta.error("unsupported repr"))
268    ///         }
269    ///     })?;
270    /// }
271    /// # anyhow::Ok(())
272    /// ```
273    pub fn parse_nested_meta(
274        &self,
275        logic: impl FnMut(ParseNestedMeta) -> Result<()>,
276    ) -> Result<()> {
277        let content;
278        parenthesized!(content in self.input);
279        parse_nested_meta(&content, logic)
280    }
281
282    /// Report that the attribute's content did not conform to expectations.
283    ///
284    /// The span of the resulting error will cover `meta.path` *and* everything
285    /// that has been parsed so far since it.
286    ///
287    /// There are 2 ways you might call this. First, if `meta.path` is not
288    /// something you recognize:
289    ///
290    /// ```
291    /// # use syn::Attribute;
292    /// #
293    /// # fn example(attr: &Attribute) -> syn::Result<()> {
294    /// attr.parse_nested_meta(|meta| {
295    ///     if meta.path.is_ident("kind") {
296    ///         // ...
297    ///         Ok(())
298    ///     } else {
299    ///         Err(meta.error("unsupported tea property"))
300    ///     }
301    /// })?;
302    /// # Ok(())
303    /// # }
304    /// ```
305    ///
306    /// In this case, it behaves exactly like
307    /// `syn::Error::new_spanned(&meta.path, "message...")`.
308    ///
309    /// ```console
310    /// error: unsupported tea property
311    ///  --> src/main.rs:3:26
312    ///   |
313    /// 3 | #[tea(kind = "EarlGrey", wat = "foo")]
314    ///   |                          ^^^
315    /// ```
316    ///
317    /// More usefully, the second place is if you've already parsed a value but
318    /// have decided not to accept the value:
319    ///
320    /// ```
321    /// # use syn::Attribute;
322    /// #
323    /// # fn example(attr: &Attribute) -> syn::Result<()> {
324    /// use syn::Expr;
325    ///
326    /// attr.parse_nested_meta(|meta| {
327    ///     if meta.path.is_ident("kind") {
328    ///         let expr: Expr = meta.value()?.parse()?;
329    ///         match expr {
330    ///             Expr::Lit(expr) => /* ... */
331    /// #               unimplemented!(),
332    ///             Expr::Path(expr) => /* ... */
333    /// #               unimplemented!(),
334    ///             Expr::Macro(expr) => /* ... */
335    /// #               unimplemented!(),
336    ///             _ => Err(meta.error("tea kind must be a string literal, path, or macro")),
337    ///         }
338    ///     } else /* as above */
339    /// #       { unimplemented!() }
340    ///
341    /// })?;
342    /// # Ok(())
343    /// # }
344    /// ```
345    ///
346    /// ```console
347    /// error: tea kind must be a string literal, path, or macro
348    ///  --> src/main.rs:3:7
349    ///   |
350    /// 3 | #[tea(kind = async { replicator.await })]
351    ///   |       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
352    /// ```
353    ///
354    /// Often you may want to use `syn::Error::new_spanned` even in this
355    /// situation. In the above code, that would be:
356    ///
357    /// ```
358    /// # use syn::{Error, Expr};
359    /// #
360    /// # fn example(expr: Expr) -> syn::Result<()> {
361    ///     match expr {
362    ///         Expr::Lit(expr) => /* ... */
363    /// #           unimplemented!(),
364    ///         Expr::Path(expr) => /* ... */
365    /// #           unimplemented!(),
366    ///         Expr::Macro(expr) => /* ... */
367    /// #           unimplemented!(),
368    ///         _ => Err(Error::new_spanned(expr, "unsupported expression type for `kind`")),
369    ///     }
370    /// # }
371    /// ```
372    ///
373    /// ```console
374    /// error: unsupported expression type for `kind`
375    ///  --> src/main.rs:3:14
376    ///   |
377    /// 3 | #[tea(kind = async { replicator.await })]
378    ///   |              ^^^^^^^^^^^^^^^^^^^^^^^^^^
379    /// ```
380    pub fn error(&self, msg: impl Display) -> Error {
381        let start_span = self.path.segments[0].ident.span();
382        let end_span = self.input.cursor().prev_span();
383        crate::error::new2(start_span, end_span, msg)
384    }
385}
386
387pub(crate) fn parse_nested_meta(
388    input: ParseStream,
389    mut logic: impl FnMut(ParseNestedMeta) -> Result<()>,
390) -> Result<()> {
391    loop {
392        let path = input.call(parse_meta_path)?;
393        logic(ParseNestedMeta { path, input })?;
394        if input.is_empty() {
395            return Ok(());
396        }
397        input.parse::<Token![,]>()?;
398        if input.is_empty() {
399            return Ok(());
400        }
401    }
402}
403
404// Like Path::parse_mod_style, but accepts keywords in the path.
405fn parse_meta_path(input: ParseStream) -> Result<Path> {
406    Ok(Path {
407        leading_colon: input.parse()?,
408        segments: {
409            let mut segments = Punctuated::new();
410            if input.peek(Ident::peek_any) {
411                let ident = Ident::parse_any(input)?;
412                segments.push_value(PathSegment::from(ident));
413            } else if input.is_empty() {
414                return Err(input.error("expected nested attribute"));
415            } else if input.peek(Lit) {
416                return Err(input.error("unexpected literal in nested attribute, expected ident"));
417            } else {
418                return Err(input.error("unexpected token in nested attribute, expected ident"));
419            }
420            while input.peek(Token![::]) {
421                let punct = input.parse()?;
422                segments.push_punct(punct);
423                let ident = Ident::parse_any(input)?;
424                segments.push_value(PathSegment::from(ident));
425            }
426            segments
427        },
428    })
429}