// Copyright (c) 2024 Jonas Schäfer // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. //! # Parse Rust attributes //! //! This module is concerned with parsing attributes from the Rust "meta" //! annotations on structs, enums, enum variants and fields. use std::hash::{Hash, Hasher}; use proc_macro2::{Span, TokenStream}; use quote::{quote, quote_spanned}; use syn::{meta::ParseNestedMeta, spanned::Spanned, *}; use rxml_validation::NcName; /// XML core namespace URI (for the `xml:` prefix) pub const XMLNS_XML: &str = "http://www.w3.org/XML/1998/namespace"; /// XML namespace URI (for the `xmlns:` prefix) pub const XMLNS_XMLNS: &str = "http://www.w3.org/2000/xmlns/"; macro_rules! reject_key { ($key:ident not on $not_allowed_on:literal only on $only_allowed_on:literal) => { if let Some($key) = $key { return Err(Error::new_spanned( $key, concat!( "`", stringify!($key), "` is not allowed on ", $not_allowed_on, " (only on ", $only_allowed_on, ")" ), )); } }; ($key:ident flag not on $not_allowed_on:literal only on $only_allowed_on:literal) => { if let Flag::Present($key) = $key { return Err(Error::new( $key, concat!( "`", stringify!($key), "` is not allowed on ", $not_allowed_on, " (only on ", $only_allowed_on, ")" ), )); } }; } pub(crate) use reject_key; /// Value for the `#[xml(namespace = ..)]` attribute. #[derive(Debug)] pub(crate) enum NamespaceRef { /// The XML namespace is specified as a string literal. LitStr(LitStr), /// The XML namespace is specified as a path. Path(Path), } impl NamespaceRef { fn fudge(value: &str, span: Span) -> Self { Self::LitStr(LitStr::new(value, span)) } } impl syn::parse::Parse for NamespaceRef { fn parse(input: syn::parse::ParseStream<'_>) -> Result { if input.peek(syn::LitStr) { Ok(Self::LitStr(input.parse()?)) } else { Ok(Self::Path(input.parse()?)) } } } impl quote::ToTokens for NamespaceRef { fn to_tokens(&self, tokens: &mut TokenStream) { match self { Self::LitStr(ref lit) => lit.to_tokens(tokens), Self::Path(ref path) => path.to_tokens(tokens), } } } /// Value for the `#[xml(name = .. )]` attribute. #[derive(Debug, Clone)] pub(crate) enum NameRef { /// The XML name is specified as a string literal. Literal { /// The validated XML name. value: NcName, /// The span of the original [`syn::LitStr`]. span: Span, }, /// The XML name is specified as a path. Path(Path), } impl Hash for NameRef { fn hash(&self, h: &mut H) { match self { Self::Literal { ref value, .. } => value.hash(h), Self::Path(ref path) => path.hash(h), } } } impl PartialEq for NameRef { fn eq(&self, other: &NameRef) -> bool { match self { Self::Literal { value: ref my_value, .. } => match other { Self::Literal { value: ref other_value, .. } => my_value == other_value, _ => false, }, Self::Path(ref my_path) => match other { Self::Path(ref other_path) => my_path == other_path, _ => false, }, } } } impl Eq for NameRef {} impl syn::parse::Parse for NameRef { fn parse(input: syn::parse::ParseStream<'_>) -> Result { if input.peek(syn::LitStr) { let s: LitStr = input.parse()?; let span = s.span(); match NcName::try_from(s.value()) { Ok(value) => Ok(Self::Literal { value, span }), Err(e) => Err(Error::new(span, format!("not a valid XML name: {}", e))), } } else { let p: Path = input.parse()?; Ok(Self::Path(p)) } } } impl quote::ToTokens for NameRef { fn to_tokens(&self, tokens: &mut TokenStream) { match self { Self::Literal { ref value, span } => { let span = *span; let value = value.as_str(); let value = quote_spanned! { span=> #value }; // SAFETY: self.0 is a known-good NcName, so converting it to an // NcNameStr is known to be safe. // NOTE: we cannot use `quote_spanned! { self.span=> }` for the unsafe // block as that would then in fact trip a `#[deny(unsafe_code)]` lint // at the use site of the macro. tokens.extend(quote! { unsafe { ::xso::exports::rxml::NcNameStr::from_str_unchecked(#value) } }) } Self::Path(ref path) => path.to_tokens(tokens), } } } /// Represents the amount constraint used with child elements. /// /// Currently, this only supports "one" (literal `1`) or "any amount" (`..`). /// In the future, we might want to add support for any range pattern for /// `usize` and any positive integer literal. #[derive(Debug)] pub(crate) enum AmountConstraint { /// Equivalent to `1` #[allow(dead_code)] FixedSingle(Span), /// Equivalent to `..`. Any(Span), } impl syn::parse::Parse for AmountConstraint { fn parse(input: syn::parse::ParseStream<'_>) -> Result { if input.peek(LitInt) && !input.peek2(token::DotDot) && !input.peek2(token::DotDotEq) { let lit: LitInt = input.parse()?; let value: usize = lit.base10_parse()?; if value == 1 { Ok(Self::FixedSingle(lit.span())) } else { Err(Error::new(lit.span(), "only `1` and `..` are allowed here")) } } else { let p: PatRange = input.parse()?; if let Some(attr) = p.attrs.first() { return Err(Error::new_spanned(attr, "attributes not allowed here")); } if let Some(start) = p.start.as_ref() { return Err(Error::new_spanned( start, "only full ranges (`..`) are allowed here", )); } if let Some(end) = p.end.as_ref() { return Err(Error::new_spanned( end, "only full ranges (`..`) are allowed here", )); } Ok(Self::Any(p.span())) } } } /// Represents a boolean flag from a `#[xml(..)]` attribute meta. #[derive(Clone, Copy, Debug)] pub(crate) enum Flag { /// The flag is not set. Absent, /// The flag was set. Present( /// The span of the syntax element which enabled the flag. /// /// This is used to generate useful error messages by pointing at the /// specific place the flag was activated. #[allow(dead_code)] Span, ), } impl Flag { /// Return true if the flag is set, false otherwise. pub(crate) fn is_set(&self) -> bool { match self { Self::Absent => false, Self::Present(_) => true, } } } impl From for Flag { fn from(other: T) -> Flag { Flag::Present(other.span()) } } /// A pair of `namespace` and `name` keys. #[derive(Debug, Default)] pub(crate) struct QNameRef { /// The XML namespace supplied. pub(crate) namespace: Option, /// The XML name supplied. pub(crate) name: Option, } impl QNameRef { /// Attempt to incrementally parse this QNameRef. /// /// If `meta` contains either `namespace` or `name` keys, they are /// processed and either `Ok(None)` or an error is returned. /// /// If no matching key is found, `Ok(Some(meta))` is returned for further /// processing. fn parse_incremental_from_meta<'x>( &mut self, meta: ParseNestedMeta<'x>, ) -> Result>> { if meta.path.is_ident("name") { if self.name.is_some() { return Err(Error::new_spanned(meta.path, "duplicate `name` key")); } let value = meta.value()?; let name_span = value.span(); let (new_namespace, new_name) = parse_prefixed_name(value)?; if let Some(new_namespace) = new_namespace { if let Some(namespace) = self.namespace.as_ref() { let mut error = Error::new( name_span, "cannot combine `namespace` key with prefixed `name`", ); error.combine(Error::new_spanned(namespace, "`namespace` was set here")); return Err(error); } self.namespace = Some(new_namespace); } self.name = Some(new_name); Ok(None) } else if meta.path.is_ident("namespace") { if self.namespace.is_some() { return Err(Error::new_spanned( meta.path, "duplicate `namespace` key or `name` key has prefix", )); } self.namespace = Some(meta.value()?.parse()?); Ok(None) } else { Ok(Some(meta)) } } } /// Contents of an `#[xml(..)]` attribute on a struct, enum variant, or enum. #[derive(Debug)] pub(crate) struct XmlCompoundMeta { /// The span of the `#[xml(..)]` meta from which this was parsed. /// /// This is useful for error messages. pub(crate) span: Span, /// The value assigned to `namespace` and `name` fields inside /// `#[xml(..)]`, if any. pub(crate) qname: QNameRef, /// The debug flag. pub(crate) debug: Flag, /// The value assigned to `builder` inside `#[xml(..)]`, if any. pub(crate) builder: Option, /// The value assigned to `iterator` inside `#[xml(..)]`, if any. pub(crate) iterator: Option, /// The exhaustive flag. pub(crate) exhaustive: Flag, } impl XmlCompoundMeta { /// Parse the meta values from a `#[xml(..)]` attribute. /// /// Undefined options or options with incompatible values are rejected /// with an appropriate compile-time error. fn parse_from_attribute(attr: &Attribute) -> Result { let mut qname = QNameRef::default(); let mut builder = None; let mut iterator = None; let mut debug = Flag::Absent; let mut exhaustive = Flag::Absent; attr.parse_nested_meta(|meta| { if meta.path.is_ident("debug") { if debug.is_set() { return Err(Error::new_spanned(meta.path, "duplicate `debug` key")); } debug = (&meta.path).into(); Ok(()) } else if meta.path.is_ident("builder") { if builder.is_some() { return Err(Error::new_spanned(meta.path, "duplicate `builder` key")); } builder = Some(meta.value()?.parse()?); Ok(()) } else if meta.path.is_ident("iterator") { if iterator.is_some() { return Err(Error::new_spanned(meta.path, "duplicate `iterator` key")); } iterator = Some(meta.value()?.parse()?); Ok(()) } else if meta.path.is_ident("exhaustive") { if exhaustive.is_set() { return Err(Error::new_spanned(meta.path, "duplicate `exhaustive` key")); } exhaustive = (&meta.path).into(); Ok(()) } else { match qname.parse_incremental_from_meta(meta)? { None => Ok(()), Some(meta) => Err(Error::new_spanned(meta.path, "unsupported key")), } } })?; Ok(Self { span: attr.span(), qname, debug, builder, iterator, exhaustive, }) } /// Search through `attrs` for a single `#[xml(..)]` attribute and parse /// it. /// /// Undefined options or options with incompatible values are rejected /// with an appropriate compile-time error. /// /// If more than one `#[xml(..)]` attribute is found, an error is /// emitted. /// /// If no `#[xml(..)]` attribute is found, `None` is returned. pub(crate) fn try_parse_from_attributes(attrs: &[Attribute]) -> Result> { let mut result = None; for attr in attrs { if !attr.path().is_ident("xml") { continue; } if result.is_some() { return Err(syn::Error::new_spanned( attr.path(), "only one #[xml(..)] per struct or enum variant allowed", )); } result = Some(Self::parse_from_attribute(attr)?); } Ok(result) } /// Search through `attrs` for a single `#[xml(..)]` attribute and parse /// it. /// /// Undefined options or options with incompatible values are rejected /// with an appropriate compile-time error. /// /// If more than one or no `#[xml(..)]` attribute is found, an error is /// emitted. pub(crate) fn parse_from_attributes(attrs: &[Attribute]) -> Result { match Self::try_parse_from_attributes(attrs)? { Some(v) => Ok(v), None => Err(syn::Error::new( Span::call_site(), "#[xml(..)] attribute required on struct or enum variant", )), } } } /// Return true if the tokens the cursor points at are a valid type path /// prefix. /// /// This does not advance the parse stream. /// /// If the tokens *do* look like a type path, a Span which points at the first /// `<` encountered is returned. This can be used for a helpful error message /// in case parsing the type path does then fail. fn maybe_type_path(p: parse::ParseStream<'_>) -> (bool, Option) { // ParseStream cursors do not advance the stream, but they are also rather // unwieldly to use. Prepare for a lot of `let .. = ..`. let cursor = if p.peek(token::PathSep) { // If we have a path separator, we need to skip that initially. We // do this by skipping two punctuations. We use unwrap() here because // we already know for sure that we see two punctuation items (because // of the peek). p.cursor().punct().unwrap().1.punct().unwrap().1 } else { // No `::` initially, so we just take what we have. p.cursor() }; // Now we loop over `$ident::` segments. If we find anything but a `:` // after the ident, we exit. Depending on *what* we find, we either exit // true or false, but see for yourself. let mut cursor = cursor; loop { // Here we look for the identifier, but we do not care for its // contents. let Some((_, new_cursor)) = cursor.ident() else { return (false, None); }; cursor = new_cursor; // Now we see what actually follows the ident (it must be punctuation // for it to be a type path...) let Some((punct, new_cursor)) = cursor.punct() else { return (false, None); }; cursor = new_cursor; match punct.as_char() { // Looks like a `foo<..`, we treat that as a type path for the // reasons stated in [`parse_codec_expr`]'s doc. '<' => return (true, Some(punct.span())), // Continue looking ahead: looks like a path separator. ':' => (), // Anything else (such as `,` (separating another argument most // likely), or `.` (a method call?)) we treat as "not a type // path". _ => return (false, None), } // If we are here, we saw a `:`. Look for the second one. let Some((punct, new_cursor)) = cursor.punct() else { return (false, None); }; cursor = new_cursor; if punct.as_char() != ':' { // If it is not another `:`, it cannot be a type path. return (false, None); } // And round and round and round it goes. // We will terminate eventually because the cursor will return None // on any of the lookups because parse streams are (hopefully!) // finite. Most likely, we'll however encounter a `<` or other non-`:` // punctuation first. } } /// Parse expressions passed to `codec`. /// /// Those will generally be paths to unit type constructors (such as `Foo`) /// or references to static values or chains of function calls. /// /// In the case of unit type constructors for generic types, users may type /// for example `FixedHex<20>`, thinking they are writing a type path. However, /// while `FixedHex<20>` is indeed a valid type path, it is not a valid /// expression for a unit type constructor. Instead it is parsed as /// `FixedHex < 20` and then a syntax error. /// /// We however know that `Foo < Bar` is never a valid expression for a type. /// Thus, we can be smart about this and inject the `::` at the right place /// automatically. fn parse_codec_expr(p: parse::ParseStream<'_>) -> Result<(Expr, Option)> { let (maybe_type_path, punct_span) = maybe_type_path(p); if maybe_type_path { let helpful_error = punct_span.map(|span| Error::new(span, "help: try inserting a `::` before this `<`")); let mut type_path: TypePath = match p.parse() { Ok(v) => v, Err(mut e) => match helpful_error { Some(help) => { e.combine(help); return Err(e); } None => return Err(e), }, }; // We got a type path -- so we now inject the `::` before any `<` as // needed. for segment in type_path.path.segments.iter_mut() { match segment.arguments { PathArguments::AngleBracketed(ref mut arguments) => { let span = arguments.span(); arguments .colon2_token .get_or_insert_with(|| token::PathSep { spans: [span, span], }); } _ => (), } } Ok(( Expr::Path(ExprPath { attrs: Vec::new(), qself: type_path.qself, path: type_path.path, }), helpful_error, )) } else { p.parse().map(|x| (x, None)) } } /// Parse an XML name while resolving built-in namespace prefixes. fn parse_prefixed_name( value: syn::parse::ParseStream<'_>, ) -> Result<(Option, NameRef)> { if !value.peek(LitStr) { // if we don't have a string literal next, we delegate to the default // `NameRef` parser. return Ok((None, value.parse()?)); } let name: LitStr = value.parse()?; let name_span = name.span(); let (prefix, name) = match name .value() .try_into() .and_then(|name: rxml_validation::Name| name.split_name()) { Ok(v) => v, Err(e) => { return Err(Error::new( name_span, format!("not a valid XML name: {}", e), )) } }; let name = NameRef::Literal { value: name, span: name_span, }; if let Some(prefix) = prefix { let namespace_uri = match prefix.as_str() { "xml" => XMLNS_XML, "xmlns" => XMLNS_XMLNS, other => return Err(Error::new( name_span, format!("prefix `{}` is not a built-in prefix and cannot be used. specify the desired namespace using the `namespace` key instead.", other) )), }; Ok((Some(NamespaceRef::fudge(namespace_uri, name_span)), name)) } else { Ok((None, name)) } } /// Contents of an `#[xml(..)]` attribute on a struct or enum variant member. #[derive(Debug)] pub(crate) enum XmlFieldMeta { /// `#[xml(attribute)]`, `#[xml(attribute = ..)]` or `#[xml(attribute(..))]` Attribute { /// The span of the `#[xml(attribute)]` meta from which this was parsed. /// /// This is useful for error messages. span: Span, /// The namespace/name keys. qname: QNameRef, /// The `default` flag. default_: Flag, }, /// `#[xml(text)]` Text { /// The path to the optional codec type. codec: Option, }, /// `#[xml(child)` Child { /// The `default` flag. default_: Flag, /// The `n` flag. amount: Option, }, } impl XmlFieldMeta { /// Parse a `#[xml(attribute(..))]` meta. /// /// That meta can have three distinct syntax styles: /// - argument-less: `#[xml(attribute)]` /// - shorthand: `#[xml(attribute = ..)]` /// - full: `#[xml(attribute(..))]` fn attribute_from_meta(meta: ParseNestedMeta<'_>) -> Result { if meta.input.peek(Token![=]) { // shorthand syntax let (namespace, name) = parse_prefixed_name(meta.value()?)?; Ok(Self::Attribute { span: meta.path.span(), qname: QNameRef { name: Some(name), namespace, }, default_: Flag::Absent, }) } else if meta.input.peek(syn::token::Paren) { // full syntax let mut qname = QNameRef::default(); let mut default_ = Flag::Absent; meta.parse_nested_meta(|meta| { if meta.path.is_ident("default") { if default_.is_set() { return Err(Error::new_spanned(meta.path, "duplicate `default` key")); } default_ = (&meta.path).into(); Ok(()) } else { match qname.parse_incremental_from_meta(meta)? { None => Ok(()), Some(meta) => Err(Error::new_spanned(meta.path, "unsupported key")), } } })?; Ok(Self::Attribute { span: meta.path.span(), qname, default_, }) } else { // argument-less syntax Ok(Self::Attribute { span: meta.path.span(), qname: QNameRef::default(), default_: Flag::Absent, }) } } /// Parse a `#[xml(text)]` meta. fn text_from_meta(meta: ParseNestedMeta<'_>) -> Result { if meta.input.peek(Token![=]) { let (codec, helpful_error) = parse_codec_expr(meta.value()?)?; // A meta value can only be followed by either a `,`, or the end // of the parse stream (because of the delimited group ending). // Hence we check we are there. And if we are *not* there, we emit // an error straight away, with the helpful addition from the // `parse_codec_expr` if we have it. // // If we do not do this, the user gets a rather confusing // "expected `,`" message if the `maybe_type_path` guess was // wrong. let lookahead = meta.input.lookahead1(); if !lookahead.peek(Token![,]) && !meta.input.is_empty() { if let Some(helpful_error) = helpful_error { let mut e = lookahead.error(); e.combine(helpful_error); return Err(e); } } Ok(Self::Text { codec: Some(codec) }) } else if meta.input.peek(syn::token::Paren) { let mut codec: Option = None; meta.parse_nested_meta(|meta| { if meta.path.is_ident("codec") { if codec.is_some() { return Err(Error::new_spanned(meta.path, "duplicate `codec` key")); } let (new_codec, helpful_error) = parse_codec_expr(meta.value()?)?; // See above (at the top-ish of this function) for why we // do this. let lookahead = meta.input.lookahead1(); if !lookahead.peek(Token![,]) && !meta.input.is_empty() { if let Some(helpful_error) = helpful_error { let mut e = lookahead.error(); e.combine(helpful_error); return Err(e); } } codec = Some(new_codec); Ok(()) } else { Err(Error::new_spanned(meta.path, "unsupported key")) } })?; Ok(Self::Text { codec }) } else { Ok(Self::Text { codec: None }) } } /// Parse a `#[xml(child)]` meta. fn child_from_meta(meta: ParseNestedMeta<'_>) -> Result { if meta.input.peek(syn::token::Paren) { let mut default_ = Flag::Absent; let mut amount = None; meta.parse_nested_meta(|meta| { if meta.path.is_ident("default") { if default_.is_set() { return Err(Error::new_spanned(meta.path, "duplicate `default` key")); } default_ = (&meta.path).into(); Ok(()) } else if meta.path.is_ident("n") { if amount.is_some() { return Err(Error::new_spanned(meta.path, "duplicate `n` key")); } amount = Some(meta.value()?.parse()?); Ok(()) } else { Err(Error::new_spanned(meta.path, "unsupported key")) } })?; Ok(Self::Child { default_, amount }) } else { Ok(Self::Child { default_: Flag::Absent, amount: None, }) } } /// Parse [`Self`] from a nestd meta, switching on the identifier /// of that nested meta. fn parse_from_meta(meta: ParseNestedMeta<'_>) -> Result { if meta.path.is_ident("attribute") { Self::attribute_from_meta(meta) } else if meta.path.is_ident("text") { Self::text_from_meta(meta) } else if meta.path.is_ident("child") { Self::child_from_meta(meta) } else { Err(Error::new_spanned(meta.path, "unsupported field meta")) } } /// Parse an `#[xml(..)]` meta on a field. /// /// This switches based on the first identifier within the `#[xml(..)]` /// meta and generates an enum variant accordingly. /// /// Only a single nested meta is allowed; more than one will be /// rejected with an appropriate compile-time error. /// /// If no meta is contained at all, a compile-time error is generated. /// /// Undefined options or options with incompatible values are rejected /// with an appropriate compile-time error. pub(crate) fn parse_from_attribute(attr: &Attribute) -> Result { let mut result: Option = None; attr.parse_nested_meta(|meta| { if result.is_some() { return Err(Error::new_spanned( meta.path, "multiple field type specifiers are not supported", )); } result = Some(Self::parse_from_meta(meta)?); Ok(()) })?; if let Some(result) = result { Ok(result) } else { Err(Error::new_spanned( attr, "missing field type specifier within `#[xml(..)]`", )) } } /// Find and parse a `#[xml(..)]` meta on a field. /// /// This invokes [`Self::parse_from_attribute`] internally on the first /// encountered `#[xml(..)]` meta. /// /// If not exactly one `#[xml(..)]` meta is encountered, an error is /// returned. The error is spanned to `err_span`. pub(crate) fn parse_from_attributes(attrs: &[Attribute], err_span: &Span) -> Result { let mut result: Option = None; for attr in attrs { if !attr.path().is_ident("xml") { continue; } if result.is_some() { return Err(Error::new_spanned( attr, "only one #[xml(..)] attribute per field allowed.", )); } result = Some(Self::parse_from_attribute(attr)?); } if let Some(result) = result { Ok(result) } else { Err(Error::new(*err_span, "missing #[xml(..)] meta on field")) } } }