From 0628afaf5efd861dcb4967eae3e9f1b3e5e7735d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Kijewski?= Date: Wed, 7 Aug 2024 20:28:33 +0200 Subject: [PATCH 1/4] parser: add wrong node terminator test --- testing/tests/ui/wrong-end.rs | 27 ++++++++++++++++++ testing/tests/ui/wrong-end.stderr | 47 +++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 testing/tests/ui/wrong-end.rs create mode 100644 testing/tests/ui/wrong-end.stderr diff --git a/testing/tests/ui/wrong-end.rs b/testing/tests/ui/wrong-end.rs new file mode 100644 index 00000000..9d267716 --- /dev/null +++ b/testing/tests/ui/wrong-end.rs @@ -0,0 +1,27 @@ +use rinja::Template; + +#[derive(Template)] +#[template(source = "{% for _ in 1..=10 %}{% end %}", ext = "txt")] +struct For; + +#[derive(Template)] +#[template(source = "{% macro test() %}{% end %}", ext = "txt")] +struct Macro; + +#[derive(Template)] +#[template(source = "{% filter upper %}{% end %}", ext = "txt")] +struct Filter; + +#[derive(Template)] +#[template(source = "{% match () %}{% when () %}{% end %}", ext = "txt")] +struct Match; + +#[derive(Template)] +#[template(source = "{% block body %}{% end %}", ext = "txt")] +struct Block; + +#[derive(Template)] +#[template(source = "{% if true %}{% end %}", ext = "txt")] +struct If; + +fn main() {} diff --git a/testing/tests/ui/wrong-end.stderr b/testing/tests/ui/wrong-end.stderr new file mode 100644 index 00000000..cb04736d --- /dev/null +++ b/testing/tests/ui/wrong-end.stderr @@ -0,0 +1,47 @@ +error: failed to parse template source + --> :1:24 + "end %}" + --> tests/ui/wrong-end.rs:4:21 + | +4 | #[template(source = "{% for _ in 1..=10 %}{% end %}", ext = "txt")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: failed to parse template source + --> :1:21 + "end %}" + --> tests/ui/wrong-end.rs:8:21 + | +8 | #[template(source = "{% macro test() %}{% end %}", ext = "txt")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: failed to parse template source + --> :1:21 + "end %}" + --> tests/ui/wrong-end.rs:12:21 + | +12 | #[template(source = "{% filter upper %}{% end %}", ext = "txt")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: failed to parse template source + --> :1:30 + "end %}" + --> tests/ui/wrong-end.rs:16:21 + | +16 | #[template(source = "{% match () %}{% when () %}{% end %}", ext = "txt")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: failed to parse template source + --> :1:19 + "end %}" + --> tests/ui/wrong-end.rs:20:21 + | +20 | #[template(source = "{% block body %}{% end %}", ext = "txt")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: failed to parse template source + --> :1:16 + "end %}" + --> tests/ui/wrong-end.rs:24:21 + | +24 | #[template(source = "{% if true %}{% end %}", ext = "txt")] + | ^^^^^^^^^^^^^^^^^^^^^^^^ From dec67c686b8ca31acffd444545ae6b69cd2f84de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Kijewski?= Date: Wed, 7 Aug 2024 08:38:35 +0200 Subject: [PATCH 2/4] parser: tell user proper keyword to end node --- rinja_parser/src/node.rs | 35 ++++++++++++++++++++++++------- testing/tests/ui/wrong-end.stderr | 32 ++++++++++++++-------------- 2 files changed, 43 insertions(+), 24 deletions(-) diff --git a/rinja_parser/src/node.rs b/rinja_parser/src/node.rs index 5a822bb2..6ee2f7ea 100644 --- a/rinja_parser/src/node.rs +++ b/rinja_parser/src/node.rs @@ -3,7 +3,7 @@ use std::str; use nom::branch::alt; use nom::bytes::complete::{tag, take_till}; use nom::character::complete::char; -use nom::combinator::{complete, consumed, cut, eof, map, not, opt, peek, recognize, value}; +use nom::combinator::{complete, consumed, cut, eof, fail, map, not, opt, peek, recognize, value}; use nom::error::ErrorKind; use nom::error_position; use nom::multi::{many0, many1, separated_list0}; @@ -377,7 +377,7 @@ impl<'a> Loop<'a> { |i| s.tag_block_start(i), opt(Whitespace::parse), opt(else_block), - ws(keyword("endfor")), + end_node("for", "endfor"), opt(Whitespace::parse), ))), ))), @@ -449,7 +449,7 @@ impl<'a> Macro<'a> { cut(tuple(( |i| s.tag_block_start(i), opt(Whitespace::parse), - ws(keyword("endmacro")), + end_node("macro", "endmacro"), cut(preceded( opt(|before| { let (after, end_name) = ws(identifier)(before)?; @@ -525,7 +525,7 @@ impl<'a> FilterBlock<'a> { cut(tuple(( |i| s.tag_block_start(i), opt(Whitespace::parse), - ws(keyword("endfilter")), + end_node("filter", "endfilter"), opt(Whitespace::parse), ))), ))); @@ -645,7 +645,7 @@ impl<'a> Match<'a> { cut(tuple(( ws(|i| s.tag_block_start(i)), opt(Whitespace::parse), - ws(keyword("endmatch")), + end_node("match", "endmatch"), opt(Whitespace::parse), ))), ))), @@ -699,7 +699,7 @@ impl<'a> BlockDef<'a> { cut(tuple(( |i| s.tag_block_start(i), opt(Whitespace::parse), - ws(keyword("endblock")), + end_node("block", "endblock"), cut(tuple(( opt(|before| { let (after, end_name) = ws(identifier)(before)?; @@ -806,7 +806,7 @@ impl<'a> Raw<'a> { let endraw = tuple(( |i| s.tag_block_start(i), opt(Whitespace::parse), - ws(keyword("endraw")), + ws(keyword("endraw")), // sic: ignore `{% end %}` in raw blocks opt(Whitespace::parse), peek(|i| s.tag_block_end(i)), )); @@ -888,7 +888,7 @@ impl<'a> If<'a> { cut(tuple(( |i| s.tag_block_start(i), opt(Whitespace::parse), - ws(keyword("endif")), + end_node("if", "endif"), opt(Whitespace::parse), ))), ))), @@ -1057,6 +1057,25 @@ impl<'a> Comment<'a> { #[derive(Clone, Copy, Debug, PartialEq)] pub struct Ws(pub Option, pub Option); +fn end_node<'a, 'g: 'a>( + node: &'g str, + expected: &'g str, +) -> impl Fn(&'a str) -> ParseResult<'a> + 'g { + move |start| { + let (i, actual) = ws(identifier)(start)?; + if actual == expected { + Ok((i, actual)) + } else if actual == "end" { + Err(nom::Err::Failure(ErrorContext::new( + format!("expected `{expected}` to terminate `{node}` node, found `{actual}`"), + start, + ))) + } else { + fail(start) + } + } +} + #[doc(hidden)] pub const MAX_KW_LEN: usize = 8; const MAX_REPL_LEN: usize = MAX_KW_LEN + 2; diff --git a/testing/tests/ui/wrong-end.stderr b/testing/tests/ui/wrong-end.stderr index cb04736d..fffff991 100644 --- a/testing/tests/ui/wrong-end.stderr +++ b/testing/tests/ui/wrong-end.stderr @@ -1,28 +1,28 @@ -error: failed to parse template source - --> :1:24 - "end %}" +error: expected `endfor` to terminate `for` node, found `end` + --> :1:23 + " end %}" --> tests/ui/wrong-end.rs:4:21 | 4 | #[template(source = "{% for _ in 1..=10 %}{% end %}", ext = "txt")] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -error: failed to parse template source - --> :1:21 - "end %}" +error: expected `endmacro` to terminate `macro` node, found `end` + --> :1:20 + " end %}" --> tests/ui/wrong-end.rs:8:21 | 8 | #[template(source = "{% macro test() %}{% end %}", ext = "txt")] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -error: failed to parse template source - --> :1:21 - "end %}" +error: expected `endfilter` to terminate `filter` node, found `end` + --> :1:20 + " end %}" --> tests/ui/wrong-end.rs:12:21 | 12 | #[template(source = "{% filter upper %}{% end %}", ext = "txt")] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -error: failed to parse template source +error: expected `endmatch` to terminate `match` node, found `end` --> :1:30 "end %}" --> tests/ui/wrong-end.rs:16:21 @@ -30,17 +30,17 @@ error: failed to parse template source 16 | #[template(source = "{% match () %}{% when () %}{% end %}", ext = "txt")] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -error: failed to parse template source - --> :1:19 - "end %}" +error: expected `endblock` to terminate `block` node, found `end` + --> :1:18 + " end %}" --> tests/ui/wrong-end.rs:20:21 | 20 | #[template(source = "{% block body %}{% end %}", ext = "txt")] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ -error: failed to parse template source - --> :1:16 - "end %}" +error: expected `endif` to terminate `if` node, found `end` + --> :1:15 + " end %}" --> tests/ui/wrong-end.rs:24:21 | 24 | #[template(source = "{% if true %}{% end %}", ext = "txt")] From e0a7d4df55805e04f6fe35f38356bbc75dccd205 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Kijewski?= Date: Wed, 7 Aug 2024 20:36:31 +0200 Subject: [PATCH 3/4] parser: use fail `fail()` where appropriate --- rinja_parser/src/expr.rs | 9 +++------ rinja_parser/src/lib.rs | 28 +++++----------------------- rinja_parser/src/node.rs | 11 ++--------- 3 files changed, 10 insertions(+), 38 deletions(-) diff --git a/rinja_parser/src/expr.rs b/rinja_parser/src/expr.rs index 2dd1b257..ced11f36 100644 --- a/rinja_parser/src/expr.rs +++ b/rinja_parser/src/expr.rs @@ -4,7 +4,7 @@ use std::str; use nom::branch::alt; use nom::bytes::complete::{tag, take_till}; use nom::character::complete::char; -use nom::combinator::{cut, map, not, opt, peek, recognize, value}; +use nom::combinator::{cut, fail, map, not, opt, peek, recognize, value}; use nom::error::ErrorKind; use nom::error_position; use nom::multi::{fold_many0, many0, separated_list0}; @@ -127,7 +127,7 @@ impl<'a> Expr<'a> { if !is_template_macro { // If this is not a template macro, we don't want to parse named arguments so // we instead return an error which will allow to continue the parsing. - return Err(nom::Err::Error(error_position!(i, ErrorKind::Alt))); + return fail(i); } let (_, level) = level.nest(i)?; @@ -486,10 +486,7 @@ impl<'a> Suffix<'a> { if nested == 0 { Ok((&input[last..], ())) } else { - Err(nom::Err::Error(error_position!( - input, - ErrorKind::SeparatedNonEmptyList - ))) + fail(input) } } diff --git a/rinja_parser/src/lib.rs b/rinja_parser/src/lib.rs index a4534b97..2fbc20f8 100644 --- a/rinja_parser/src/lib.rs +++ b/rinja_parser/src/lib.rs @@ -12,11 +12,11 @@ use std::{fmt, str}; use nom::branch::alt; use nom::bytes::complete::{escaped, is_not, tag, take_till, take_while_m_n}; use nom::character::complete::{anychar, char, one_of, satisfy}; -use nom::combinator::{complete, cut, eof, map, not, opt, recognize}; -use nom::error::{Error, ErrorKind, FromExternalError}; +use nom::combinator::{complete, cut, eof, fail, map, not, opt, recognize}; +use nom::error::{ErrorKind, FromExternalError}; use nom::multi::{many0_count, many1}; use nom::sequence::{delimited, pair, preceded, terminated, tuple}; -use nom::{error_position, AsChar, InputTakeAtPosition}; +use nom::{AsChar, InputTakeAtPosition}; pub mod expr; pub use expr::{Expr, Filter}; @@ -240,20 +240,6 @@ impl<'a> ErrorContext<'a> { message: Some(message.into()), } } - - pub(crate) fn from_err(error: nom::Err>) -> nom::Err { - 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, - }), - } - } } impl<'a> nom::error::ParseError<&'a str> for ErrorContext<'a> { @@ -323,11 +309,7 @@ fn skip_till<'a, 'b, O>( fn keyword<'a>(k: &'a str) -> impl FnMut(&'a str) -> ParseResult<'_> { move |i: &'a str| -> ParseResult<'a> { let (j, v) = identifier(i)?; - if k == v { - Ok((j, v)) - } else { - Err(nom::Err::Error(error_position!(i, ErrorKind::Tag))) - } + if k == v { Ok((j, v)) } else { fail(i) } } } @@ -366,7 +348,7 @@ fn num_lit(i: &str) -> ParseResult<'_> { Ok((i, suffix)) } else if ignore.contains(&suffix) { // no need for a message, this case only occures in an `opt(…)` - Err(nom::Err::Error(ErrorContext::new("", i))) + fail(i) } else { Err(nom::Err::Failure(ErrorContext::new( format!("unknown {kind} suffix `{suffix}`"), diff --git a/rinja_parser/src/node.rs b/rinja_parser/src/node.rs index 6ee2f7ea..de0a5ccb 100644 --- a/rinja_parser/src/node.rs +++ b/rinja_parser/src/node.rs @@ -4,8 +4,6 @@ use nom::branch::alt; use nom::bytes::complete::{tag, take_till}; use nom::character::complete::char; use nom::combinator::{complete, consumed, cut, eof, fail, map, not, opt, peek, recognize, value}; -use nom::error::ErrorKind; -use nom::error_position; use nom::multi::{many0, many1, separated_list0}; use nom::sequence::{delimited, pair, preceded, tuple}; @@ -79,12 +77,7 @@ impl<'a> Node<'a> { "break" => |i, s| Self::r#break(i, s), "continue" => |i, s| Self::r#continue(i, s), "filter" => |i, s| wrap(Self::FilterBlock, FilterBlock::parse(i, s)), - _ => { - return Err(ErrorContext::from_err(nom::Err::Error(error_position!( - i, - ErrorKind::Tag - )))); - } + _ => return fail(i), }; let (i, node) = s.nest(j, |i| func(i, s))?; @@ -773,7 +766,7 @@ impl<'a> Lit<'a> { let (i, content) = match content { Some("") => { // {block,comment,expr}_start follows immediately. - return Err(nom::Err::Error(error_position!(i, ErrorKind::TakeUntil))); + return fail(i); } Some(content) => (i, content), None => ("", i), // there is no {block,comment,expr}_start: take everything From 5efd31310f3d91dc69041b0a8dc5251f88aa2d9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Kijewski?= Date: Wed, 7 Aug 2024 20:43:18 +0200 Subject: [PATCH 4/4] parser: understand arbitrarily mixed up end nodes --- rinja_parser/src/node.rs | 2 +- testing/tests/ui/typo_in_keyword.stderr | 2 +- testing/tests/ui/wrong-end.rs | 4 ++++ testing/tests/ui/wrong-end.stderr | 8 ++++++++ 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/rinja_parser/src/node.rs b/rinja_parser/src/node.rs index de0a5ccb..ef16a8b4 100644 --- a/rinja_parser/src/node.rs +++ b/rinja_parser/src/node.rs @@ -1058,7 +1058,7 @@ fn end_node<'a, 'g: 'a>( let (i, actual) = ws(identifier)(start)?; if actual == expected { Ok((i, actual)) - } else if actual == "end" { + } else if actual.starts_with("end") { Err(nom::Err::Failure(ErrorContext::new( format!("expected `{expected}` to terminate `{node}` node, found `{actual}`"), start, diff --git a/testing/tests/ui/typo_in_keyword.stderr b/testing/tests/ui/typo_in_keyword.stderr index b18eccee..bd9daecd 100644 --- a/testing/tests/ui/typo_in_keyword.stderr +++ b/testing/tests/ui/typo_in_keyword.stderr @@ -1,4 +1,4 @@ -error: failed to parse template source +error: expected `endfor` to terminate `for` node, found `endfo` --> :1:26 "endfo%}\n1234567890123456789012345678901234567890" --> tests/ui/typo_in_keyword.rs:5:14 diff --git a/testing/tests/ui/wrong-end.rs b/testing/tests/ui/wrong-end.rs index 9d267716..0e43f5f3 100644 --- a/testing/tests/ui/wrong-end.rs +++ b/testing/tests/ui/wrong-end.rs @@ -24,4 +24,8 @@ struct Block; #[template(source = "{% if true %}{% end %}", ext = "txt")] struct If; +#[derive(Template)] +#[template(source = "{% if true %}{% endfor %}", ext = "txt")] +struct IfFor; + fn main() {} diff --git a/testing/tests/ui/wrong-end.stderr b/testing/tests/ui/wrong-end.stderr index fffff991..eb61b1f6 100644 --- a/testing/tests/ui/wrong-end.stderr +++ b/testing/tests/ui/wrong-end.stderr @@ -45,3 +45,11 @@ error: expected `endif` to terminate `if` node, found `end` | 24 | #[template(source = "{% if true %}{% end %}", ext = "txt")] | ^^^^^^^^^^^^^^^^^^^^^^^^ + +error: expected `endif` to terminate `if` node, found `endfor` + --> :1:15 + " endfor %}" + --> tests/ui/wrong-end.rs:28:21 + | +28 | #[template(source = "{% if true %}{% endfor %}", ext = "txt")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^