tamer: xir::parse::attr: Error and recovery on duplicate attr
This was a TODO for the attribute parser generator. The first attribute will be kept and later ones will be ignored, producing an error. Recovery permits further attribute parsing having ignored the duplicate. DEV-7145main
parent
21dfff0110
commit
ce765d3b56
|
@ -41,11 +41,13 @@ use crate::{
|
|||
diagnose::{Annotate, AnnotatedSpan, Diagnostic},
|
||||
fmt::ListDisplayWrapper,
|
||||
parse::ParseState,
|
||||
span::Span,
|
||||
xir::{attr::Attr, fmt::XmlAttrList, EleSpan, OpenSpan, QName},
|
||||
};
|
||||
use std::{error::Error, fmt::Display};
|
||||
|
||||
pub type ElementQName = QName;
|
||||
pub type FirstSpan = Span;
|
||||
|
||||
/// Error while parsing element attributes.
|
||||
#[derive(Debug, PartialEq)]
|
||||
|
@ -73,6 +75,19 @@ pub enum AttrParseError<S: AttrParseState> {
|
|||
///
|
||||
/// Parsing may recover by simply ignoring this attribute.
|
||||
UnexpectedAttr(Attr, ElementQName),
|
||||
|
||||
/// An attribute with the same name as a previous attribute has been
|
||||
/// encountered within the context of this element.
|
||||
///
|
||||
/// The duplicate attribute is provided in its entirety.
|
||||
/// The key span of the first-encountered attribute of the same name is
|
||||
/// included to provide more robust diagnostic information.
|
||||
/// The value of the previous attribute is not included because it is
|
||||
/// expected that the diagnostic system will render the code
|
||||
/// associated with the span;
|
||||
/// displaying an attribute value in an error message is asking for
|
||||
/// too much trouble given that it is arbitrary text.
|
||||
DuplicateAttr(Attr, FirstSpan, ElementQName),
|
||||
}
|
||||
|
||||
impl<S: AttrParseState> Display for AttrParseError<S> {
|
||||
|
@ -88,7 +103,16 @@ impl<S: AttrParseState> Display for AttrParseError<S> {
|
|||
Self::UnexpectedAttr(attr, ele_name) => {
|
||||
write!(
|
||||
f,
|
||||
"element `{ele_name}` contains unexpected attribute `{attr}`"
|
||||
"unexpected attribute `{attr}` for \
|
||||
element element `{ele_name}`"
|
||||
)
|
||||
}
|
||||
|
||||
Self::DuplicateAttr(attr, _, ele_name) => {
|
||||
write!(
|
||||
f,
|
||||
"duplicate attribute `{attr}` for \
|
||||
element element `{ele_name}`"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -103,6 +127,8 @@ impl<S: AttrParseState> Error for AttrParseError<S> {
|
|||
|
||||
impl<S: AttrParseState> Diagnostic for AttrParseError<S> {
|
||||
fn describe(&self) -> Vec<AnnotatedSpan> {
|
||||
use crate::fmt::{DisplayWrapper, TtQuote};
|
||||
|
||||
match self {
|
||||
Self::MissingRequired(st) => st
|
||||
.element_span()
|
||||
|
@ -118,6 +144,19 @@ impl<S: AttrParseState> Diagnostic for AttrParseError<S> {
|
|||
.key_span()
|
||||
.error(format!("element `{ele_name}` cannot contain `{attr}`"))
|
||||
.into(),
|
||||
|
||||
Self::DuplicateAttr(Attr(name, _, aspan), first_span, _) => {
|
||||
vec![
|
||||
first_span.note(format!(
|
||||
"{} previously encountered here",
|
||||
TtQuote::wrap(name)
|
||||
)),
|
||||
aspan.key_span().error(format!(
|
||||
"{} here is a duplicate",
|
||||
TtQuote::wrap(name)
|
||||
)),
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -200,7 +239,8 @@ macro_rules! attr_parse {
|
|||
#[doc(hidden)]
|
||||
___done: bool,
|
||||
$(
|
||||
pub $field: Option<$ty>,
|
||||
// Value + key span
|
||||
pub $field: Option<($ty, Span)>,
|
||||
)*
|
||||
}
|
||||
|
||||
|
@ -342,10 +382,33 @@ macro_rules! attr_parse {
|
|||
// forget to import a const for `$qname`.
|
||||
// We don't use `$qname:pat` because we reuse
|
||||
// `$qname` for error messages.
|
||||
flat::XirfToken::Attr(attr @ Attr(qn, ..)) if qn == $qname => {
|
||||
// TODO: Error on prev value
|
||||
self.$field.replace(attr.into());
|
||||
Transition(self).incomplete()
|
||||
flat::XirfToken::Attr(
|
||||
attr @ Attr(qn, _, AttrSpan(kspan, _))
|
||||
) if qn == $qname => {
|
||||
match self.$field {
|
||||
// Duplicate attribute name
|
||||
Some((_, first_kspan)) => {
|
||||
let ele_name = self.element_name();
|
||||
|
||||
Transition(self).err(
|
||||
AttrParseError::DuplicateAttr(
|
||||
attr,
|
||||
first_kspan,
|
||||
ele_name,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// First time seeing attribute name
|
||||
None => {
|
||||
self.$field.replace((
|
||||
attr.into(),
|
||||
kspan,
|
||||
));
|
||||
|
||||
Transition(self).incomplete()
|
||||
}
|
||||
}
|
||||
}
|
||||
)*
|
||||
|
||||
|
@ -402,7 +465,10 @@ macro_rules! attr_parse {
|
|||
// This does not produce a great error if the user forgets to use an
|
||||
// `Option` type for optional attributes,
|
||||
// but the comment is better than nothing.
|
||||
$from.$field.unwrap_or(None) // field type must be Option<T>
|
||||
match $from.$field { // field type must be Option<T>
|
||||
Some((value, _kspan)) => value,
|
||||
None => None,
|
||||
}
|
||||
};
|
||||
|
||||
// Otherwise,
|
||||
|
@ -412,7 +478,9 @@ macro_rules! attr_parse {
|
|||
// This assumes that we've already validated via
|
||||
// `@validate_req` above,
|
||||
// and so should never actually panic.
|
||||
$from.$field.unwrap()
|
||||
match $from.$field.unwrap() {
|
||||
(value, _kspan) => value
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ use crate::{
|
|||
};
|
||||
use std::assert_matches::assert_matches;
|
||||
|
||||
const S1: Span = DUMMY_SPAN;
|
||||
const S1: Span = DUMMY_SPAN.offset_add(1).unwrap();
|
||||
const S2: Span = S1.offset_add(1).unwrap();
|
||||
const S3: Span = S2.offset_add(1).unwrap();
|
||||
const SE: OpenSpan = OpenSpan(S1.offset_add(100).unwrap(), 0);
|
||||
|
@ -294,10 +294,10 @@ mod required {
|
|||
err,
|
||||
ParseError::StateError(AttrParseError::MissingRequired(
|
||||
ReqMissingState {
|
||||
name: Some(ref given_name),
|
||||
name: Some((ref given_name, _)),
|
||||
src: None, // cause of the error
|
||||
ty: None, // another cause of the error
|
||||
yields: Some(ref given_yields),
|
||||
yields: Some((ref given_yields, _)),
|
||||
..
|
||||
},
|
||||
)) if given_name == &ATTR_NAME
|
||||
|
@ -313,8 +313,8 @@ mod required {
|
|||
// `required_missing_values` above verifies that this state is what
|
||||
// is in fact constructed from a failed parsing attempt.
|
||||
let mut partial = ReqMissingState::with_element(QN_ELE, SE);
|
||||
partial.name.replace(ATTR_NAME);
|
||||
partial.yields.replace(ATTR_YIELDS);
|
||||
partial.name.replace((ATTR_NAME, S1));
|
||||
partial.yields.replace((ATTR_YIELDS, S2));
|
||||
|
||||
let err = AttrParseError::MissingRequired(partial);
|
||||
|
||||
|
@ -346,8 +346,8 @@ mod required {
|
|||
#[test]
|
||||
fn diagnostic_message_contains_all_required_missing_attr_name() {
|
||||
let mut partial = ReqMissingState::with_element(QN_ELE, SE);
|
||||
partial.name.replace(ATTR_NAME);
|
||||
partial.yields.replace(ATTR_YIELDS);
|
||||
partial.name.replace((ATTR_NAME, S1));
|
||||
partial.yields.replace((ATTR_YIELDS, S2));
|
||||
|
||||
let err = AttrParseError::MissingRequired(partial);
|
||||
let desc = err.describe();
|
||||
|
@ -430,3 +430,75 @@ fn unexpected_attr_with_recovery() {
|
|||
parse_aggregate_with(&mut sut),
|
||||
);
|
||||
}
|
||||
|
||||
// A duplicate attribute will result in an error,
|
||||
// and recovery will cause the duplicate to be ignored.
|
||||
#[test]
|
||||
fn duplicate_attr_with_recovery() {
|
||||
attr_parse! {
|
||||
struct DupState -> Dup {
|
||||
name: (QN_NAME) => Attr,
|
||||
src: (QN_SRC) => Attr,
|
||||
}
|
||||
}
|
||||
|
||||
let attr_keep = Attr(QN_NAME, "keep me".into(), AttrSpan(S1, S2));
|
||||
let attr_dup = Attr(QN_NAME, "duplicate".into(), AttrSpan(S2, S3));
|
||||
let attr_src = Attr(QN_SRC, "val_src".into(), AttrSpan(S3, S1));
|
||||
let tok_dead = close_empty(S3, Depth(0));
|
||||
|
||||
let toks = vec![
|
||||
// Both of these have the same name (@name).
|
||||
XirfToken::Attr(attr_keep.clone()),
|
||||
XirfToken::Attr(attr_dup.clone()),
|
||||
// Another attribute just to show that error recovery permits
|
||||
// further attribute parsing.
|
||||
XirfToken::Attr(attr_src.clone()),
|
||||
// Will cause dead state:
|
||||
tok_dead.clone(),
|
||||
]
|
||||
.into_iter();
|
||||
|
||||
let mut sut = Parser::with_state(DupState::with_element(QN_ELE, SE), toks);
|
||||
|
||||
// The first token is good,
|
||||
// since we haven't encountered the attribute yet.
|
||||
assert_eq!(sut.next(), Some(Ok(Parsed::Incomplete)));
|
||||
|
||||
// The second one results in an error,
|
||||
// since the name is the same.
|
||||
let err = sut
|
||||
.next()
|
||||
.unwrap()
|
||||
.expect_err("DuplicateAttr error expected");
|
||||
|
||||
assert_eq!(
|
||||
ParseError::StateError(AttrParseError::DuplicateAttr(
|
||||
attr_dup,
|
||||
attr_keep.attr_span().key_span(),
|
||||
QN_ELE,
|
||||
)),
|
||||
err,
|
||||
);
|
||||
|
||||
// The diagnostic description of this error should contain first a
|
||||
// reference to the original attribute,
|
||||
// and then a reference to the duplicate.
|
||||
let desc = err.describe();
|
||||
assert_eq!(desc[0].span(), S1);
|
||||
assert_eq!(desc[1].span(), S2);
|
||||
|
||||
// Once parsing is completed,
|
||||
// we must have kept the first occurrence of the attribute and
|
||||
// discarded the second.
|
||||
assert_eq!(
|
||||
Ok((
|
||||
Dup {
|
||||
name: attr_keep,
|
||||
src: attr_src,
|
||||
},
|
||||
tok_dead
|
||||
)),
|
||||
parse_aggregate_with(&mut sut),
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue