Repository: djc/askama
Pull request #1050 (2024-05-17 14:31:09)

 Better error messages for truncated files


Patch:
diff --git a/askama_derive/src/lib.rs b/askama_derive/src/lib.rs
index b956d48..e347001 100644
--- a/askama_derive/src/lib.rs
+++ b/askama_derive/src/lib.rs
@@ -17,6 +17,8 @@ mod heritage;
 use heritage::{Context, Heritage};
 mod input;
 use input::{Print, TemplateArgs, TemplateInput};
+#[cfg(test)]
+mod tests;
 
 #[proc_macro_derive(Template, attributes(template))]
 pub fn derive_template(input: TokenStream) -> TokenStream {
diff --git a/askama_parser/src/node.rs b/askama_parser/src/node.rs
index 5ba5e5f..408a800 100644
--- a/askama_parser/src/node.rs
+++ b/askama_parser/src/node.rs
@@ -12,7 +12,7 @@ use nom::error_position;
 use nom::multi::{fold_many0, many0, many1, separated_list0, separated_list1};
 use nom::sequence::{delimited, pair, preceded, terminated, tuple};
 
-use crate::{not_ws, ErrorContext, ParseResult};
+use crate::{not_ws, ErrorContext, ParseErr, ParseResult};
 
 use super::{
     bool_lit, char_lit, filter, identifier, is_ws, keyword, num_lit, path_or_identifier, skip_till,
@@ -93,10 +93,16 @@ impl<'a> Node<'a> {
         let (i, _) = s.nest(j)?;
         let result = func(i, s);
         s.leave();
-
         let (i, node) = result?;
-        let (i, _) = cut(|i| s.tag_block_end(i))(i)?;
-        Ok((i, node))
+
+        let (i, closed) = cut(alt((
+            value(true, |i| s.tag_block_end(i)),
+            value(false, ws(eof)),
+        )))(i)?;
+        match closed {
+            true => Ok((i, node)),
+            false => Err(fail_unclosed("block", s.syntax.block_end, i)),
+        }
     }
 
     fn r#break(i: &'a str, s: &State<'_>) -> ParseResult<'a, Self> {
@@ -132,17 +138,22 @@ impl<'a> Node<'a> {
     }
 
     fn expr(i: &'a str, s: &State<'_>) -> ParseResult<'a, Self> {
-        let mut p = tuple((
+        let (i, (pws, expr)) = preceded(
             |i| s.tag_expr_start(i),
-            cut(tuple((
+            cut(pair(
                 opt(Whitespace::parse),
                 ws(|i| Expr::parse(i, s.level.get())),
-                opt(Whitespace::parse),
-                |i| s.tag_expr_end(i),
-            ))),
-        ));
-        let (i, (_, (pws, expr, nws, _))) = p(i)?;
-        Ok((i, Self::Expr(Ws(pws, nws), expr)))
+            )),
+        )(i)?;
+
+        let (i, (nws, closed)) = cut(pair(
+            opt(Whitespace::parse),
+            alt((value(true, |i| s.tag_expr_end(i)), value(false, ws(eof)))),
+        ))(i)?;
+        match closed {
+            true => Ok((i, Self::Expr(Ws(pws, nws), expr))),
+            false => Err(fail_unclosed("expression", s.syntax.expr_end, i)),
+        }
     }
 }
 
@@ -1059,7 +1070,10 @@ impl<'a> Comment<'a> {
         fn content<'a>(mut i: &'a str, s: &State<'_>) -> ParseResult<'a, ()> {
             let mut depth = 0usize;
             loop {
-                let (_, (j, tag)) = skip_till(|i| tag(i, s))(i)?;
+                let (_, tag) = opt(skip_till(|i| tag(i, s)))(i)?;
+                let Some((j, tag)) = tag else {
+                    return Err(fail_unclosed("comment", s.syntax.comment_end, i));
+                };
                 match tag {
                     Tag::Open => match depth.checked_add(1) {
                         Some(new_depth) => depth = new_depth,
@@ -1109,3 +1123,10 @@ impl<'a> Comment<'a> {
 /// Second field is "minus/plus sign was used on the right part of the item".
 #[derive(Clone, Copy, Debug, PartialEq)]
 pub struct Ws(pub Option<Whitespace>, pub Option<Whitespace>);
+
+fn fail_unclosed<'a>(kind: &str, tag: &str, i: &'a str) -> ParseErr<'a> {
+    nom::Err::Failure(ErrorContext {
+        input: i,
+        message: Some(Cow::Owned(format!("unclosed {kind}, missing {tag:?}"))),
+    })
+}


Test patch:
diff --git a/askama_derive/src/tests.rs b/askama_derive/src/tests.rs
new file mode 100644
index 0000000..fd5148e
--- /dev/null
+++ b/askama_derive/src/tests.rs
@@ -0,0 +1,89 @@
+// Files containing tests for generated code.
+
+use crate::build_template;
+use std::fmt::Write;
+
+#[test]
+fn check_if_let() {
+    // This function makes it much easier to compare expected code by adding the wrapping around
+    // the code we want to check.
+    fn compare(jinja: &str, expected: &str) {
+        let jinja = format!(
+            r##"#[template(source = r#"{jinja}"#, ext = "txt")]
+struct Foo;"##
+        );
+        let generated =
+            build_template(&syn::parse_str::<syn::DeriveInput>(&jinja).unwrap()).unwrap();
+
+        let generated_s = syn::parse_str::<proc_macro2::TokenStream>(&generated)
+            .unwrap()
+            .to_string();
+        let mut new_expected = String::with_capacity(expected.len());
+        for line in expected.split('\n') {
+            new_expected.write_fmt(format_args!("{line}\n")).unwrap();
+        }
+        let expected = format!(
+            r#"impl ::askama::Template for Foo {{
+    fn render_into(&self, writer: &mut (impl ::std::fmt::Write + ?Sized)) -> ::askama::Result<()> {{
+        {new_expected}
+        ::askama::Result::Ok(())
+    }}
+    const EXTENSION: ::std::option::Option<&'static ::std::primitive::str> = Some("txt");
+    const SIZE_HINT: ::std::primitive::usize = 3;
+    const MIME_TYPE: &'static ::std::primitive::str = "text/plain; charset=utf-8";
+}}
+impl ::std::fmt::Display for Foo {{
+    #[inline]
+    fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {{
+        ::askama::Template::render_into(self, f).map_err(|_| ::std::fmt::Error {{}})
+    }}
+}}"#
+        );
+        let expected_s = syn::parse_str::<proc_macro2::TokenStream>(&expected)
+            .unwrap()
+            .to_string();
+        assert_eq!(
+            generated_s, expected_s,
+            "=== Expected ===\n{}\n=== Found ===\n{}\n=====",
+            generated, expected
+        );
+    }
+
+    // In this test, we ensure that `query` never is `self.query`.
+    compare(
+        "{% if let Some(query) = s && !query.is_empty() %}{{query}}{% endif %}",
+        r#"if let Some(query,) = &self.s && !query.is_empty() {
+    ::std::write!(
+        writer,
+        "{expr0}",
+        expr0 = &::askama::MarkupDisplay::new_unsafe(&(query), ::askama::Text),
+    )?;
+}"#,
+    );
+
+    // In this test, we ensure that `s` is `self.s` only in the first `if let Some(s) = self.s`
+    // condition.
+    compare(
+        "{% if let Some(s) = s %}{{ s }}{% endif %}",
+        r#"if let Some(s,) = &self.s {
+    ::std::write!(
+        writer,
+        "{expr0}",
+        expr0 = &::askama::MarkupDisplay::new_unsafe(&(s), ::askama::Text),
+    )?;
+}"#,
+    );
+
+    // In this test, we ensure that `s` is `self.s` only in the first `if let Some(s) = self.s`
+    // condition.
+    compare(
+        "{% if let Some(s) = s && !s.is_empty() %}{{s}}{% endif %}",
+        r#"if let Some(s,) = &self.s && !s.is_empty() {
+    ::std::write!(
+        writer,
+        "{expr0}",
+        expr0 = &::askama::MarkupDisplay::new_unsafe(&(s), ::askama::Text),
+    )?;
+}"#,
+    );
+}
diff --git a/testing/tests/ui/unclosed-nodes.rs b/testing/tests/ui/unclosed-nodes.rs
new file mode 100644
index 0000000..9fc69ec
--- /dev/null
+++ b/testing/tests/ui/unclosed-nodes.rs
@@ -0,0 +1,51 @@
+use askama::Template;
+
+#[derive(Template)]
+#[template(source = "{{ expr", ext = "txt")]
+struct Expr1;
+
+#[derive(Template)]
+#[template(source = "{{ expr ", ext = "txt")]
+struct Expr2;
+
+#[derive(Template)]
+#[template(source = "{{ expr -", ext = "txt")]
+struct Expr3;
+
+#[derive(Template)]
+#[template(source = "{{ expr -}", ext = "txt")]
+struct Expr4;
+
+#[derive(Template)]
+#[template(source = "{% let x", ext = "txt")]
+struct Node1;
+
+#[derive(Template)]
+#[template(source = "{% let x ", ext = "txt")]
+struct Node2;
+
+#[derive(Template)]
+#[template(source = "{% let x -", ext = "txt")]
+struct Node3;
+
+#[derive(Template)]
+#[template(source = "{% let x -%", ext = "txt")]
+struct Node4;
+
+#[derive(Template)]
+#[template(source = "{# comment", ext = "txt")]
+struct Comment1;
+
+#[derive(Template)]
+#[template(source = "{# comment ", ext = "txt")]
+struct Comment2;
+
+#[derive(Template)]
+#[template(source = "{# comment -", ext = "txt")]
+struct Comment3;
+
+#[derive(Template)]
+#[template(source = "{# comment -#", ext = "txt")]
+struct Comment4;
+
+fn main() {}
diff --git a/testing/tests/ui/unclosed-nodes.stderr b/testing/tests/ui/unclosed-nodes.stderr
new file mode 100644
index 0000000..44312cc
--- /dev/null
+++ b/testing/tests/ui/unclosed-nodes.stderr
@@ -0,0 +1,117 @@
+error: unclosed expression, missing "}}"
+       failed to parse template source at row 1, column 7 near:
+       ""
+ --> tests/ui/unclosed-nodes.rs:3:10
+  |
+3 | #[derive(Template)]
+  |          ^^^^^^^^
+  |
+  = note: this error originates in the derive macro `Template` (in Nightly builds, run with -Z macro-backtrace for more info)
+
+error: unclosed expression, missing "}}"
+       failed to parse template source at row 1, column 8 near:
+       ""
+ --> tests/ui/unclosed-nodes.rs:7:10
+  |
+7 | #[derive(Template)]
+  |          ^^^^^^^^
+  |
+  = note: this error originates in the derive macro `Template` (in Nightly builds, run with -Z macro-backtrace for more info)
+
+error: unclosed expression, missing "}}"
+       failed to parse template source at row 1, column 9 near:
+       ""
+  --> tests/ui/unclosed-nodes.rs:11:10
+   |
+11 | #[derive(Template)]
+   |          ^^^^^^^^
+   |
+   = note: this error originates in the derive macro `Template` (in Nightly builds, run with -Z macro-backtrace for more info)
+
+error: failed to parse template source at row 1, column 9 near:
+       "}"
+  --> tests/ui/unclosed-nodes.rs:15:10
+   |
+15 | #[derive(Template)]
+   |          ^^^^^^^^
+   |
+   = note: this error originates in the derive macro `Template` (in Nightly builds, run with -Z macro-backtrace for more info)
+
+error: unclosed block, missing "%}"
+       failed to parse template source at row 1, column 8 near:
+       ""
+  --> tests/ui/unclosed-nodes.rs:19:10
+   |
+19 | #[derive(Template)]
+   |          ^^^^^^^^
+   |
+   = note: this error originates in the derive macro `Template` (in Nightly builds, run with -Z macro-backtrace for more info)
+
+error: unclosed block, missing "%}"
+       failed to parse template source at row 1, column 9 near:
+       ""
+  --> tests/ui/unclosed-nodes.rs:23:10
+   |
+23 | #[derive(Template)]
+   |          ^^^^^^^^
+   |
+   = note: this error originates in the derive macro `Template` (in Nightly builds, run with -Z macro-backtrace for more info)
+
+error: unclosed block, missing "%}"
+       failed to parse template source at row 1, column 10 near:
+       ""
+  --> tests/ui/unclosed-nodes.rs:27:10
+   |
+27 | #[derive(Template)]
+   |          ^^^^^^^^
+   |
+   = note: this error originates in the derive macro `Template` (in Nightly builds, run with -Z macro-backtrace for more info)
+
+error: failed to parse template source at row 1, column 10 near:
+       "%"
+  --> tests/ui/unclosed-nodes.rs:31:10
+   |
+31 | #[derive(Template)]
+   |          ^^^^^^^^
+   |
+   = note: this error originates in the derive macro `Template` (in Nightly builds, run with -Z macro-backtrace for more info)
+
+error: unclosed comment, missing "#}"
+       failed to parse template source at row 1, column 2 near:
+       " comment"
+  --> tests/ui/unclosed-nodes.rs:35:10
+   |
+35 | #[derive(Template)]
+   |          ^^^^^^^^
+   |
+   = note: this error originates in the derive macro `Template` (in Nightly builds, run with -Z macro-backtrace for more info)
+
+error: unclosed comment, missing "#}"
+       failed to parse template source at row 1, column 2 near:
+       " comment "
+  --> tests/ui/unclosed-nodes.rs:39:10
+   |
+39 | #[derive(Template)]
+   |          ^^^^^^^^
+   |
+   = note: this error originates in the derive macro `Template` (in Nightly builds, run with -Z macro-backtrace for more info)
+
+error: unclosed comment, missing "#}"
+       failed to parse template source at row 1, column 2 near:
+       " comment -"
+  --> tests/ui/unclosed-nodes.rs:43:10
+   |
+43 | #[derive(Template)]
+   |          ^^^^^^^^
+   |
+   = note: this error originates in the derive macro `Template` (in Nightly builds, run with -Z macro-backtrace for more info)
+
+error: unclosed comment, missing "#}"
+       failed to parse template source at row 1, column 2 near:
+       " comment -#"
+  --> tests/ui/unclosed-nodes.rs:47:10
+   |
+47 | #[derive(Template)]
+   |          ^^^^^^^^
+   |
+   = note: this error originates in the derive macro `Template` (in Nightly builds, run with -Z macro-backtrace for more info)