Repository: djc/askama
Pull request #887 (2023-10-23 19:26:46)

Improve error message for incompatible names in `endmacro` and `endblock`
To achieve this, I needed to completely change how errors were handled and to create a new type (`ParsingError`). Thanks to this, we can now add custom error messages. I'm planning to iterate on this base to provide better error messages overall and potentially even suggestion (like when the parser encounters `elif` so it can suggest to use `else if` instead).

GuillaumeGomez: Should be ready now.
GuillaumeGomez: I moved `ParsingError` into `lib.rs` and added documentation on it.
GuillaumeGomez: Updated!

Patch:
diff --git a/askama_parser/src/expr.rs b/askama_parser/src/expr.rs
index 3b6bced..803681e 100644
--- a/askama_parser/src/expr.rs
+++ b/askama_parser/src/expr.rs
@@ -12,11 +12,12 @@ use nom::{error_position, IResult};
 use super::{
     char_lit, identifier, not_ws, num_lit, path_or_identifier, str_lit, ws, Level, PathOrIdentifier,
 };
+use crate::ErrorContext;
 
 macro_rules! expr_prec_layer {
     ( $name:ident, $inner:ident, $op:expr ) => {
-        fn $name(i: &'a str, level: Level) -> IResult<&'a str, Self> {
-            let level = level.nest(i)?;
+        fn $name(i: &'a str, level: Level) -> IResult<&'a str, Self, ErrorContext<&'a str>> {
+            let (_, level) = level.nest(i)?;
             let (i, left) = Self::$inner(i, level)?;
             let (i, right) = many0(pair(
                 ws(tag($op)),
@@ -31,8 +32,8 @@ macro_rules! expr_prec_layer {
         }
     };
     ( $name:ident, $inner:ident, $( $op:expr ),+ ) => {
-        fn $name(i: &'a str, level: Level) -> IResult<&'a str, Self> {
-            let level = level.nest(i)?;
+        fn $name(i: &'a str, level: Level) -> IResult<&'a str, Self, ErrorContext<&'a str>> {
+            let (_, level) = level.nest(i)?;
             let (i, left) = Self::$inner(i, level)?;
             let (i, right) = many0(pair(
                 ws(alt(($( tag($op) ),+,))),
@@ -71,8 +72,11 @@ pub enum Expr<'a> {
 }
 
 impl<'a> Expr<'a> {
-    pub(super) fn arguments(i: &'a str, level: Level) -> IResult<&'a str, Vec<Self>> {
-        let level = level.nest(i)?;
+    pub(super) fn arguments(
+        i: &'a str,
+        level: Level,
+    ) -> IResult<&'a str, Vec<Self>, ErrorContext<&'a str>> {
+        let (_, level) = level.nest(i)?;
         preceded(
             ws(char('(')),
             cut(terminated(
@@ -82,8 +86,8 @@ impl<'a> Expr<'a> {
         )(i)
     }
 
-    pub(super) fn parse(i: &'a str, level: Level) -> IResult<&'a str, Self> {
-        let level = level.nest(i)?;
+    pub(super) fn parse(i: &'a str, level: Level) -> IResult<&'a str, Self, ErrorContext<&'a str>> {
+        let (_, level) = level.nest(i)?;
         let range_right = move |i| {
             pair(
                 ws(alt((tag("..="), tag("..")))),
@@ -114,9 +118,13 @@ impl<'a> Expr<'a> {
     expr_prec_layer!(addsub, muldivmod, "+", "-");
     expr_prec_layer!(muldivmod, filtered, "*", "/", "%");
 
-    fn filtered(i: &'a str, level: Level) -> IResult<&'a str, Self> {
-        let level = level.nest(i)?;
-        fn filter(i: &str, level: Level) -> IResult<&str, (&str, Option<Vec<Expr<'_>>>)> {
+    fn filtered(i: &'a str, level: Level) -> IResult<&'a str, Self, ErrorContext<&'a str>> {
+        let (_, level) = level.nest(i)?;
+        #[allow(clippy::type_complexity)]
+        fn filter(
+            i: &str,
+            level: Level,
+        ) -> IResult<&str, (&str, Option<Vec<Expr<'_>>>), ErrorContext<&str>> {
             let (i, (_, fname, args)) = tuple((
                 char('|'),
                 ws(identifier),
@@ -143,8 +151,8 @@ impl<'a> Expr<'a> {
         Ok((i, res))
     }
 
-    fn prefix(i: &'a str, level: Level) -> IResult<&'a str, Self> {
-        let level = level.nest(i)?;
+    fn prefix(i: &'a str, level: Level) -> IResult<&'a str, Self, ErrorContext<&'a str>> {
+        let (_, level) = level.nest(i)?;
         let (i, (ops, mut expr)) = pair(many0(ws(alt((tag("!"), tag("-"))))), |i| {
             Suffix::parse(i, level)
         })(i)?;
@@ -154,8 +162,8 @@ impl<'a> Expr<'a> {
         Ok((i, expr))
     }
 
-    fn single(i: &'a str, level: Level) -> IResult<&'a str, Self> {
-        let level = level.nest(i)?;
+    fn single(i: &'a str, level: Level) -> IResult<&'a str, Self, ErrorContext<&'a str>> {
+        let (_, level) = level.nest(i)?;
         alt((
             Self::num,
             Self::str,
@@ -166,8 +174,8 @@ impl<'a> Expr<'a> {
         ))(i)
     }
 
-    fn group(i: &'a str, level: Level) -> IResult<&'a str, Self> {
-        let level = level.nest(i)?;
+    fn group(i: &'a str, level: Level) -> IResult<&'a str, Self, ErrorContext<&'a str>> {
+        let (_, level) = level.nest(i)?;
         let (i, expr) = preceded(ws(char('(')), opt(|i| Self::parse(i, level)))(i)?;
         let expr = match expr {
             Some(expr) => expr,
@@ -195,8 +203,8 @@ impl<'a> Expr<'a> {
         Ok((i, Self::Tuple(exprs)))
     }
 
-    fn array(i: &'a str, level: Level) -> IResult<&'a str, Self> {
-        let level = level.nest(i)?;
+    fn array(i: &'a str, level: Level) -> IResult<&'a str, Self, ErrorContext<&'a str>> {
+        let (_, level) = level.nest(i)?;
         preceded(
             ws(char('[')),
             cut(terminated(
@@ -209,7 +217,7 @@ impl<'a> Expr<'a> {
         )(i)
     }
 
-    fn path_var_bool(i: &'a str) -> IResult<&'a str, Self> {
+    fn path_var_bool(i: &'a str) -> IResult<&'a str, Self, ErrorContext<&'a str>> {
         map(path_or_identifier, |v| match v {
             PathOrIdentifier::Path(v) => Self::Path(v),
             PathOrIdentifier::Identifier(v @ "true") => Self::BoolLit(v),
@@ -218,15 +226,15 @@ impl<'a> Expr<'a> {
         })(i)
     }
 
-    fn str(i: &'a str) -> IResult<&'a str, Self> {
+    fn str(i: &'a str) -> IResult<&'a str, Self, ErrorContext<&'a str>> {
         map(str_lit, Self::StrLit)(i)
     }
 
-    fn num(i: &'a str) -> IResult<&'a str, Self> {
+    fn num(i: &'a str) -> IResult<&'a str, Self, ErrorContext<&'a str>> {
         map(num_lit, Self::NumLit)(i)
     }
 
-    fn char(i: &'a str) -> IResult<&'a str, Self> {
+    fn char(i: &'a str) -> IResult<&'a str, Self, ErrorContext<&'a str>> {
         map(char_lit, Self::CharLit)(i)
     }
 }
@@ -241,8 +249,8 @@ enum Suffix<'a> {
 }
 
 impl<'a> Suffix<'a> {
-    fn parse(i: &'a str, level: Level) -> IResult<&'a str, Expr<'a>> {
-        let level = level.nest(i)?;
+    fn parse(i: &'a str, level: Level) -> IResult<&'a str, Expr<'a>, ErrorContext<&'a str>> {
+        let (_, level) = level.nest(i)?;
         let (mut i, mut expr) = Expr::single(i, level)?;
         loop {
             let (j, suffix) = opt(alt((
@@ -271,8 +279,8 @@ impl<'a> Suffix<'a> {
         Ok((i, expr))
     }
 
-    fn r#macro(i: &'a str) -> IResult<&'a str, Self> {
-        fn nested_parenthesis(input: &str) -> IResult<&str, ()> {
+    fn r#macro(i: &'a str) -> IResult<&'a str, Self, ErrorContext<&'a str>> {
+        fn nested_parenthesis(input: &str) -> IResult<&str, (), ErrorContext<&str>> {
             let mut nested = 0;
             let mut last = 0;
             let mut in_str = false;
@@ -329,7 +337,7 @@ impl<'a> Suffix<'a> {
         )(i)
     }
 
-    fn attr(i: &'a str) -> IResult<&'a str, Self> {
+    fn attr(i: &'a str) -> IResult<&'a str, Self, ErrorContext<&'a str>> {
         map(
             preceded(
                 ws(pair(char('.'), not(char('.')))),
@@ -339,8 +347,8 @@ impl<'a> Suffix<'a> {
         )(i)
     }
 
-    fn index(i: &'a str, level: Level) -> IResult<&'a str, Self> {
-        let level = level.nest(i)?;
+    fn index(i: &'a str, level: Level) -> IResult<&'a str, Self, ErrorContext<&'a str>> {
+        let (_, level) = level.nest(i)?;
         map(
             preceded(
                 ws(char('[')),
@@ -350,12 +358,12 @@ impl<'a> Suffix<'a> {
         )(i)
     }
 
-    fn call(i: &'a str, level: Level) -> IResult<&'a str, Self> {
-        let level = level.nest(i)?;
+    fn call(i: &'a str, level: Level) -> IResult<&'a str, Self, ErrorContext<&'a str>> {
+        let (_, level) = level.nest(i)?;
         map(move |i| Expr::arguments(i, level), Self::Call)(i)
     }
 
-    fn r#try(i: &'a str) -> IResult<&'a str, Self> {
+    fn r#try(i: &'a str) -> IResult<&'a str, Self, ErrorContext<&'a str>> {
         map(preceded(take_till(not_ws), char('?')), |_| Self::Try)(i)
     }
 }
diff --git a/askama_parser/src/lib.rs b/askama_parser/src/lib.rs
index 78b0807..5abc771 100644
--- a/askama_parser/src/lib.rs
+++ b/askama_parser/src/lib.rs
@@ -1,6 +1,7 @@
 #![deny(unreachable_pub)]
 #![deny(elided_lifetimes_in_paths)]
 
+use std::borrow::Cow;
 use std::cell::Cell;
 use std::{fmt, str};
 
@@ -9,7 +10,7 @@ use nom::bytes::complete::{escaped, is_not, tag, take_till};
 use nom::character::complete::char;
 use nom::character::complete::{anychar, digit1};
 use nom::combinator::{cut, eof, map, opt, recognize};
-use nom::error::ErrorKind;
+use nom::error::{Error, ErrorKind, FromExternalError};
 use nom::multi::many1;
 use nom::sequence::{delimited, pair, preceded, terminated, tuple};
 use nom::{error_position, AsChar, IResult, InputTakeAtPosition};
@@ -76,14 +77,16 @@ pub struct Ast<'a> {
 impl<'a> Ast<'a> {
     pub fn from_str(src: &'a str, syntax: &Syntax<'_>) -> Result<Self, ParseError> {
         let parse = |i: &'a str| Node::many(i, &State::new(syntax));
-        let err = match terminated(parse, cut(eof))(src) {
+        let (input, message) = match terminated(parse, cut(eof))(src) {
             Ok(("", nodes)) => return Ok(Self { nodes }),
             Ok(_) => unreachable!("eof() is not eof?"),
-            Err(nom::Err::Error(err)) | Err(nom::Err::Failure(err)) => err,
+            Err(
+                nom::Err::Error(ErrorContext { input, message, .. })
+                | nom::Err::Failure(ErrorContext { input, message, .. }),
+            ) => (input, message),
             Err(nom::Err::Incomplete(_)) => return Err(ParseError("parsing incomplete".into())),
         };
 
-        let nom::error::Error { input, .. } = err;
         let offset = src.len() - input.len();
         let (source_before, source_after) = src.split_at(offset);
 
@@ -96,7 +99,12 @@ impl<'a> Ast<'a> {
         let column = last_line.chars().count();
 
         let msg = format!(
-            "problems parsing template source at row {}, column {} near:\n{}",
+            "{}problems parsing template source at row {}, column {} near:\n{}",
+            if let Some(message) = message {
+                format!("{message}\n")
+            } else {
+                String::new()
+            },
             row + 1,
             column,
             source_after,
@@ -121,6 +129,55 @@ impl fmt::Display for ParseError {
     }
 }
 
+/// This type is used to handle `nom` errors and in particular to add custom error messages.
+/// It used to generate `ParserError`.
+///
+/// It cannot be used to replace `ParseError` because it expects a generic, which would make
+/// `askama`'s users experience less good (since this generic is only needed for `nom`).
+#[derive(Debug)]
+pub(crate) struct ErrorContext<I> {
+    pub(crate) input: I,
+    pub(crate) message: Option<Cow<'static, str>>,
+}
+
+impl<I> nom::error::ParseError<I> for ErrorContext<I> {
+    fn from_error_kind(input: I, _code: ErrorKind) -> Self {
+        Self {
+            input,
+            message: None,
+        }
+    }
+
+    fn append(_: I, _: ErrorKind, other: Self) -> Self {
+        other
+    }
+}
+
+impl<I, E: std::fmt::Display> FromExternalError<I, E> for ErrorContext<I> {
+    fn from_external_error(input: I, _kind: ErrorKind, e: E) -> Self {
+        Self {
+            input,
+            message: Some(Cow::Owned(e.to_string())),
+        }
+    }
+}
+
+impl<I> ErrorContext<I> {
+    pub(crate) fn from_err(error: nom::Err<Error<I>>) -> nom::Err<Self> {
+        match error {
+            nom::Err::Incomplete(i) => nom::Err::Incomplete(i),
+            nom::Err::Failure(Error { input, .. }) => nom::Err::Failure(Self {
+                input,
+                message: None,
+            }),
+            nom::Err::Error(Error { input, .. }) => nom::Err::Error(Self {
+                input,
+                message: None,
+            }),
+        }
+    }
+}
+
 fn is_ws(c: char) -> bool {
     matches!(c, ' ' | '\t' | '\r' | '\n')
 }
@@ -130,16 +187,16 @@ fn not_ws(c: char) -> bool {
 }
 
 fn ws<'a, O>(
-    inner: impl FnMut(&'a str) -> IResult<&'a str, O>,
-) -> impl FnMut(&'a str) -> IResult<&'a str, O> {
+    inner: impl FnMut(&'a str) -> IResult<&'a str, O, ErrorContext<&'a str>>,
+) -> impl FnMut(&'a str) -> IResult<&'a str, O, ErrorContext<&'a str>> {
     delimited(take_till(not_ws), inner, take_till(not_ws))
 }
 
 /// Skips input until `end` was found, but does not consume it.
 /// Returns tuple that would be returned when parsing `end`.
 fn skip_till<'a, O>(
-    end: impl FnMut(&'a str) -> IResult<&'a str, O>,
-) -> impl FnMut(&'a str) -> IResult<&'a str, (&'a str, O)> {
+    end: impl FnMut(&'a str) -> IResult<&'a str, O, ErrorContext<&'a str>>,
+) -> impl FnMut(&'a str) -> IResult<&'a str, (&'a str, O), ErrorContext<&'a str>> {
     enum Next<O> {
         IsEnd(O),
         NotEnd(char),
@@ -157,8 +214,10 @@ fn skip_till<'a, O>(
     }
 }
 
-fn keyword<'a>(k: &'a str) -> impl FnMut(&'a str) -> IResult<&'a str, &'a str> {
-    move |i: &'a str| -> IResult<&'a str, &'a str> {
+fn keyword<'a>(
+    k: &'a str,
+) -> impl FnMut(&'a str) -> IResult<&'a str, &'a str, ErrorContext<&'a str>> {
+    move |i: &'a str| -> IResult<&'a str, &'a str, ErrorContext<&'a str>> {
         let (j, v) = identifier(i)?;
         if k == v {
             Ok((j, v))
@@ -168,15 +227,15 @@ fn keyword<'a>(k: &'a str) -> impl FnMut(&'a str) -> IResult<&'a str, &'a str> {
     }
 }
 
-fn identifier(input: &str) -> IResult<&str, &str> {
-    fn start(s: &str) -> IResult<&str, &str> {
+fn identifier(input: &str) -> IResult<&str, &str, ErrorContext<&str>> {
+    fn start(s: &str) -> IResult<&str, &str, ErrorContext<&str>> {
         s.split_at_position1_complete(
             |c| !(c.is_alpha() || c == '_' || c >= '\u{0080}'),
             nom::error::ErrorKind::Alpha,
         )
     }
 
-    fn tail(s: &str) -> IResult<&str, &str> {
+    fn tail(s: &str) -> IResult<&str, &str, ErrorContext<&str>> {
         s.split_at_position1_complete(
             |c| !(c.is_alphanum() || c == '_' || c >= '\u{0080}'),
             nom::error::ErrorKind::Alpha,
@@ -186,11 +245,11 @@ fn identifier(input: &str) -> IResult<&str, &str> {
     recognize(pair(start, opt(tail)))(input)
 }
 
-fn bool_lit(i: &str) -> IResult<&str, &str> {
+fn bool_lit(i: &str) -> IResult<&str, &str, ErrorContext<&str>> {
     alt((keyword("false"), keyword("true")))(i)
 }
 
-fn num_lit(i: &str) -> IResult<&str, &str> {
+fn num_lit(i: &str) -> IResult<&str, &str, ErrorContext<&str>> {
     recognize(tuple((
         opt(char('-')),
         digit1,
@@ -198,7 +257,7 @@ fn num_lit(i: &str) -> IResult<&str, &str> {
     )))(i)
 }
 
-fn str_lit(i: &str) -> IResult<&str, &str> {
+fn str_lit(i: &str) -> IResult<&str, &str, ErrorContext<&str>> {
     let (i, s) = delimited(
         char('"'),
         opt(escaped(is_not("\\\""), '\\', anychar)),
@@ -207,7 +266,7 @@ fn str_lit(i: &str) -> IResult<&str, &str> {
     Ok((i, s.unwrap_or_default()))
 }
 
-fn char_lit(i: &str) -> IResult<&str, &str> {
+fn char_lit(i: &str) -> IResult<&str, &str, ErrorContext<&str>> {
     let (i, s) = delimited(
         char('\''),
         opt(escaped(is_not("\\\'"), '\\', anychar)),
@@ -221,7 +280,7 @@ enum PathOrIdentifier<'a> {
     Identifier(&'a str),
 }
 
-fn path_or_identifier(i: &str) -> IResult<&str, PathOrIdentifier<'_>> {
+fn path_or_identifier(i: &str) -> IResult<&str, PathOrIdentifier<'_>, ErrorContext<&str>> {
     let root = ws(opt(tag("::")));
     let tail = opt(many1(preceded(ws(tag("::")), identifier)));
 
@@ -267,36 +326,40 @@ impl<'a> State<'a> {
         }
     }
 
-    fn nest<'b>(&self, i: &'b str) -> Result<(), nom::Err<nom::error::Error<&'b str>>> {
-        self.level.set(self.level.get().nest(i)?);
-        Ok(())
+    fn nest<'b>(&self, i: &'b str) -> IResult<&'b str, (), ErrorContext<&'b str>> {
+        let (_, level) = self.level.get().nest(i)?;
+        self.level.set(level);
+        Ok((i, ()))
     }
 
     fn leave(&self) {
         self.level.set(self.level.get().leave());
     }
 
-    fn tag_block_start<'i>(&self, i: &'i str) -> IResult<&'i str, &'i str> {
+    fn tag_block_start<'i>(&self, i: &'i str) -> IResult<&'i str, &'i str, ErrorContext<&'i str>> {
         tag(self.syntax.block_start)(i)
     }
 
-    fn tag_block_end<'i>(&self, i: &'i str) -> IResult<&'i str, &'i str> {
+    fn tag_block_end<'i>(&self, i: &'i str) -> IResult<&'i str, &'i str, ErrorContext<&'i str>> {
         tag(self.syntax.block_end)(i)
     }
 
-    fn tag_comment_start<'i>(&self, i: &'i str) -> IResult<&'i str, &'i str> {
+    fn tag_comment_start<'i>(
+        &self,
+        i: &'i str,
+    ) -> IResult<&'i str, &'i str, ErrorContext<&'i str>> {
         tag(self.syntax.comment_start)(i)
     }
 
-    fn tag_comment_end<'i>(&self, i: &'i str) -> IResult<&'i str, &'i str> {
+    fn tag_comment_end<'i>(&self, i: &'i str) -> IResult<&'i str, &'i str, ErrorContext<&'i str>> {
         tag(self.syntax.comment_end)(i)
     }
 
-    fn tag_expr_start<'i>(&self, i: &'i str) -> IResult<&'i str, &'i str> {
+    fn tag_expr_start<'i>(&self, i: &'i str) -> IResult<&'i str, &'i str, ErrorContext<&'i str>> {
         tag(self.syntax.expr_start)(i)
     }
 
-    fn tag_expr_end<'i>(&self, i: &'i str) -> IResult<&'i str, &'i str> {
+    fn tag_expr_end<'i>(&self, i: &'i str) -> IResult<&'i str, &'i str, ErrorContext<&'i str>> {
         tag(self.syntax.expr_end)(i)
     }
 
@@ -340,12 +403,15 @@ impl Default for Syntax<'static> {
 pub(crate) struct Level(u8);
 
 impl Level {
-    fn nest(self, i: &str) -> Result<Level, nom::Err<nom::error::Error<&str>>> {
+    fn nest(self, i: &str) -> IResult<&str, Level, ErrorContext<&str>> {
         if self.0 >= Self::MAX_DEPTH {
-            return Err(nom::Err::Failure(error_position!(i, ErrorKind::TooLarge)));
+            return Err(ErrorContext::from_err(nom::Err::Failure(error_position!(
+                i,
+                ErrorKind::TooLarge
+            ))));
         }
 
-        Ok(Level(self.0 + 1))
+        Ok((i, Level(self.0 + 1)))
     }
 
     fn leave(&self) -> Self {
diff --git a/askama_parser/src/node.rs b/askama_parser/src/node.rs
index ba4d09e..93fca73 100644
--- a/askama_parser/src/node.rs
+++ b/askama_parser/src/node.rs
@@ -1,3 +1,4 @@
+use std::borrow::Cow;
 use std::str;
 
 use nom::branch::alt;
@@ -11,6 +12,8 @@ use nom::multi::{fold_many0, many0, many1, separated_list0, separated_list1};
 use nom::sequence::{delimited, pair, preceded, terminated, tuple};
 use nom::{error_position, IResult};
 
+use crate::ErrorContext;
+
 use super::{
     bool_lit, char_lit, identifier, is_ws, keyword, num_lit, path_or_identifier, skip_till,
     str_lit, ws, Expr, PathOrIdentifier, State,
@@ -37,7 +40,10 @@ pub enum Node<'a> {
 }
 
 impl<'a> Node<'a> {
-    pub(super) fn many(i: &'a str, s: &State<'_>) -> IResult<&'a str, Vec<Self>> {
+    pub(super) fn many(
+        i: &'a str,
+        s: &State<'_>,
+    ) -> IResult<&'a str, Vec<Self>, ErrorContext<&'a str>> {
         complete(many0(alt((
             map(|i| Lit::parse(i, s), Self::Lit),
             map(|i| Comment::parse(i, s), Self::Comment),
@@ -46,7 +52,7 @@ impl<'a> Node<'a> {
         ))))(i)
     }
 
-    fn parse(i: &'a str, s: &State<'_>) -> IResult<&'a str, Self> {
+    fn parse(i: &'a str, s: &State<'_>) -> IResult<&'a str, Self, ErrorContext<&'a str>> {
         let mut p = delimited(
             |i| s.tag_block_start(i),
             alt((
@@ -74,7 +80,7 @@ impl<'a> Node<'a> {
         result
     }
 
-    fn r#break(i: &'a str, s: &State<'_>) -> IResult<&'a str, Self> {
+    fn r#break(i: &'a str, s: &State<'_>) -> IResult<&'a str, Self, ErrorContext<&'a str>> {
         let mut p = tuple((
             opt(Whitespace::parse),
             ws(keyword("break")),
@@ -87,7 +93,7 @@ impl<'a> Node<'a> {
         Ok((j, Self::Break(Ws(pws, nws))))
     }
 
-    fn r#continue(i: &'a str, s: &State<'_>) -> IResult<&'a str, Self> {
+    fn r#continue(i: &'a str, s: &State<'_>) -> IResult<&'a str, Self, ErrorContext<&'a str>> {
         let mut p = tuple((
             opt(Whitespace::parse),
             ws(keyword("continue")),
@@ -100,7 +106,7 @@ impl<'a> Node<'a> {
         Ok((j, Self::Continue(Ws(pws, nws))))
     }
 
-    fn expr(i: &'a str, s: &State<'_>) -> IResult<&'a str, Self> {
+    fn expr(i: &'a str, s: &State<'_>) -> IResult<&'a str, Self, ErrorContext<&'a str>> {
         let mut p = tuple((
             |i| s.tag_expr_start(i),
             cut(tuple((
@@ -128,7 +134,7 @@ pub enum Target<'a> {
 }
 
 impl<'a> Target<'a> {
-    pub(super) fn parse(i: &'a str) -> IResult<&'a str, Self> {
+    pub(super) fn parse(i: &'a str) -> IResult<&'a str, Self, ErrorContext<&'a str>> {
         let mut opt_opening_paren = map(opt(ws(char('('))), |o| o.is_some());
         let mut opt_closing_paren = map(opt(ws(char(')'))), |o| o.is_some());
         let mut opt_opening_brace = map(opt(ws(char('{'))), |o| o.is_some());
@@ -211,7 +217,7 @@ impl<'a> Target<'a> {
         map(identifier, Self::Name)(i)
     }
 
-    fn lit(i: &'a str) -> IResult<&'a str, Self> {
+    fn lit(i: &'a str) -> IResult<&'a str, Self, ErrorContext<&'a str>> {
         alt((
             map(str_lit, Self::StrLit),
             map(char_lit, Self::CharLit),
@@ -220,7 +226,7 @@ impl<'a> Target<'a> {
         ))(i)
     }
 
-    fn named(i: &'a str) -> IResult<&str, (&str, Self)> {
+    fn named(i: &'a str) -> IResult<&str, (&str, Self), ErrorContext<&'a str>> {
         let (i, (src, target)) = pair(identifier, opt(preceded(ws(char(':')), Self::parse)))(i)?;
         Ok((i, (src, target.unwrap_or(Self::Name(src)))))
     }
@@ -234,7 +240,7 @@ pub struct When<'a> {
 }
 
 impl<'a> When<'a> {
-    fn r#match(i: &'a str, s: &State<'_>) -> IResult<&'a str, Self> {
+    fn r#match(i: &'a str, s: &State<'_>) -> IResult<&'a str, Self, ErrorContext<&'a str>> {
         let mut p = tuple((
             |i| s.tag_block_start(i),
             opt(Whitespace::parse),
@@ -257,7 +263,7 @@ impl<'a> When<'a> {
     }
 
     #[allow(clippy::self_named_constructors)]
-    fn when(i: &'a str, s: &State<'_>) -> IResult<&'a str, Self> {
+    fn when(i: &'a str, s: &State<'_>) -> IResult<&'a str, Self, ErrorContext<&'a str>> {
         let mut p = tuple((
             |i| s.tag_block_start(i),
             opt(Whitespace::parse),
@@ -289,7 +295,7 @@ pub struct Cond<'a> {
 }
 
 impl<'a> Cond<'a> {
-    fn parse(i: &'a str, s: &State<'_>) -> IResult<&'a str, Self> {
+    fn parse(i: &'a str, s: &State<'_>) -> IResult<&'a str, Self, ErrorContext<&'a str>> {
         let mut p = tuple((
             |i| s.tag_block_start(i),
             opt(Whitespace::parse),
@@ -320,7 +326,7 @@ pub struct CondTest<'a> {
 }
 
 impl<'a> CondTest<'a> {
-    fn parse(i: &'a str, s: &State<'_>) -> IResult<&'a str, Self> {
+    fn parse(i: &'a str, s: &State<'_>) -> IResult<&'a str, Self, ErrorContext<&'a str>> {
         let mut p = preceded(
             ws(keyword("if")),
             cut(tuple((
@@ -345,7 +351,7 @@ pub enum Whitespace {
 }
 
 impl Whitespace {
-    fn parse(i: &str) -> IResult<&str, Self> {
+    fn parse(i: &str) -> IResult<&str, Self, ErrorContext<&str>> {
         alt((
             value(Self::Preserve, char('+')),
             value(Self::Suppress, char('-')),
@@ -367,8 +373,11 @@ pub struct Loop<'a> {
 }
 
 impl<'a> Loop<'a> {
-    fn parse(i: &'a str, s: &State<'_>) -> IResult<&'a str, Self> {
-        fn content<'a>(i: &'a str, s: &State<'_>) -> IResult<&'a str, Vec<Node<'a>>> {
+    fn parse(i: &'a str, s: &State<'_>) -> IResult<&'a str, Self, ErrorContext<&'a str>> {
+        fn content<'a>(
+            i: &'a str,
+            s: &State<'_>,
+        ) -> IResult<&'a str, Vec<Node<'a>>, ErrorContext<&'a str>> {
             s.enter_loop();
             let result = Node::many(i, s);
             s.leave_loop();
@@ -448,8 +457,8 @@ pub struct Macro<'a> {
 }
 
 impl<'a> Macro<'a> {
-    fn parse(i: &'a str, s: &State<'_>) -> IResult<&'a str, Self> {
-        fn parameters(i: &str) -> IResult<&str, Vec<&str>> {
+    fn parse(i: &'a str, s: &State<'_>) -> IResult<&'a str, Self, ErrorContext<&'a str>> {
+        fn parameters(i: &str) -> IResult<&str, Vec<&str>, ErrorContext<&str>> {
             delimited(
                 ws(char('(')),
                 separated_list0(char(','), ws(identifier)),
@@ -475,14 +484,23 @@ impl<'a> Macro<'a> {
                 |i| s.tag_block_start(i),
                 opt(Whitespace::parse),
                 ws(keyword("endmacro")),
-                cut(preceded(ws(opt(keyword(name))), opt(Whitespace::parse))),
+                cut(preceded(
+                    opt(|before| {
+                        let (after, end_name) = ws(identifier)(before)?;
+                        check_end_name(before, after, name, end_name, "macro")
+                    }),
+                    opt(Whitespace::parse),
+                )),
             ))),
         )));
         let (i, (contents, (_, pws2, _, nws2))) = end(i)?;
 
         if name == "super" {
             // TODO: yield a a better error message here
-            return Err(nom::Err::Failure(Error::new(i, ErrorKind::Fail)));
+            return Err(ErrorContext::from_err(nom::Err::Failure(Error::new(
+                i,
+                ErrorKind::Fail,
+            ))));
         }
 
         Ok((
@@ -506,7 +524,7 @@ pub struct Import<'a> {
 }
 
 impl<'a> Import<'a> {
-    fn parse(i: &'a str) -> IResult<&'a str, Self> {
+    fn parse(i: &'a str) -> IResult<&'a str, Self, ErrorContext<&'a str>> {
         let mut p = tuple((
             opt(Whitespace::parse),
             ws(keyword("import")),
@@ -537,7 +555,7 @@ pub struct Call<'a> {
 }
 
 impl<'a> Call<'a> {
-    fn parse(i: &'a str, s: &State<'_>) -> IResult<&'a str, Self> {
+    fn parse(i: &'a str, s: &State<'_>) -> IResult<&'a str, Self, ErrorContext<&'a str>> {
         let mut p = tuple((
             opt(Whitespace::parse),
             ws(keyword("call")),
@@ -572,7 +590,7 @@ pub struct Match<'a> {
 }
 
 impl<'a> Match<'a> {
-    fn parse(i: &'a str, s: &State<'_>) -> IResult<&'a str, Self> {
+    fn parse(i: &'a str, s: &State<'_>) -> IResult<&'a str, Self, ErrorContext<&'a str>> {
         let mut p = tuple((
             opt(Whitespace::parse),
             ws(keyword("match")),
@@ -623,7 +641,7 @@ pub struct BlockDef<'a> {
 }
 
 impl<'a> BlockDef<'a> {
-    fn parse(i: &'a str, s: &State<'_>) -> IResult<&'a str, Self> {
+    fn parse(i: &'a str, s: &State<'_>) -> IResult<&'a str, Self, ErrorContext<&'a str>> {
         let mut start = tuple((
             opt(Whitespace::parse),
             ws(keyword("block")),
@@ -639,7 +657,13 @@ impl<'a> BlockDef<'a> {
                 |i| s.tag_block_start(i),
                 opt(Whitespace::parse),
                 ws(keyword("endblock")),
-                cut(tuple((opt(ws(keyword(name))), opt(Whitespace::parse)))),
+                cut(tuple((
+                    opt(|before| {
+                        let (after, end_name) = ws(identifier)(before)?;
+                        check_end_name(before, after, name, end_name, "block")
+                    }),
+                    opt(Whitespace::parse),
+                ))),
             ))),
         )));
         let (i, (nodes, (_, pws2, _, (_, nws2)))) = end(i)?;
@@ -656,6 +680,27 @@ impl<'a> BlockDef<'a> {
     }
 }
 
+fn check_end_name<'a>(
+    before: &'a str,
+    after: &'a str,
+    name: &'a str,
+    end_name: &'a str,
+    kind: &str,
+) -> IResult<&'a str, &'a str, ErrorContext<&'a str>> {
+    if name == end_name {
+        return Ok((after, end_name));
+    }
+    let message = if name.is_empty() && !end_name.is_empty() {
+        format!("unexpected name `{end_name}` in `end{kind}` tag for unnamed `{kind}`")
+    } else {
+        format!("expected name `{name}` in `end{kind}` tag, found `{end_name}`")
+    };
+    Err(nom::Err::Failure(ErrorContext {
+        input: before,
+        message: Some(Cow::Owned(message)),
+    }))
+}
+
 #[derive(Debug, PartialEq)]
 pub struct Lit<'a> {
     pub lws: &'a str,
@@ -664,7 +709,7 @@ pub struct Lit<'a> {
 }
 
 impl<'a> Lit<'a> {
-    fn parse(i: &'a str, s: &State<'_>) -> IResult<&'a str, Self> {
+    fn parse(i: &'a str, s: &State<'_>) -> IResult<&'a str, Self, ErrorContext<&'a str>> {
         let p_start = alt((
             tag(s.syntax.block_start),
             tag(s.syntax.comment_start),
@@ -704,7 +749,7 @@ pub struct Raw<'a> {
 }
 
 impl<'a> Raw<'a> {
-    fn parse(i: &'a str, s: &State<'_>) -> IResult<&'a str, Self> {
+    fn parse(i: &'a str, s: &State<'_>) -> IResult<&'a str, Self, ErrorContext<&'a str>> {
         let endraw = tuple((
             |i| s.tag_block_start(i),
             opt(Whitespace::parse),
@@ -739,7 +784,7 @@ pub struct Let<'a> {
 }
 
 impl<'a> Let<'a> {
-    fn parse(i: &'a str, s: &State<'_>) -> IResult<&'a str, Self> {
+    fn parse(i: &'a str, s: &State<'_>) -> IResult<&'a str, Self, ErrorContext<&'a str>> {
         let mut p = tuple((
             opt(Whitespace::parse),
             ws(alt((keyword("let"), keyword("set")))),
@@ -772,7 +817,7 @@ pub struct If<'a> {
 }
 
 impl<'a> If<'a> {
-    fn parse(i: &'a str, s: &State<'_>) -> IResult<&'a str, Self> {
+    fn parse(i: &'a str, s: &State<'_>) -> IResult<&'a str, Self, ErrorContext<&'a str>> {
         let mut p = tuple((
             opt(Whitespace::parse),
             |i| CondTest::parse(i, s),
@@ -817,7 +862,7 @@ pub struct Include<'a> {
 }
 
 impl<'a> Include<'a> {
-    fn parse(i: &'a str) -> IResult<&'a str, Self> {
+    fn parse(i: &'a str) -> IResult<&'a str, Self, ErrorContext<&'a str>> {
         let mut p = tuple((
             opt(Whitespace::parse),
             ws(keyword("include")),
@@ -840,7 +885,7 @@ pub struct Extends<'a> {
 }
 
 impl<'a> Extends<'a> {
-    fn parse(i: &'a str) -> IResult<&'a str, Self> {
+    fn parse(i: &'a str) -> IResult<&'a str, Self, ErrorContext<&'a str>> {
         let (i, path) = preceded(ws(keyword("extends")), cut(ws(str_lit)))(i)?;
         Ok((i, Self { path }))
     }
@@ -853,12 +898,15 @@ pub struct Comment<'a> {
 }
 
 impl<'a> Comment<'a> {
-    fn parse(i: &'a str, s: &State<'_>) -> IResult<&'a str, Self> {
-        fn body<'a>(mut i: &'a str, s: &State<'_>) -> IResult<&'a str, &'a str> {
+    fn parse(i: &'a str, s: &State<'_>) -> IResult<&'a str, Self, ErrorContext<&'a str>> {
+        fn body<'a>(
+            mut i: &'a str,
+            s: &State<'_>,
+        ) -> IResult<&'a str, &'a str, ErrorContext<&'a str>> {
             let mut level = 0;
             loop {
                 let (end, tail) = take_until(s.syntax.comment_end)(i)?;
-                match take_until::<_, _, Error<_>>(s.syntax.comment_start)(i) {
+                match take_until::<_, _, ErrorContext<_>>(s.syntax.comment_start)(i) {
                     Ok((start, _)) if start.as_ptr() < end.as_ptr() => {
                         level += 1;
                         i = &start[2..];


Test patch:
diff --git a/testing/tests/ui/name_mismatch_endblock.stderr b/testing/tests/ui/name_mismatch_endblock.stderr
index 2cd7aaf..958c413 100644
--- a/testing/tests/ui/name_mismatch_endblock.stderr
+++ b/testing/tests/ui/name_mismatch_endblock.stderr
@@ -1,4 +1,5 @@
-error: problems parsing template source at row 1, column 27 near:
+error: expected name `foo` in `endblock` tag, found `not_foo`
+       problems parsing template source at row 1, column 27 near:
        "not_foo %}"
  --> tests/ui/name_mismatch_endblock.rs:3:10
   |
diff --git a/testing/tests/ui/name_mismatch_endmacro.stderr b/testing/tests/ui/name_mismatch_endmacro.stderr
index ef389a2..74101a4 100644
--- a/testing/tests/ui/name_mismatch_endmacro.stderr
+++ b/testing/tests/ui/name_mismatch_endmacro.stderr
@@ -1,4 +1,5 @@
-error: problems parsing template source at row 1, column 41 near:
+error: expected name `foo` in `endmacro` tag, found `not_foo`
+       problems parsing template source at row 1, column 41 near:
        "not_foo %}"
  --> tests/ui/name_mismatch_endmacro.rs:3:10
   |