xso: reject attempts to match the same XML attribute in different fields
This was a bit tricky to build, because it is possible to have an indirection through a `static` there. Thanks to Rust's extensive const-fn capabilities, though, it's in fact possible to cover all cases. We still do two different checks to improve user experience. If we can, from within the proc macro, determine that two fields refer to the same XML attribute (because their namespace/name values use the same Rust tokens), then we reject the fields with a clear error message pointing at both fields. In the other case, when there's e.g. `#[xml(lang)]` and `#[xml(attribute(namespace = rxml::XMLNS_XML, name = "lang"))]`, the macro cannot be sure that XMLNS_XML is in fact the XML namespace. For that case, we generate code which is evaluated at compile time (and has no runtime impact) which panics if the namespace and name of two attribute-matching fields is the same. The error message will be less clear (because it contains extra, unchangeable wording like "evaluation of constant value failed" and "the evaluated program panicked at", which may be a bit confusing) than the message generated by the macros themselves, but it's a price we have to pay unfortunately. Note that this check may seem cosmetic and purely for better user experience, but it is in fact needed to avoid generating not-well-formed and/or not-namespace-well-formed XML: As `AsXml` generates `xso::Item`, where each attribute is emitted separated (and not aggregated in a map structure), a naive (and efficient) implementation of a writer might not double-check that no duplicate attributes are generated.
This commit is contained in:
parent
174eea5e5d
commit
9fdb1564f6
10 changed files with 267 additions and 23 deletions
|
|
@ -7,10 +7,12 @@
|
|||
//! Handling of the insides of compound structures (structs and enum variants)
|
||||
|
||||
use proc_macro2::{Span, TokenStream};
|
||||
use quote::{quote, ToTokens};
|
||||
use quote::{quote, quote_spanned, ToTokens};
|
||||
use syn::{spanned::Spanned, *};
|
||||
|
||||
use crate::error_message::ParentRef;
|
||||
use std::collections::{hash_map::Entry, HashMap};
|
||||
|
||||
use crate::error_message::{FieldName, ParentRef};
|
||||
use crate::field::{FieldBuilderPart, FieldDef, FieldIteratorPart, FieldTempInit, NestedMatcher};
|
||||
use crate::meta::{DiscardSpec, Flag, NameRef, NamespaceRef, QNameRef};
|
||||
use crate::scope::{mangle_member, AsItemsScope, FromEventsScope};
|
||||
|
|
@ -61,6 +63,12 @@ pub(crate) struct Compound {
|
|||
|
||||
/// Text to discard.
|
||||
discard_text: Flag,
|
||||
|
||||
/// Attribute qualified names which are selected by fields.
|
||||
///
|
||||
/// This is used to generate code which asserts, at compile time, that no
|
||||
/// two fields select the same XML attribute.
|
||||
selected_attributes: Vec<(QNameRef, Member)>,
|
||||
}
|
||||
|
||||
impl Compound {
|
||||
|
|
@ -83,6 +91,7 @@ impl Compound {
|
|||
let size_hint = compound_fields.size_hint();
|
||||
let mut fields = Vec::with_capacity(size_hint.1.unwrap_or(size_hint.0));
|
||||
let mut text_field = None;
|
||||
let mut selected_attributes: HashMap<QNameRef, Member> = HashMap::new();
|
||||
for field in compound_fields {
|
||||
let field = field?;
|
||||
|
||||
|
|
@ -101,6 +110,26 @@ impl Compound {
|
|||
text_field = Some(field.member().span())
|
||||
}
|
||||
|
||||
if let Some(qname) = field.captures_attribute() {
|
||||
let span = field.span();
|
||||
match selected_attributes.entry(qname) {
|
||||
Entry::Occupied(o) => {
|
||||
let mut err = Error::new(
|
||||
span,
|
||||
"this field XML field matches the same attribute as another field",
|
||||
);
|
||||
err.combine(Error::new(
|
||||
o.get().span(),
|
||||
"the other field matching the same attribute is here",
|
||||
));
|
||||
return Err(err);
|
||||
}
|
||||
Entry::Vacant(v) => {
|
||||
v.insert(field.member().clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fields.push(field);
|
||||
}
|
||||
|
||||
|
|
@ -157,6 +186,7 @@ impl Compound {
|
|||
unknown_child_policy,
|
||||
discard_attr,
|
||||
discard_text,
|
||||
selected_attributes: selected_attributes.into_iter().collect(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -189,6 +219,82 @@ impl Compound {
|
|||
)
|
||||
}
|
||||
|
||||
/// Generate code which, at compile time, asserts that all attributes
|
||||
/// which are selected by this compound are disjunct.
|
||||
///
|
||||
/// NOTE: this needs rustc 1.83 or newer for `const_refs_to_static`.
|
||||
fn assert_disjunct_attributes(&self) -> TokenStream {
|
||||
let mut checks = TokenStream::default();
|
||||
|
||||
// Comparison is commutative, so we *could* reduce this to n^2/2
|
||||
// comparisons instead of n*(n-1). However, by comparing every field
|
||||
// with every other field and emitting check code for that, we can
|
||||
// point at both fields in the error messages.
|
||||
for (i, (qname_a, member_a)) in self.selected_attributes.iter().enumerate() {
|
||||
for (j, (qname_b, member_b)) in self.selected_attributes.iter().enumerate() {
|
||||
if i == j {
|
||||
continue;
|
||||
}
|
||||
// Flip a and b around if a is later than b.
|
||||
// This way, the error message is the same for both
|
||||
// conflicting fields. Note that we always take the span of
|
||||
// `a` though, so that the two errors point at different
|
||||
// fields.
|
||||
let span = member_a.span();
|
||||
let (member_a, member_b) = if i > j {
|
||||
(member_b, member_a)
|
||||
} else {
|
||||
(member_a, member_b)
|
||||
};
|
||||
if qname_a.namespace.is_some() != qname_b.namespace.is_some() {
|
||||
// cannot ever match.
|
||||
continue;
|
||||
}
|
||||
let Some((name_a, name_b)) = qname_a.name.as_ref().zip(qname_b.name.as_ref())
|
||||
else {
|
||||
panic!("selected attribute has no XML local name");
|
||||
};
|
||||
|
||||
let mut check = quote! {
|
||||
::xso::exports::const_str_eq(#name_a.as_str(), #name_b.as_str())
|
||||
};
|
||||
|
||||
let namespaces = qname_a.namespace.as_ref().zip(qname_b.namespace.as_ref());
|
||||
if let Some((ns_a, ns_b)) = namespaces {
|
||||
check.extend(quote! {
|
||||
&& ::xso::exports::const_str_eq(#ns_a, #ns_b)
|
||||
});
|
||||
};
|
||||
|
||||
let attr_a = if let Some(namespace_a) = qname_a.namespace.as_ref() {
|
||||
format!("{{{}}}{}", namespace_a, name_a)
|
||||
} else {
|
||||
format!("{}", name_a)
|
||||
};
|
||||
|
||||
let attr_b = if let Some(namespace_b) = qname_b.namespace.as_ref() {
|
||||
format!("{{{}}}{}", namespace_b, name_b)
|
||||
} else {
|
||||
format!("{}", name_b)
|
||||
};
|
||||
|
||||
let field_a = FieldName(&member_a).to_string();
|
||||
let field_b = FieldName(&member_b).to_string();
|
||||
|
||||
// By assigning the checks to a `const`, we ensure that they
|
||||
// are in fact evaluated at compile time, even if that constant
|
||||
// is never used.
|
||||
checks.extend(quote_spanned! {span=>
|
||||
const _: () = { if #check {
|
||||
panic!("member {} and member {} match the same XML attribute: {} == {}", #field_a, #field_b, #attr_a, #attr_b);
|
||||
} };
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
checks
|
||||
}
|
||||
|
||||
/// Make and return a set of states which is used to construct the target
|
||||
/// type from XML events.
|
||||
///
|
||||
|
|
@ -513,9 +619,12 @@ impl Compound {
|
|||
|
||||
let unknown_attribute_policy = &self.unknown_attribute_policy;
|
||||
|
||||
let checks = self.assert_disjunct_attributes();
|
||||
|
||||
Ok(FromEventsSubmachine {
|
||||
defs: quote! {
|
||||
#extra_defs
|
||||
#checks
|
||||
|
||||
struct #builder_data_ty {
|
||||
#builder_data_def
|
||||
|
|
@ -733,6 +842,9 @@ impl Compound {
|
|||
},
|
||||
};
|
||||
|
||||
let checks = self.assert_disjunct_attributes();
|
||||
extra_defs.extend(checks);
|
||||
|
||||
Ok(AsItemsSubmachine {
|
||||
defs: extra_defs,
|
||||
states,
|
||||
|
|
|
|||
Loading…
Reference in a new issue