Merge pull request #501 from Kijewski/pr-context_error

parser: un-inline error message generation
This commit is contained in:
Guillaume Gomez 2025-06-24 23:20:33 +02:00 committed by GitHub
commit 73bbfa9200
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 174 additions and 240 deletions

View File

@ -11,8 +11,8 @@ use winnow::token::take_until;
use crate::node::CondTest; use crate::node::CondTest;
use crate::{ use crate::{
CharLit, ErrorContext, Level, Num, ParseErr, ParseResult, PathOrIdentifier, Span, StrLit, CharLit, Level, Num, ParseResult, PathOrIdentifier, Span, StrLit, StrPrefix, WithSpan,
StrPrefix, WithSpan, char_lit, filter, identifier, keyword, not_suffix_with_hash, num_lit, char_lit, cut_error, filter, identifier, keyword, not_suffix_with_hash, num_lit,
path_or_identifier, skip_ws0, skip_ws1, str_lit, ws, path_or_identifier, skip_ws0, skip_ws1, str_lit, ws,
}; };
@ -50,22 +50,22 @@ struct Allowed {
super_keyword: bool, super_keyword: bool,
} }
fn check_expr<'a>(expr: &WithSpan<'a, Expr<'a>>, allowed: Allowed) -> Result<(), ParseErr<'a>> { fn check_expr<'a>(expr: &WithSpan<'a, Expr<'a>>, allowed: Allowed) -> ParseResult<'a, ()> {
match &expr.inner { match &expr.inner {
&Expr::Var(name) => { &Expr::Var(name) => {
// List can be found in rust compiler "can_be_raw" function (although in our case, it's // List can be found in rust compiler "can_be_raw" function (although in our case, it's
// also used in cases like `match`, so `self` is allowed in this case). // also used in cases like `match`, so `self` is allowed in this case).
if (!allowed.super_keyword && name == "super") || matches!(name, "crate" | "Self") { if (!allowed.super_keyword && name == "super") || matches!(name, "crate" | "Self") {
Err(err_reserved_identifier(name)) err_reserved_identifier(name)
} else if !allowed.underscore && name == "_" { } else if !allowed.underscore && name == "_" {
Err(err_underscore_identifier(name)) err_underscore_identifier(name)
} else { } else {
Ok(()) Ok(())
} }
} }
&Expr::IsDefined(var) | &Expr::IsNotDefined(var) => { &Expr::IsDefined(var) | &Expr::IsNotDefined(var) => {
if var == "_" { if var == "_" {
Err(err_underscore_identifier(var)) err_underscore_identifier(var)
} else { } else {
Ok(()) Ok(())
} }
@ -73,7 +73,7 @@ fn check_expr<'a>(expr: &WithSpan<'a, Expr<'a>>, allowed: Allowed) -> Result<(),
Expr::Path(path) => { Expr::Path(path) => {
if let &[name] = path.as_slice() { if let &[name] = path.as_slice() {
if !crate::can_be_variable_name(name) { if !crate::can_be_variable_name(name) {
return Err(err_reserved_identifier(name)); return err_reserved_identifier(name);
} }
} }
Ok(()) Ok(())
@ -86,9 +86,9 @@ fn check_expr<'a>(expr: &WithSpan<'a, Expr<'a>>, allowed: Allowed) -> Result<(),
} }
Expr::AssociatedItem(elem, associated_item) => { Expr::AssociatedItem(elem, associated_item) => {
if associated_item.name == "_" { if associated_item.name == "_" {
Err(err_underscore_identifier(associated_item.name)) err_underscore_identifier(associated_item.name)
} else if !crate::can_be_variable_name(associated_item.name) { } else if !crate::can_be_variable_name(associated_item.name) {
Err(err_reserved_identifier(associated_item.name)) err_reserved_identifier(associated_item.name)
} else { } else {
check_expr(elem, Allowed::default()) check_expr(elem, Allowed::default())
} }
@ -126,7 +126,7 @@ fn check_expr<'a>(expr: &WithSpan<'a, Expr<'a>>, allowed: Allowed) -> Result<(),
} }
Ok(()) Ok(())
} }
Expr::ArgumentPlaceholder => Err(ErrMode::Cut(ErrorContext::new("unreachable", expr.span))), Expr::ArgumentPlaceholder => cut_error!("unreachable", expr.span),
Expr::BoolLit(_) Expr::BoolLit(_)
| Expr::NumLit(_, _) | Expr::NumLit(_, _)
| Expr::StrLit(_) | Expr::StrLit(_)
@ -140,18 +140,14 @@ fn check_expr<'a>(expr: &WithSpan<'a, Expr<'a>>, allowed: Allowed) -> Result<(),
} }
} }
fn err_underscore_identifier(name: &str) -> ErrMode<ErrorContext<'_>> { #[inline(always)]
ErrMode::Cut(ErrorContext::new( fn err_underscore_identifier<T>(name: &str) -> ParseResult<'_, T> {
"reserved keyword `_` cannot be used here", cut_error!("reserved keyword `_` cannot be used here", name)
name,
))
} }
fn err_reserved_identifier(name: &str) -> ErrMode<ErrorContext<'_>> { #[inline(always)]
ErrMode::Cut(ErrorContext::new( fn err_reserved_identifier<T>(name: &str) -> ParseResult<'_, T> {
format!("`{name}` cannot be used as an identifier"), cut_error!(format!("`{name}` cannot be used as an identifier"), name)
name,
))
} }
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
@ -244,10 +240,7 @@ impl<'a> Expr<'a> {
)) ))
.parse_next(i)?; .parse_next(i)?;
if has_named_arguments && !matches!(*expr, Self::NamedArgument(_, _)) { if has_named_arguments && !matches!(*expr, Self::NamedArgument(_, _)) {
Err(ErrMode::Cut(ErrorContext::new( cut_error!("named arguments must always be passed last", start)
"named arguments must always be passed last",
start,
)))
} else { } else {
Ok(expr) Ok(expr)
} }
@ -283,10 +276,10 @@ impl<'a> Expr<'a> {
start, start,
)) ))
} else { } else {
Err(ErrMode::Cut(ErrorContext::new( cut_error!(
format!("named argument `{argument}` was passed more than once"), format!("named argument `{argument}` was passed more than once"),
start, start
))) )
} }
} }
@ -357,13 +350,13 @@ impl<'a> Expr<'a> {
let expr = WithSpan::new(Expr::BinOp(Box::new(BinOp { op, lhs: expr, rhs })), start); let expr = WithSpan::new(Expr::BinOp(Box::new(BinOp { op, lhs: expr, rhs })), start);
if let Some((op2, _)) = opt(right).parse_next(i)? { if let Some((op2, _)) = opt(right).parse_next(i)? {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!(
format!( format!(
"comparison operators cannot be chained; \ "comparison operators cannot be chained; \
consider using explicit parentheses, e.g. `(_ {op} _) {op2} _`" consider using explicit parentheses, e.g. `(_ {op} _) {op2} _`"
), ),
op, op,
))); );
} }
Ok(expr) Ok(expr)
@ -386,10 +379,10 @@ impl<'a> Expr<'a> {
let data = opt((ws1, '~', ws1, |i: &mut _| Expr::muldivmod(i, level))).parse_next(i)?; let data = opt((ws1, '~', ws1, |i: &mut _| Expr::muldivmod(i, level))).parse_next(i)?;
if let Some((t1, _, t2, expr)) = data { if let Some((t1, _, t2, expr)) = data {
if t1.is_none() || t2.is_none() { if t1.is_none() || t2.is_none() {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!(
"the concat operator `~` must be surrounded by spaces", "the concat operator `~` must be surrounded by spaces",
start, start,
))); );
} }
Ok(Some(expr)) Ok(Some(expr))
} else { } else {
@ -426,18 +419,18 @@ impl<'a> Expr<'a> {
if crate::PRIMITIVE_TYPES.contains(&target) { if crate::PRIMITIVE_TYPES.contains(&target) {
return Ok(WithSpan::new(Self::As(Box::new(lhs), target), start)); return Ok(WithSpan::new(Self::As(Box::new(lhs), target), start));
} else if target.is_empty() { } else if target.is_empty() {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!(
"`as` operator expects the name of a primitive type on its right-hand side", "`as` operator expects the name of a primitive type on its right-hand side",
before_keyword.trim_start(), before_keyword.trim_start(),
))); );
} else { } else {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!(
format!( format!(
"`as` operator expects the name of a primitive type on its right-hand \ "`as` operator expects the name of a primitive type on its right-hand \
side, found `{target}`" side, found `{target}`"
), ),
before_keyword.trim_start(), before_keyword.trim_start(),
))); );
} }
} }
_ => { _ => {
@ -449,11 +442,11 @@ impl<'a> Expr<'a> {
let rhs = opt(terminated(opt(keyword("not")), ws(keyword("defined")))).parse_next(i)?; let rhs = opt(terminated(opt(keyword("not")), ws(keyword("defined")))).parse_next(i)?;
let ctor = match rhs { let ctor = match rhs {
None => { None => {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!(
"expected `defined` or `not defined` after `is`", "expected `defined` or `not defined` after `is`",
// We use `start` to show the whole `var is` thing instead of the current token. // We use `start` to show the whole `var is` thing instead of the current token.
start, start,
))); );
} }
Some(None) => Self::IsDefined, Some(None) => Self::IsDefined,
Some(Some(_)) => Self::IsNotDefined, Some(Some(_)) => Self::IsNotDefined,
@ -461,16 +454,13 @@ impl<'a> Expr<'a> {
let var_name = match *lhs { let var_name = match *lhs {
Self::Var(var_name) => var_name, Self::Var(var_name) => var_name,
Self::AssociatedItem(_, _) => { Self::AssociatedItem(_, _) => {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!(
"`is defined` operator can only be used on variables, not on their fields", "`is defined` operator can only be used on variables, not on their fields",
start, start,
))); );
} }
_ => { _ => {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!("`is defined` operator can only be used on variables", start);
"`is defined` operator can only be used on variables",
start,
)));
} }
}; };
Ok(WithSpan::new(ctor(var_name), start)) Ok(WithSpan::new(ctor(var_name), start))
@ -642,10 +632,7 @@ fn token_xor<'a>(i: &mut &'a str) -> ParseResult<'a> {
if good { if good {
Ok("^") Ok("^")
} else { } else {
Err(ErrMode::Cut(ErrorContext::new( cut_error!("the binary XOR operator is called `xor` in askama", *i)
"the binary XOR operator is called `xor` in askama",
*i,
)))
} }
} }
@ -654,10 +641,7 @@ fn token_bitand<'a>(i: &mut &'a str) -> ParseResult<'a> {
if good { if good {
Ok("&") Ok("&")
} else { } else {
Err(ErrMode::Cut(ErrorContext::new( cut_error!("the binary AND operator is called `bitand` in askama", *i)
"the binary AND operator is called `bitand` in askama",
*i,
)))
} }
} }
@ -784,10 +768,7 @@ impl<'a> Suffix<'a> {
let before = *i; let before = *i;
let (token, token_span) = ws(opt(token).with_taken()).parse_next(i)?; let (token, token_span) = ws(opt(token).with_taken()).parse_next(i)?;
let Some(token) = token else { let Some(token) = token else {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!("expected valid tokens in macro call", token_span);
"expected valid tokens in macro call",
token_span,
)));
}; };
let close_token = match token { let close_token = match token {
Token::SomeOther => continue, Token::SomeOther => continue,
@ -800,14 +781,14 @@ impl<'a> Suffix<'a> {
let open_token = open_list.pop().unwrap(); let open_token = open_list.pop().unwrap();
if open_token != close_token { if open_token != close_token {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!(
format!( format!(
"expected `{}` but found `{}`", "expected `{}` but found `{}`",
open_token.as_close_char(), open_token.as_close_char(),
close_token.as_close_char(), close_token.as_close_char(),
), ),
token_span, token_span,
))); );
} else if open_list.is_empty() { } else if open_list.is_empty() {
return Ok(Suffix::MacroCall(&start[..start.len() - before.len()])); return Ok(Suffix::MacroCall(&start[..start.len() - before.len()]));
} }
@ -844,13 +825,13 @@ impl<'a> Suffix<'a> {
)) ))
.parse_next(i)?; .parse_next(i)?;
if opt(take_until(.., '\n')).parse_next(i)?.is_none() { if opt(take_until(.., '\n')).parse_next(i)?.is_none() {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!(
format!( format!(
"you are probably missing a line break to end {}comment", "you are probably missing a line break to end {}comment",
if is_doc_comment { "doc " } else { "" } if is_doc_comment { "doc " } else { "" }
), ),
start, start,
))); );
} }
Ok(()) Ok(())
} }
@ -864,13 +845,13 @@ impl<'a> Suffix<'a> {
)) ))
.parse_next(i)?; .parse_next(i)?;
if opt(take_until(.., "*/")).parse_next(i)?.is_none() { if opt(take_until(.., "*/")).parse_next(i)?.is_none() {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!(
format!( format!(
"missing `*/` to close block {}comment", "missing `*/` to close block {}comment",
if is_doc_comment { "doc " } else { "" } if is_doc_comment { "doc " } else { "" }
), ),
start, start,
))); );
} }
Ok(()) Ok(())
} }
@ -881,10 +862,10 @@ impl<'a> Suffix<'a> {
let prefix = identifier.parse_next(i)?; let prefix = identifier.parse_next(i)?;
let hashes: usize = repeat(.., '#').parse_next(i)?; let hashes: usize = repeat(.., '#').parse_next(i)?;
if hashes >= 256 { if hashes >= 256 {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!(
"a maximum of 255 hashes `#` are allowed with raw strings", "a maximum of 255 hashes `#` are allowed with raw strings",
prefix, prefix,
))); );
} }
let str_kind = match prefix { let str_kind = match prefix {
@ -897,10 +878,10 @@ impl<'a> Suffix<'a> {
_ if hashes == 0 => return Ok(()), _ if hashes == 0 => return Ok(()),
// reserved prefix: reject // reserved prefix: reject
_ => { _ => {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!(
format!("reserved prefix `{}#`", prefix.escape_debug()), format!("reserved prefix `{}#`", prefix.escape_debug()),
prefix, prefix,
))); );
} }
}; };
@ -908,10 +889,7 @@ impl<'a> Suffix<'a> {
// got a raw string // got a raw string
let Some((inner, j)) = i.split_once(&format!("\"{:#<hashes$}", "")) else { let Some((inner, j)) = i.split_once(&format!("\"{:#<hashes$}", "")) else {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!("unterminated raw string", prefix);
"unterminated raw string",
prefix,
)));
}; };
*i = j; *i = j;
@ -927,7 +905,7 @@ impl<'a> Suffix<'a> {
None => None, None => None,
}; };
if let Some(msg) = msg { if let Some(msg) = msg {
return Err(ErrMode::Cut(ErrorContext::new(msg, prefix))); return cut_error!(msg, prefix);
} }
not_suffix_with_hash(i)?; not_suffix_with_hash(i)?;
@ -940,31 +918,31 @@ impl<'a> Suffix<'a> {
if str_kind.is_some() { if str_kind.is_some() {
// an invalid raw identifier like `cr#async` // an invalid raw identifier like `cr#async`
Err(ErrMode::Cut(ErrorContext::new( cut_error!(
format!( format!(
"reserved prefix `{}#`, only `r#` is allowed with raw identifiers", "reserved prefix `{}#`, only `r#` is allowed with raw identifiers",
prefix.escape_debug(), prefix.escape_debug(),
), ),
prefix, prefix,
))) )
} else if hashes > 1 { } else if hashes > 1 {
// an invalid raw identifier like `r##async` // an invalid raw identifier like `r##async`
Err(ErrMode::Cut(ErrorContext::new( cut_error!(
"only one `#` is allowed in raw identifier delimitation", "only one `#` is allowed in raw identifier delimitation",
prefix, prefix,
))) )
} else { } else {
// a raw identifier like `r#async` // a raw identifier like `r#async`
Ok(()) Ok(())
} }
} else { } else {
Err(ErrMode::Cut(ErrorContext::new( cut_error!(
format!( format!(
"prefix `{}#` is only allowed with raw identifiers and raw strings", "prefix `{}#` is only allowed with raw identifiers and raw strings",
prefix.escape_debug(), prefix.escape_debug(),
), ),
prefix, prefix,
))) )
} }
} }
@ -972,10 +950,10 @@ impl<'a> Suffix<'a> {
let start = *i; let start = *i;
'#'.parse_next(i)?; '#'.parse_next(i)?;
if opt('"').parse_next(i)?.is_some() { if opt('"').parse_next(i)?.is_some() {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!(
"unprefixed guarded string literals are reserved for future use", "unprefixed guarded string literals are reserved for future use",
start, start,
))); );
} }
Ok(Token::SomeOther) Ok(Token::SomeOther)
} }
@ -1044,10 +1022,7 @@ impl<'a> Suffix<'a> {
|i: &mut _| { |i: &mut _| {
let name = alt((digit1, identifier)).parse_next(i)?; let name = alt((digit1, identifier)).parse_next(i)?;
if !crate::can_be_variable_name(name) { if !crate::can_be_variable_name(name) {
Err(ErrMode::Cut(ErrorContext::new( cut_error!(format!("`{name}` cannot be used as an identifier"), name)
format!("`{name}` cannot be used as an identifier"),
name,
)))
} else { } else {
Ok(name) Ok(name)
} }
@ -1094,10 +1069,9 @@ impl<'a> Suffix<'a> {
fn ensure_macro_name(name: &str) -> ParseResult<'_, ()> { fn ensure_macro_name(name: &str) -> ParseResult<'_, ()> {
match name { match name {
"crate" | "super" | "Self" | "self" => Err(ErrMode::Cut(ErrorContext::new( "crate" | "super" | "Self" | "self" => {
format!("`{name}` is not a valid macro name"), cut_error!(format!("`{name}` is not a valid macro name"), name)
name, }
))),
_ => Ok(()), _ => Ok(()),
} }
} }
@ -1122,17 +1096,17 @@ impl<'i> TyGenerics<'i> {
if let &[name] = path.as_slice() { if let &[name] = path.as_slice() {
if matches!(name, "super" | "self" | "crate") { if matches!(name, "super" | "self" | "crate") {
// `Self` and `_` are allowed // `Self` and `_` are allowed
return Err(err_reserved_identifier(name)); return err_reserved_identifier(name);
} }
} else { } else {
for (idx, &name) in path.iter().enumerate() { for (idx, &name) in path.iter().enumerate() {
if name == "_" { if name == "_" {
// `_` is never allowed // `_` is never allowed
return Err(err_underscore_identifier(name)); return err_underscore_identifier(name);
} else if idx > 0 && matches!(name, "super" | "self" | "Self" | "crate") { } else if idx > 0 && matches!(name, "super" | "self" | "Self" | "crate") {
// At the front of the path, "super" | "self" | "Self" | "crate" are allowed. // At the front of the path, "super" | "self" | "Self" | "crate" are allowed.
// Inside the path, they are not allowed. // Inside the path, they are not allowed.
return Err(err_reserved_identifier(name)); return err_reserved_identifier(name);
} }
} }
} }

View File

@ -21,7 +21,7 @@ use winnow::ascii::take_escaped;
use winnow::combinator::{ use winnow::combinator::{
alt, cut_err, delimited, fail, not, opt, peek, preceded, repeat, terminated, alt, cut_err, delimited, fail, not, opt, peek, preceded, repeat, terminated,
}; };
use winnow::error::FromExternalError; use winnow::error::{ErrMode, FromExternalError};
use winnow::stream::{AsChar, Stream as _}; use winnow::stream::{AsChar, Stream as _};
use winnow::token::{any, none_of, one_of, take_till, take_while}; use winnow::token::{any, none_of, one_of, take_till, take_while};
use winnow::{ModalParser, Parser}; use winnow::{ModalParser, Parser};
@ -122,10 +122,10 @@ impl<'a> Ast<'a> {
}; };
match Node::parse_template(&mut src, &state) { match Node::parse_template(&mut src, &state) {
Ok(nodes) if src.is_empty() => Ok(Self { nodes }), Ok(nodes) if src.is_empty() => Ok(Self { nodes }),
Ok(_) | Err(winnow::error::ErrMode::Incomplete(_)) => unreachable!(), Ok(_) | Err(ErrMode::Incomplete(_)) => unreachable!(),
Err( Err(
winnow::error::ErrMode::Backtrack(ErrorContext { span, message, .. }) ErrMode::Backtrack(ErrorContext { span, message, .. })
| winnow::error::ErrMode::Cut(ErrorContext { span, message, .. }), | ErrMode::Cut(ErrorContext { span, message, .. }),
) => Err(ParseError { ) => Err(ParseError {
message, message,
offset: span.offset_from(start).unwrap_or_default(), offset: span.offset_from(start).unwrap_or_default(),
@ -287,7 +287,7 @@ impl fmt::Display for ParseError {
} }
} }
pub(crate) type ParseErr<'a> = winnow::error::ErrMode<ErrorContext<'a>>; pub(crate) type ParseErr<'a> = ErrMode<ErrorContext<'a>>;
pub(crate) type ParseResult<'a, T = &'a str> = Result<T, ParseErr<'a>>; pub(crate) type ParseResult<'a, T = &'a str> = Result<T, ParseErr<'a>>;
/// This type is used to handle `nom` errors and in particular to add custom error messages. /// This type is used to handle `nom` errors and in particular to add custom error messages.
@ -313,12 +313,12 @@ impl<'a> ErrorContext<'a> {
} }
} }
fn backtrack(self) -> winnow::error::ErrMode<Self> { fn backtrack(self) -> ErrMode<Self> {
winnow::error::ErrMode::Backtrack(self) ErrMode::Backtrack(self)
} }
fn cut(self) -> winnow::error::ErrMode<Self> { fn cut(self) -> ErrMode<Self> {
winnow::error::ErrMode::Cut(self) ErrMode::Cut(self)
} }
} }
@ -405,10 +405,7 @@ fn num_lit<'a>(i: &mut &'a str) -> ParseResult<'a, Num<'a>> {
{ {
Ok(value) Ok(value)
} else { } else {
Err(winnow::error::ErrMode::Cut(ErrorContext::new( cut_error!(format!("unknown {kind} suffix `{suffix}`"), start)
format!("unknown {kind} suffix `{suffix}`"),
start,
)))
} }
} }
@ -421,10 +418,7 @@ fn num_lit<'a>(i: &mut &'a str) -> ParseResult<'a, Num<'a>> {
.parse_next(i)?; .parse_next(i)?;
match opt(separated_digits(base, false)).parse_next(i)? { match opt(separated_digits(base, false)).parse_next(i)? {
Some(_) => Ok(()), Some(_) => Ok(()),
None => Err(winnow::error::ErrMode::Cut(ErrorContext::new( None => cut_error!(format!("expected digits after `{kind}`"), start),
format!("expected digits after `{kind}`"),
start,
))),
} }
}); });
@ -436,10 +430,12 @@ fn num_lit<'a>(i: &mut &'a str) -> ParseResult<'a, Num<'a>> {
let (kind, op) = (one_of(['e', 'E']), opt(one_of(['+', '-']))).parse_next(i)?; let (kind, op) = (one_of(['e', 'E']), opt(one_of(['+', '-']))).parse_next(i)?;
match opt(separated_digits(10, op.is_none())).parse_next(i)? { match opt(separated_digits(10, op.is_none())).parse_next(i)? {
Some(_) => Ok(()), Some(_) => Ok(()),
None => Err(winnow::error::ErrMode::Cut(ErrorContext::new( None => {
format!("expected decimal digits, `+` or `-` after exponent `{kind}`"), cut_error!(
start, format!("expected decimal digits, `+` or `-` after exponent `{kind}`"),
))), start,
)
}
} }
}) })
.parse_next(i)?; .parse_next(i)?;
@ -578,12 +574,12 @@ fn str_lit<'a>(i: &mut &'a str) -> ParseResult<'a, StrLit<'a>> {
if has_lf { if has_lf {
continue; continue;
} else { } else {
return Err(winnow::error::ErrMode::Cut(ErrorContext::new( return cut_error!(
"a bare CR (Mac linebreak) is not allowed in string literals, \ "a bare CR (Mac linebreak) is not allowed in string literals, \
use NL (Unix linebreak) or CRNL (Windows linebreak) instead, \ use NL (Unix linebreak) or CRNL (Windows linebreak) instead, \
or type `\\r` explicitly", or type `\\r` explicitly",
start, start,
))); );
} }
} }
Sequence::Close => break, Sequence::Close => break,
@ -611,16 +607,10 @@ fn str_lit<'a>(i: &mut &'a str) -> ParseResult<'a, StrLit<'a>> {
match u32::from_str_radix(code, 16).unwrap() { match u32::from_str_radix(code, 16).unwrap() {
0 => contains_null = true, 0 => contains_null = true,
0xd800..0xe000 => { 0xd800..0xe000 => {
return Err(winnow::error::ErrMode::Cut(ErrorContext::new( return cut_error!("unicode escape must not be a surrogate", start);
"unicode escape must not be a surrogate",
start,
)));
} }
0x110000.. => { 0x110000.. => {
return Err(winnow::error::ErrMode::Cut(ErrorContext::new( return cut_error!("unicode escape must be at most 10FFFF", start);
"unicode escape must be at most 10FFFF",
start,
)));
} }
128.. => contains_unicode_character = true, 128.. => contains_unicode_character = true,
_ => {} _ => {}
@ -653,10 +643,7 @@ fn str_lit<'a>(i: &mut &'a str) -> ParseResult<'a, StrLit<'a>> {
let lit = opt(terminated(inner.with_taken(), '"')).parse_next(i)?; let lit = opt(terminated(inner.with_taken(), '"')).parse_next(i)?;
let Some((mut lit, content)) = lit else { let Some((mut lit, content)) = lit else {
return Err(winnow::error::ErrMode::Cut(ErrorContext::new( return cut_error!("unclosed or broken string", start);
"unclosed or broken string",
start,
)));
}; };
lit.content = content; lit.content = content;
lit.prefix = prefix; lit.prefix = prefix;
@ -677,7 +664,7 @@ fn str_lit<'a>(i: &mut &'a str) -> ParseResult<'a, StrLit<'a>> {
None => lit.contains_high_ascii.then_some("out of range hex escape"), None => lit.contains_high_ascii.then_some("out of range hex escape"),
}; };
if let Some(msg) = msg { if let Some(msg) = msg {
return Err(winnow::error::ErrMode::Cut(ErrorContext::new(msg, start))); return cut_error!(msg, start);
} }
not_suffix_with_hash(i)?; not_suffix_with_hash(i)?;
@ -686,10 +673,10 @@ fn str_lit<'a>(i: &mut &'a str) -> ParseResult<'a, StrLit<'a>> {
fn not_suffix_with_hash<'a>(i: &mut &'a str) -> ParseResult<'a, ()> { fn not_suffix_with_hash<'a>(i: &mut &'a str) -> ParseResult<'a, ()> {
if let Some(suffix) = opt((identifier, '#').take()).parse_next(i)? { if let Some(suffix) = opt((identifier, '#').take()).parse_next(i)? {
return Err(winnow::error::ErrMode::Cut(ErrorContext::new( return cut_error!(
"you are missing a space to separate two string literals", "you are missing a space to separate two string literals",
suffix, suffix,
))); );
} }
Ok(()) Ok(())
} }
@ -704,10 +691,10 @@ fn str_lit_without_prefix<'a>(i: &mut &'a str) -> ParseResult<'a> {
None => None, None => None,
}; };
if let Some(kind) = kind { if let Some(kind) = kind {
return Err(winnow::error::ErrMode::Cut(ErrorContext::new( return cut_error!(
format!("expected an unprefixed normal string, not a {kind}"), format!("expected an unprefixed normal string, not a {kind}"),
start, start,
))); );
} }
Ok(lit.content) Ok(lit.content)
@ -740,18 +727,12 @@ fn char_lit<'a>(i: &mut &'a str) -> ParseResult<'a, CharLit<'a>> {
let Some(s) = s else { let Some(s) = s else {
i.reset(&start); i.reset(&start);
return Err(winnow::error::ErrMode::Cut(ErrorContext::new( return cut_error!("empty character literal", *i);
"empty character literal",
*i,
)));
}; };
let mut is = s; let mut is = s;
let Ok(c) = Char::parse(&mut is) else { let Ok(c) = Char::parse(&mut is) else {
i.reset(&start); i.reset(&start);
return Err(winnow::error::ErrMode::Cut(ErrorContext::new( return cut_error!("invalid character", *i);
"invalid character",
*i,
)));
}; };
let (nb, max_value, err1, err2) = match c { let (nb, max_value, err1, err2) = match c {
@ -779,11 +760,11 @@ fn char_lit<'a>(i: &mut &'a str) -> ParseResult<'a, CharLit<'a>> {
let Ok(nb) = u32::from_str_radix(nb, 16) else { let Ok(nb) = u32::from_str_radix(nb, 16) else {
i.reset(&start); i.reset(&start);
return Err(winnow::error::ErrMode::Cut(ErrorContext::new(err1, *i))); return cut_error!(err1, *i);
}; };
if nb > max_value { if nb > max_value {
i.reset(&start); i.reset(&start);
return Err(winnow::error::ErrMode::Cut(ErrorContext::new(err2, *i))); return cut_error!(err2, *i);
} }
Ok(CharLit { Ok(CharLit {
@ -1120,17 +1101,16 @@ impl Level<'_> {
count: 1, count: 1,
}) })
} else { } else {
Err(Self::_fail(i)) Self::_fail(i)
} }
} }
#[cold] #[inline(always)]
#[inline(never)] fn _fail<T>(i: &str) -> ParseResult<'_, T> {
fn _fail(i: &str) -> ParseErr<'_> { cut_error!(
winnow::error::ErrMode::Cut(ErrorContext::new(
"your template code is too deeply nested, or the last expression is too complex", "your template code is too deeply nested, or the last expression is too complex",
i, i,
)) )
} }
} }
@ -1156,7 +1136,7 @@ impl LevelGuard<'_> {
self.count += 1; self.count += 1;
Ok(()) Ok(())
} else { } else {
Err(Level::_fail(i)) Level::_fail(i)
} }
} }
} }
@ -1440,6 +1420,30 @@ fn is_rust_keyword(ident: &str) -> bool {
false false
} }
macro_rules! cut_error {
($message:expr, $span:expr $(,)?) => {{
use ::std::convert::Into;
use ::std::option::Option::Some;
$crate::cut_context_err(
#[cold]
#[inline(always)]
move || $crate::ErrorContext {
span: Into::into($span),
message: Some(Into::into($message)),
},
)
}};
}
pub(crate) use cut_error;
#[cold]
#[inline(never)]
fn cut_context_err<'a, T>(gen_err: impl FnOnce() -> ErrorContext<'a>) -> ParseResult<'a, T> {
Err(ErrMode::Cut(gen_err()))
}
#[cfg(not(windows))] #[cfg(not(windows))]
#[cfg(test)] #[cfg(test)]
mod test { mod test {

View File

@ -5,13 +5,14 @@ use winnow::combinator::{
alt, cut_err, delimited, eof, fail, not, opt, peek, preceded, repeat, separated, alt, cut_err, delimited, eof, fail, not, opt, peek, preceded, repeat, separated,
separated_pair, terminated, separated_pair, terminated,
}; };
use winnow::error::ErrMode;
use winnow::stream::Stream as _; use winnow::stream::Stream as _;
use winnow::token::{any, rest, take_until}; use winnow::token::{any, rest, take_until};
use winnow::{ModalParser, Parser}; use winnow::{ModalParser, Parser};
use crate::{ use crate::{
ErrorContext, Expr, Filter, ParseResult, Span, State, Target, WithSpan, filter, identifier, ErrorContext, Expr, Filter, ParseResult, Span, State, Target, WithSpan, cut_error, filter,
is_rust_keyword, keyword, skip_ws0, str_lit_without_prefix, ws, identifier, is_rust_keyword, keyword, skip_ws0, str_lit_without_prefix, ws,
}; };
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
@ -41,9 +42,7 @@ impl<'a> Node<'a> {
let result = match (|i: &mut _| Self::many(i, s)).parse_next(i) { let result = match (|i: &mut _| Self::many(i, s)).parse_next(i) {
Ok(result) => result, Ok(result) => result,
Err(err) => { Err(err) => {
if let winnow::error::ErrMode::Backtrack(err) | winnow::error::ErrMode::Cut(err) = if let ErrMode::Backtrack(err) | ErrMode::Cut(err) = &err {
&err
{
if err.message.is_none() { if err.message.is_none() {
*i = start; *i = start;
if let Some(mut span) = err.span.as_suffix_of(i) { if let Some(mut span) = err.span.as_suffix_of(i) {
@ -57,12 +56,12 @@ impl<'a> Node<'a> {
opt(|i: &mut _| unexpected_tag(i, s)).parse_next(i)?; opt(|i: &mut _| unexpected_tag(i, s)).parse_next(i)?;
let is_eof = opt(eof).parse_next(i)?; let is_eof = opt(eof).parse_next(i)?;
if is_eof.is_none() { if is_eof.is_none() {
return Err(winnow::error::ErrMode::Cut(ErrorContext::new( return cut_error!(
"cannot parse entire template\n\ "cannot parse entire template\n\
you should never encounter this error\n\ you should never encounter this error\n\
please report this error to <https://github.com/askama-rs/askama/issues>", please report this error to <https://github.com/askama-rs/askama/issues>",
*i, *i,
))); );
} }
Ok(result) Ok(result)
} }
@ -133,10 +132,7 @@ impl<'a> Node<'a> {
let start = *i; let start = *i;
let (pws, _, nws) = p.parse_next(i)?; let (pws, _, nws) = p.parse_next(i)?;
if !s.is_in_loop() { if !s.is_in_loop() {
return Err(winnow::error::ErrMode::Cut(ErrorContext::new( return cut_error!("you can only `break` inside a `for` loop", start);
"you can only `break` inside a `for` loop",
start,
)));
} }
Ok(Self::Break(WithSpan::new(Ws(pws, nws), start))) Ok(Self::Break(WithSpan::new(Ws(pws, nws), start)))
} }
@ -151,10 +147,7 @@ impl<'a> Node<'a> {
let start = *i; let start = *i;
let (pws, _, nws) = p.parse_next(i)?; let (pws, _, nws) = p.parse_next(i)?;
if !s.is_in_loop() { if !s.is_in_loop() {
return Err(winnow::error::ErrMode::Cut(ErrorContext::new( return cut_error!("you can only `continue` inside a `for` loop", start);
"you can only `continue` inside a `for` loop",
start,
)));
} }
Ok(Self::Continue(WithSpan::new(Ws(pws, nws), start))) Ok(Self::Continue(WithSpan::new(Ws(pws, nws), start)))
} }
@ -223,9 +216,7 @@ fn cut_node<'a, O>(
move |i: &mut &'a str| { move |i: &mut &'a str| {
let start = *i; let start = *i;
let result = inner.parse_next(i); let result = inner.parse_next(i);
if let Err(winnow::error::ErrMode::Cut(err) | winnow::error::ErrMode::Backtrack(err)) = if let Err(ErrMode::Cut(err) | ErrMode::Backtrack(err)) = &result {
&result
{
if err.message.is_none() { if err.message.is_none() {
*i = start; *i = start;
if let Some(mut span) = err.span.as_suffix_of(i) { if let Some(mut span) = err.span.as_suffix_of(i) {
@ -259,7 +250,7 @@ fn unexpected_raw_tag<'a>(kind: Option<&'static str>, i: &mut &'a str) -> ParseR
tag if tag.starts_with("end") => format!("unexpected closing tag `{tag}`"), tag if tag.starts_with("end") => format!("unexpected closing tag `{tag}`"),
tag => format!("unknown node `{tag}`"), tag => format!("unknown node `{tag}`"),
}; };
Err(winnow::error::ErrMode::Cut(ErrorContext::new(msg, *i))) cut_error!(msg, *i)
} }
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
@ -491,10 +482,10 @@ fn check_block_start<'a>(
expected: &str, expected: &str,
) -> ParseResult<'a, ()> { ) -> ParseResult<'a, ()> {
if i.is_empty() { if i.is_empty() {
return Err(winnow::error::ErrMode::Cut(ErrorContext::new( return cut_error!(
format!("expected `{expected}` to terminate `{node}` node, found nothing"), format!("expected `{expected}` to terminate `{node}` node, found nothing"),
start, start,
))); );
} }
(|i: &mut _| s.tag_block_start(i)).parse_next(i) (|i: &mut _| s.tag_block_start(i)).parse_next(i)
} }
@ -625,10 +616,7 @@ fn check_duplicated_name<'a>(
i: &'a str, i: &'a str,
) -> Result<(), crate::ParseErr<'a>> { ) -> Result<(), crate::ParseErr<'a>> {
if !names.insert(arg_name) { if !names.insert(arg_name) {
return Err(winnow::error::ErrMode::Cut(ErrorContext::new( return cut_error!(format!("duplicated argument `{arg_name}`"), i);
format!("duplicated argument `{arg_name}`"),
i,
)));
} }
Ok(()) Ok(())
} }
@ -661,10 +649,7 @@ impl<'a> Macro<'a> {
.parse_next(i)?; .parse_next(i)?;
match args { match args {
Some((args, Some(_))) => Ok(args), Some((args, Some(_))) => Ok(args),
Some((_, None)) => Err(winnow::error::ErrMode::Cut(ErrorContext::new( Some((_, None)) => cut_error!("expected `)` to close macro argument list", *i),
"expected `)` to close macro argument list",
*i,
))),
None => Ok(None), None => Ok(None),
} }
}; };
@ -685,10 +670,7 @@ impl<'a> Macro<'a> {
); );
let (pws1, _, (name, params, nws1, _)) = start.parse_next(i)?; let (pws1, _, (name, params, nws1, _)) = start.parse_next(i)?;
if is_rust_keyword(name) { if is_rust_keyword(name) {
return Err(winnow::error::ErrMode::Cut(ErrorContext::new( return cut_error!(format!("'{name}' is not a valid name for a macro"), start_s);
format!("'{name}' is not a valid name for a macro"),
start_s,
)));
} }
if let Some(ref params) = params { if let Some(ref params) = params {
@ -701,13 +683,13 @@ impl<'a> Macro<'a> {
for (new_arg_name, default_value) in iter.by_ref() { for (new_arg_name, default_value) in iter.by_ref() {
check_duplicated_name(&mut names, new_arg_name, start_s)?; check_duplicated_name(&mut names, new_arg_name, start_s)?;
if default_value.is_none() { if default_value.is_none() {
return Err(winnow::error::ErrMode::Cut(ErrorContext::new( return cut_error!(
format!( format!(
"all arguments following `{arg_name}` should have a default \ "all arguments following `{arg_name}` should have a default \
value, `{new_arg_name}` doesn't have a default value" value, `{new_arg_name}` doesn't have a default value"
), ),
start_s, start_s,
))); );
} }
} }
} }
@ -880,10 +862,7 @@ impl<'a> Call<'a> {
match args { match args {
Some((args, Some(_))) => Ok(args), Some((args, Some(_))) => Ok(args),
Some((_, None)) => Err(winnow::error::ErrMode::Cut(ErrorContext::new( Some((_, None)) => cut_error!("expected `)` to close call argument list", *i),
"expected `)` to close call argument list",
*i,
))),
None => Ok(None), None => Ok(None),
} }
}; };
@ -992,10 +971,10 @@ impl<'a> Match<'a> {
arms.push(arm); arms.push(arm);
} }
if arms.is_empty() { if arms.is_empty() {
return Err(winnow::error::ErrMode::Cut(ErrorContext::new( return cut_error!(
"`match` nodes must contain at least one `when` node and/or an `else` case", "`match` nodes must contain at least one `when` node and/or an `else` case",
start, start,
))); );
} }
Ok(WithSpan::new( Ok(WithSpan::new(
@ -1082,13 +1061,13 @@ fn check_end_name<'a>(
return Ok(end_name); return Ok(end_name);
} }
Err(winnow::error::ErrMode::Cut(ErrorContext::new( cut_error!(
match name.is_empty() && !end_name.is_empty() { match name.is_empty() && !end_name.is_empty() {
true => format!("unexpected name `{end_name}` in `end{kind}` tag for unnamed `{kind}`"), true => format!("unexpected name `{end_name}` in `end{kind}` tag for unnamed `{kind}`"),
false => format!("expected name `{name}` in `end{kind}` tag, found `{end_name}`"), false => format!("expected name `{name}` in `end{kind}` tag, found `{end_name}`"),
}, },
before, before,
))) )
} }
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
@ -1234,20 +1213,20 @@ impl<'a> Let<'a> {
} }
}; };
if let Some(kind) = kind { if let Some(kind) = kind {
return Err(winnow::error::ErrMode::Cut(ErrorContext::new( return cut_error!(
format!( format!(
"when you forward-define a variable, you cannot use {kind} in place of \ "when you forward-define a variable, you cannot use {kind} in place of \
a variable name" a variable name"
), ),
start, start,
))); );
} }
} }
if is_mut.is_some() && !matches!(var, Target::Name(_)) { if is_mut.is_some() && !matches!(var, Target::Name(_)) {
return Err(winnow::error::ErrMode::Cut(ErrorContext::new( return cut_error!(
"you can only use the `mut` keyword with a variable name", "you can only use the `mut` keyword with a variable name",
start, start,
))); );
} }
Ok(WithSpan::new( Ok(WithSpan::new(
@ -1407,7 +1386,7 @@ impl<'a> Comment<'a> {
let mut ws = Ws(None, None); let mut ws = Ws(None, None);
if content.len() == 1 && matches!(content, "-" | "+" | "~") { if content.len() == 1 && matches!(content, "-" | "+" | "~") {
return Err(winnow::error::ErrMode::Cut(ErrorContext::new( return cut_error!(
format!( format!(
"ambiguous whitespace stripping\n\ "ambiguous whitespace stripping\n\
use `{}{content} {content}{}` to apply the same whitespace stripping on both \ use `{}{content} {content}{}` to apply the same whitespace stripping on both \
@ -1415,7 +1394,7 @@ impl<'a> Comment<'a> {
s.syntax.comment_start, s.syntax.comment_end, s.syntax.comment_start, s.syntax.comment_end,
), ),
start, start,
))); );
} else if content.len() >= 2 { } else if content.len() >= 2 {
ws.0 = Whitespace::parse_char(content.chars().next().unwrap_or_default()); ws.0 = Whitespace::parse_char(content.chars().next().unwrap_or_default());
ws.1 = Whitespace::parse_char(content.chars().next_back().unwrap_or_default()); ws.1 = Whitespace::parse_char(content.chars().next_back().unwrap_or_default());
@ -1442,10 +1421,10 @@ fn end_node<'a, 'g: 'a>(
Ok(actual) Ok(actual)
} else if actual.starts_with("end") { } else if actual.starts_with("end") {
i.reset(&start); i.reset(&start);
Err(winnow::error::ErrMode::Cut(ErrorContext::new( cut_error!(
format!("expected `{expected}` to terminate `{node}` node, found `{actual}`"), format!("expected `{expected}` to terminate `{node}` node, found `{actual}`"),
*i, *i,
))) )
} else { } else {
i.reset(&start); i.reset(&start);
fail.parse_next(i) fail.parse_next(i)

View File

@ -1,11 +1,12 @@
use winnow::combinator::{alt, opt, peek, preceded, separated}; use winnow::combinator::{alt, opt, peek, preceded, separated};
use winnow::error::ErrMode;
use winnow::token::one_of; use winnow::token::one_of;
use winnow::{ModalParser, Parser}; use winnow::{ModalParser, Parser};
use crate::{ use crate::{
CharLit, ErrorContext, Num, ParseErr, ParseResult, PathOrIdentifier, State, StrLit, WithSpan, CharLit, ErrorContext, Num, ParseErr, ParseResult, PathOrIdentifier, State, StrLit, WithSpan,
bool_lit, can_be_variable_name, char_lit, identifier, is_rust_keyword, keyword, num_lit, bool_lit, can_be_variable_name, char_lit, cut_error, identifier, is_rust_keyword, keyword,
path_or_identifier, str_lit, ws, num_lit, path_or_identifier, str_lit, ws,
}; };
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
@ -111,18 +112,12 @@ impl<'a> Target<'a> {
if let [name] = path.as_slice() { if let [name] = path.as_slice() {
// If the path only contains one item, we need to check the name. // If the path only contains one item, we need to check the name.
if !can_be_variable_name(name) { if !can_be_variable_name(name) {
return Err(winnow::error::ErrMode::Cut(ErrorContext::new( return cut_error!(format!("`{name}` cannot be used as an identifier"), *name);
format!("`{name}` cannot be used as an identifier"),
*name,
)));
} }
} else { } else {
// Otherwise we need to check every element but the first. // Otherwise we need to check every element but the first.
if let Some(name) = path.iter().skip(1).find(|n| !can_be_variable_name(n)) { if let Some(name) = path.iter().skip(1).find(|n| !can_be_variable_name(n)) {
return Err(winnow::error::ErrMode::Cut(ErrorContext::new( return cut_error!(format!("`{name}` cannot be used as an identifier"), *name);
format!("`{name}` cannot be used as an identifier"),
*name,
)));
} }
} }
@ -162,20 +157,17 @@ impl<'a> Target<'a> {
if let Some(rest) = rest { if let Some(rest) = rest {
let chr = peek(ws(opt(one_of([',', ':'])))).parse_next(i)?; let chr = peek(ws(opt(one_of([',', ':'])))).parse_next(i)?;
if let Some(chr) = chr { if let Some(chr) = chr {
return Err(winnow::error::ErrMode::Cut(ErrorContext::new( return cut_error!(
format!( format!(
"unexpected `{chr}` character after `..`\n\ "unexpected `{chr}` character after `..`\n\
note that in a named struct, `..` must come last to ignore other members" note that in a named struct, `..` must come last to ignore other members"
), ),
*i, *i,
))); );
} }
if let Target::Rest(ref s) = rest.0 { if let Target::Rest(ref s) = rest.0 {
if s.inner.is_some() { if s.inner.is_some() {
return Err(winnow::error::ErrMode::Cut(ErrorContext::new( return cut_error!("`@ ..` cannot be used in struct", s.span);
"`@ ..` cannot be used in struct",
s.span,
)));
} }
} }
return Ok((rest.1, rest.0)); return Ok((rest.1, rest.0));
@ -190,10 +182,7 @@ impl<'a> Target<'a> {
if src == "_" { if src == "_" {
*i = start; *i = start;
return Err(winnow::error::ErrMode::Cut(ErrorContext::new( return cut_error!("cannot use placeholder `_` as source in named struct", *i);
"cannot use placeholder `_` as source in named struct",
*i,
)));
} }
let target = match target { let target = match target {
@ -213,25 +202,19 @@ impl<'a> Target<'a> {
} }
} }
fn verify_name<'a>( fn verify_name<'a>(input: &'a str, name: &'a str) -> Result<Target<'a>, ErrMode<ErrorContext<'a>>> {
input: &'a str,
name: &'a str,
) -> Result<Target<'a>, winnow::error::ErrMode<ErrorContext<'a>>> {
if is_rust_keyword(name) { if is_rust_keyword(name) {
Err(winnow::error::ErrMode::Cut(ErrorContext::new( cut_error!(
format!("cannot use `{name}` as a name: it is a rust keyword"), format!("cannot use `{name}` as a name: it is a rust keyword"),
input, input,
))) )
} else if !can_be_variable_name(name) { } else if !can_be_variable_name(name) {
Err(winnow::error::ErrMode::Cut(ErrorContext::new( cut_error!(format!("`{name}` cannot be used as an identifier"), input)
format!("`{name}` cannot be used as an identifier"),
input,
)))
} else if name.starts_with("__askama") { } else if name.starts_with("__askama") {
Err(winnow::error::ErrMode::Cut(ErrorContext::new( cut_error!(
format!("cannot use `{name}` as a name: it is reserved for `askama`"), format!("cannot use `{name}` as a name: it is reserved for `askama`"),
input, input,
))) )
} else { } else {
Ok(Target::Name(name)) Ok(Target::Name(name))
} }
@ -252,10 +235,7 @@ fn collect_targets<'a, T>(
let targets = opt(separated(1.., one, ws(',')).map(|v: Vec<_>| v)).parse_next(i)?; let targets = opt(separated(1.., one, ws(',')).map(|v: Vec<_>| v)).parse_next(i)?;
let Some(targets) = targets else { let Some(targets) = targets else {
return Err(winnow::error::ErrMode::Cut(ErrorContext::new( return cut_error!("expected comma separated list of members", *i);
"expected comma separated list of members",
*i,
)));
}; };
let (has_comma, has_end) = (opt_comma, opt_end).parse_next(i)?; let (has_comma, has_end) = (opt_comma, opt_end).parse_next(i)?;
@ -264,7 +244,7 @@ fn collect_targets<'a, T>(
true => format!("expected member, or `{delim}` as terminator"), true => format!("expected member, or `{delim}` as terminator"),
false => format!("expected `,` for more members, or `{delim}` as terminator"), false => format!("expected `,` for more members, or `{delim}` as terminator"),
}; };
return Err(winnow::error::ErrMode::Cut(ErrorContext::new(msg, *i))); return cut_error!(msg, *i);
} }
let singleton = !has_comma && targets.len() == 1; let singleton = !has_comma && targets.len() == 1;
@ -281,16 +261,13 @@ fn only_one_rest_pattern<'a>(
for target in &targets { for target in &targets {
if let Target::Rest(s) = target { if let Target::Rest(s) = target {
if !allow_named_rest && s.inner.is_some() { if !allow_named_rest && s.inner.is_some() {
return Err(winnow::error::ErrMode::Cut(ErrorContext::new( return cut_error!("`@ ..` is only allowed in slice patterns", s.span);
"`@ ..` is only allowed in slice patterns",
s.span,
)));
} }
if found_rest { if found_rest {
return Err(winnow::error::ErrMode::Cut(ErrorContext::new( return cut_error!(
format!("`..` can only be used once per {type_kind} pattern"), format!("`..` can only be used once per {type_kind} pattern"),
s.span, s.span,
))); );
} }
found_rest = true; found_rest = true;
} }