xso_proc: Unify error message generation

This provides more consistent error messages and is the stepping stone
in an attempt to include as much as possible information in every error.
This commit is contained in:
Jonas Schäfer 2024-03-24 10:59:24 +01:00
parent 6bb518e6a6
commit 53ee8faa42
11 changed files with 212 additions and 102 deletions

View File

@ -7,9 +7,10 @@ in order to build the code to convert to/from XML nodes.
*/
use proc_macro2::{Span, TokenStream};
use quote::{quote, ToTokens};
use quote::quote;
use syn::{spanned::Spanned, *};
use crate::error_message::ParentRef;
use crate::field::{FieldDef, FieldParsePart};
use crate::meta::{Flag, Name, NamespaceRef, StaticNamespace, XmlCompoundMeta};
@ -304,14 +305,14 @@ impl Compound {
///
/// The `name` argument is used to produce a nice error message.
fn need_namespace<'x>(
name: Option<&Path>,
name: &ParentRef,
a: Option<&'x CompoundNamespace>,
b: Option<&'x CompoundNamespace>,
) -> Result<&'x CompoundNamespace> {
if let Some(namespace) = a.or(b) {
Ok(namespace)
} else {
Err(Error::new_spanned(name, format!("cannot determine namespace for struct or enum variant {}. use #[xml(namespace = ..)] on the variant or enum.", name.to_token_stream())))
Err(Error::new(name.span(), format!("cannot determine namespace for struct or enum variant {}. use #[xml(namespace = ..)] on the variant or enum.", name)))
}
}
@ -329,7 +330,7 @@ impl Compound {
/// declaration order.
pub(crate) fn build_try_from_element(
self,
name: Option<&Path>,
name: &ParentRef,
item_namespace: Option<&CompoundNamespace>,
residual: &Ident,
) -> Result<TokenStream> {
@ -401,12 +402,12 @@ impl Compound {
}
let construct = match name {
Some(name) => quote! {
#name {
ParentRef::Named(ref path) => quote! {
#path {
#init
}
},
None => quote! {
ParentRef::Unnamed { .. } => quote! {
( #tupinit )
},
};
@ -451,7 +452,7 @@ impl Compound {
/// if their enum has no namespace set.
pub(crate) fn build_into_element(
self,
item_name: Option<&Path>,
item_name: &ParentRef,
item_namespace: Option<&CompoundNamespace>,
mut access_field: impl FnMut(Member) -> Expr,
) -> Result<TokenStream> {
@ -481,7 +482,7 @@ impl Compound {
let mut build = quote! {};
for field in fields {
let field_build = field.build_into_element(&mut access_field)?;
let field_build = field.build_into_element(item_name, &mut access_field)?;
build = quote! {
#build
builder = #field_build;

View File

@ -250,7 +250,7 @@ impl EnumDef {
}
let variant_impl = variant.inner.build_try_from_element(
Some(&Path {
&(Path {
leading_colon: None,
segments: [
PathSegment::from(enum_ident.clone()),
@ -258,7 +258,8 @@ impl EnumDef {
]
.into_iter()
.collect(),
}),
}
.into()),
xml_namespace.as_ref(),
residual,
)?;
@ -334,7 +335,7 @@ impl EnumDef {
}
let into_element = variant.inner.build_into_element(
Some(&Path::from(ty_ident.clone())),
&(Path::from(ty_ident.clone()).into()),
self.namespace.as_ref(),
map_ident,
)?;

View File

@ -0,0 +1,133 @@
//! Infrastructure for contextual error messages
use std::fmt;
use proc_macro2::Span;
use quote::ToTokens;
use syn::{spanned::Spanned, Member, Path};
/// Reference to a compound's parent
///
/// This reference can be converted to a hopefully-useful human-readable
/// string via [`std::fmt::Display`].
#[derive(Clone)]
#[cfg_attr(feature = "debug", derive(Debug))]
pub(super) enum ParentRef {
/// The parent is addressable by a path, e.g. a struct type or enum
/// variant.
Named(Path),
/// The parent is not addressable.
///
/// This is typically the case for compounds created for
/// [`crate::field::child::ExtractDef`].
Unnamed {
/// The parent's ref.
///
/// For extracts, this refers to the compound where the field with
/// the extract is declared.
parent: Box<ParentRef>,
/// The field inside that parent.
///
/// For extracts, this refers to the compound field where the extract
/// is declared.
field: Member,
},
}
impl From<Path> for ParentRef {
fn from(other: Path) -> Self {
Self::Named(other)
}
}
impl<'x> From<&'x Path> for ParentRef {
fn from(other: &'x Path) -> Self {
Self::Named(other.clone())
}
}
impl ParentRef {
/// Create a new `ParentRef` for a member inside this one.
///
/// Returns a [`Self::Unnamed`] with `self` as parent and `member` as
/// field.
pub(crate) fn child(&self, member: Member) -> Self {
Self::Unnamed {
parent: Box::new(self.clone()),
field: member,
}
}
/// Return a span which can be used for error messages.
///
/// This points at the closest [`Self::Named`] variant in the parent
/// chain.
pub(crate) fn span(&self) -> Span {
match self {
Self::Named(p) => p.span(),
Self::Unnamed { parent, .. } => parent.span(),
}
}
}
impl fmt::Display for ParentRef {
fn fmt<'f>(&self, f: &'f mut fmt::Formatter) -> fmt::Result {
match self {
Self::Named(name) => write!(f, "{} element", name.to_token_stream()),
Self::Unnamed { parent, field } => {
write!(f, "extraction for child {} in {}", FieldName(field), parent)
}
}
}
}
/// Ephemeral struct to create a nice human-readable representation of
/// [`syn::Member`].
///
/// It implements [`std::fmt::Display`] for that purpose and is otherwise of
/// little use.
#[repr(transparent)]
struct FieldName<'x>(&'x Member);
impl<'x> fmt::Display for FieldName<'x> {
fn fmt<'f>(&self, f: &'f mut fmt::Formatter) -> fmt::Result {
match self.0 {
Member::Named(v) => v.fmt(f),
Member::Unnamed(v) => write!(f, "<unnamed field {}>", v.index),
}
}
}
/// Create a string error message for a missing child element.
///
/// `parent_name` should point at the compound which is being parsed and
/// `field` should be the field to which the child belongs.
pub(super) fn on_missing_child(parent_name: &ParentRef, field: &Member) -> String {
format!("Missing child {} in {}.", FieldName(&field), parent_name)
}
/// Create a string error message for a duplicate child element.
///
/// `parent_name` should point at the compound which is being parsed and
/// `field` should be the field to which the child belongs.
pub(super) fn on_duplicate_child(parent_name: &ParentRef, field: &Member) -> String {
format!(
"{} must not have more than one {} child.",
parent_name,
FieldName(&field)
)
}
/// Create a string error message for a missing attribute.
///
/// `parent_name` should point at the compound which is being parsed and
/// `field` should be the field to which the attribute belongs.
pub(super) fn on_missing_attribute(parent_name: &ParentRef, field: &Member) -> String {
format!(
"Required attribute {} on {} missing.",
FieldName(&field),
parent_name
)
}

View File

@ -4,6 +4,7 @@ use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::*;
use crate::error_message::{self, ParentRef};
use crate::meta::{Flag, Name, NameRef};
use super::FieldParsePart;
@ -67,13 +68,13 @@ impl AttributeField {
/// ignored.
pub(super) fn build_try_from_element(
self,
_name: Option<&Path>,
name: &ParentRef,
_tempname: Ident,
_ident: Member,
ident: Member,
ty: Type,
) -> Result<FieldParsePart> {
let missing_msg = error_message::on_missing_attribute(name, &ident);
let name = self.name;
let missing_msg = format!("Required attribute '{}' missing.", name);
let on_missing = if self.default_on_missing.is_set() {
quote! {
<#ty as ::std::default::Default>::default()

View File

@ -2,13 +2,14 @@
//! their contents.
use proc_macro2::{Span, TokenStream};
use quote::{quote, ToTokens};
use quote::quote;
use syn::*;
use crate::compound::Compound;
use crate::error_message::{self, ParentRef};
use crate::meta::{ExtractMeta, Flag, NameRef, NamespaceRef, StaticNamespace, XmlCompoundMeta};
use super::{field_name, ChildMode, FieldDef, FieldParsePart};
use super::{ChildMode, FieldDef, FieldParsePart};
/// Definition of a child data extraction.
///
@ -98,7 +99,12 @@ impl ExtractDef {
/// them as "limitations" in the API/attribute reference).
///
/// It works nicely though.
fn build_extract(self, residual: &Ident, target_ty: Option<&Type>) -> Result<TokenStream> {
fn build_extract(
self,
name: &ParentRef,
residual: &Ident,
target_ty: Option<&Type>,
) -> Result<TokenStream> {
let nfields = self.parts.field_count().unwrap();
let repack = if nfields == 1 {
@ -126,7 +132,7 @@ impl ExtractDef {
quote! { data }
};
let build = self.parts.build_try_from_element(None, None, residual)?;
let build = self.parts.build_try_from_element(name, None, residual)?;
Ok(quote! {
match #build {
@ -152,7 +158,12 @@ impl ExtractDef {
///
/// Otherwise, the value is used nearly as-is and forwarded to the
/// implementation returned by [`Compound::build_into_element`].
fn build_assemble(self, field: &Expr, target_ty: Option<&Type>) -> Result<TokenStream> {
fn build_assemble(
self,
name: &ParentRef,
field: &Expr,
target_ty: Option<&Type>,
) -> Result<TokenStream> {
let nfields = self.parts.field_count().unwrap();
let repack = if nfields == 1 {
@ -189,7 +200,7 @@ impl ExtractDef {
quote! { Some(#field) }
};
let build = self.parts.build_into_element(None, None, |member| {
let build = self.parts.build_into_element(name, None, |member| {
Expr::Field(ExprField {
attrs: Vec::new(),
dot_token: syn::token::Dot {
@ -349,23 +360,15 @@ impl ChildField {
/// element(s).
pub(super) fn build_try_from_element(
self,
name: Option<&Path>,
name: &ParentRef,
tempname: Ident,
ident: Member,
ty: Type,
) -> Result<FieldParsePart> {
match self.mode {
ChildMode::Single => {
let missingerr = format!(
"Missing child {} in {} element.",
field_name(&ident),
name.to_token_stream()
);
let duperr = format!(
"Element {} must not have more than one {} child.",
name.to_token_stream(),
field_name(&ident)
);
let missingerr = error_message::on_missing_child(name, &ident);
let duperr = error_message::on_duplicate_child(name, &ident);
let on_missing = if self.default_on_missing.is_set() {
quote! {
<#ty as ::std::default::Default>::default()
@ -377,8 +380,11 @@ impl ChildField {
};
match self.extract {
Some(extract) => {
let extract = extract
.build_extract(&Ident::new("residual", Span::call_site()), Some(&ty))?;
let extract = extract.build_extract(
&name.child(ident),
&Ident::new("residual", Span::call_site()),
Some(&ty),
)?;
Ok(FieldParsePart {
tempinit: quote! {
@ -438,8 +444,11 @@ impl ChildField {
}
ChildMode::Collection => match self.extract {
Some(extract) => {
let extract =
extract.build_extract(&Ident::new("residual", Span::call_site()), None)?;
let extract = extract.build_extract(
&name.child(ident),
&Ident::new("residual", Span::call_site()),
None,
)?;
let item_ty: Type = syn::parse2(quote! {
<#ty as IntoIterator>::Item
})
@ -496,11 +505,18 @@ impl ChildField {
/// - `ident` must be an expression to consume the field's data. It is
/// evaluated exactly once.
/// - `ty` must be the field's type.
pub(super) fn build_into_element(self, ident: Expr, ty: Type) -> Result<TokenStream> {
pub(super) fn build_into_element(
self,
name: &ParentRef,
member: Member,
ident: Expr,
ty: Type,
) -> Result<TokenStream> {
match self.mode {
ChildMode::Single => match self.extract {
Some(extract) => {
let assemble = extract.build_assemble(&ident, Some(&ty))?;
let assemble =
extract.build_assemble(&name.child(member), &ident, Some(&ty))?;
Ok(quote! {
match #assemble {
Some(el) => builder.append(::xso::exports::minidom::Node::Element(el)),
@ -518,6 +534,7 @@ impl ChildField {
ChildMode::Collection => match self.extract {
Some(extract) => {
let assemble = extract.build_assemble(
&name.child(member),
&Expr::Path(ExprPath {
attrs: Vec::new(),
qself: None,

View File

@ -2,9 +2,10 @@
//! destructuring their contents.
use proc_macro2::{Span, TokenStream};
use quote::{quote, ToTokens};
use quote::quote;
use syn::*;
use crate::error_message::{self, ParentRef};
use crate::meta::{Flag, Name, NameRef, NamespaceRef, StaticNamespace};
use super::FieldParsePart;
@ -101,23 +102,15 @@ impl ElementField {
/// needs to implement `Default`.
pub(super) fn build_try_from_element(
self,
name: Option<&Path>,
name: &ParentRef,
tempname: Ident,
_ident: Member,
ident: Member,
ty: Type,
) -> Result<FieldParsePart> {
let field_name = self.name;
let field_namespace = self.namespace;
let missingerr = format!(
"Missing child {} in {} element.",
field_name,
name.to_token_stream()
);
let duperr = format!(
"Element {} must not have more than one {} child.",
name.to_token_stream(),
field_name,
);
let missingerr = error_message::on_missing_child(name, &ident);
let duperr = error_message::on_duplicate_child(name, &ident);
let on_missing = if self.default_on_missing.is_set() {
quote! {
<#ty as ::std::default::Default>::default()
@ -249,7 +242,7 @@ impl ElementsField {
/// otherwise compile-time errors will follow.
pub(super) fn build_try_from_element(
self,
_name: Option<&Path>,
_name: &ParentRef,
tempname: Ident,
_ident: Member,
_ty: Type,

View File

@ -1,28 +0,0 @@
use std::fmt;
pub(super) enum ParentRef {
Named(Path),
Unnamed {
parent: Box<ParentRef>,
field: Member,
},
}
impl fmt::Display for ParentRef {
fn fmt<'f>(&self, f: &'f mut fmt::Formatter) -> fmt::Result {
match self {
Self::Named(name) => write!(f, "{} element", name.to_token_stream()),
Self::Unnamed {
parent,
field,
} => write!(f, "extraction for child {} in {}", field, parent),
}
}
}
pub(super) fn on_missing_child(
field: &Member,
parent_name: ParentRef,
) -> String {
format!("Missing child {} in {}.", field_name(field), parent_name)
}

View File

@ -1,9 +1,10 @@
//! Infrastructure for parsing boolean fields indicating child presence.
use proc_macro2::{Span, TokenStream};
use quote::{quote, ToTokens};
use quote::quote;
use syn::*;
use crate::error_message::{self, ParentRef};
use crate::meta::{Name, NameRef, NamespaceRef, StaticNamespace};
use super::FieldParsePart;
@ -85,18 +86,14 @@ impl FlagField {
/// - `ty` must be the field's type. This must be `bool`.
pub(super) fn build_try_from_element(
self,
name: Option<&Path>,
name: &ParentRef,
tempname: Ident,
_ident: Member,
ident: Member,
_ty: Type,
) -> Result<FieldParsePart> {
let field_name = self.name;
let field_namespace = self.namespace;
let duperr = format!(
"Element {} must not have more than one {} child.",
name.to_token_stream(),
field_name,
);
let duperr = error_message::on_duplicate_child(name, &ident);
Ok(FieldParsePart {
tempinit: quote! {
let mut #tempname = false;

View File

@ -18,23 +18,15 @@ use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::{spanned::Spanned, *};
use crate::meta::{ChildMode, ExtractMeta, Flag, XmlFieldMeta};
use crate::compound::Compound;
use crate::error_message::ParentRef;
use crate::meta::{ChildMode, ExtractMeta, Flag, XmlFieldMeta};
use self::attribute::AttributeField;
use self::child::ChildField;
use self::element::{ElementField, ElementsField};
use self::flag::FlagField;
/// Provide some kind of human-useful string from a [`Member`].
fn field_name(m: &Member) -> String {
match m {
Member::Named(v) => v.to_string(),
Member::Unnamed(v) => format!("<unnamed {}>", v.index),
}
}
/// Code slices necessary for parsing a single field.
#[derive(Default)]
pub(crate) struct FieldParsePart {
@ -299,7 +291,7 @@ impl FieldDef {
/// Construct a [`FieldParsePart`] which creates the field's value from
/// XML.
pub(crate) fn build_try_from_element(self, name: Option<&Path>) -> Result<FieldParsePart> {
pub(crate) fn build_try_from_element(self, name: &ParentRef) -> Result<FieldParsePart> {
let ident = self.ident;
let ty = self.ty;
let tempname = quote::format_ident!("__field_init_{}", ident);
@ -340,8 +332,10 @@ impl FieldDef {
/// `access_field` and the `builder` ident are accessible.
pub(crate) fn build_into_element(
self,
name: &ParentRef,
mut access_field: impl FnMut(Member) -> Expr,
) -> Result<TokenStream> {
let member = self.ident.clone();
let ident = access_field(self.ident);
let ty = self.ty;
match self.kind {
@ -356,7 +350,7 @@ impl FieldDef {
}
}
}),
FieldKind::Child(field) => field.build_into_element(ident, ty),
FieldKind::Child(field) => field.build_into_element(name, member, ident, ty),
FieldKind::Element(field) => field.build_into_element(ident, ty),
FieldKind::Elements(field) => field.build_into_element(ident, ty),
FieldKind::Flag(field) => field.build_into_element(ident, ty),

View File

@ -41,6 +41,7 @@ The processing is roughly grouped in the following stages:
mod common;
mod compound;
mod enums;
mod error_message;
mod field;
mod meta;
mod structs;

View File

@ -68,7 +68,7 @@ pub(crate) fn try_from_element(item: syn::ItemStruct) -> Result<proc_macro2::Tok
let ident = item.ident;
let def = StructDef::new(meta, &item.fields)?;
let try_from_impl = def.inner.build_try_from_element(
Some(&Path::from(ident.clone())),
&(Path::from(ident.clone()).into()),
None,
&Ident::new("residual", Span::call_site()),
)?;
@ -116,7 +116,7 @@ pub(crate) fn into_element(item: syn::ItemStruct) -> Result<proc_macro2::TokenSt
let def = StructDef::new(meta, &item.fields)?;
let into_impl =
def.inner
.build_into_element(Some(&Path::from(ident.clone())), None, |member| {
.build_into_element(&(Path::from(ident.clone()).into()), None, |member| {
Expr::Field(ExprField {
attrs: Vec::new(),
dot_token: syn::token::Dot {