diff --git a/askama_derive/src/tests.rs b/askama_derive/src/tests.rs index 5f6dcb0f..3c89ae70 100644 --- a/askama_derive/src/tests.rs +++ b/askama_derive/src/tests.rs @@ -1225,3 +1225,17 @@ fn fuzzed_0b85() -> Result<(), syn::Error> { let _: syn::File = syn::parse2(output)?; Ok(()) } + +#[test] +fn fuzzed_comparator_chain() -> Result<(), syn::Error> { + let input = quote! { + #[template( + ext = "", + source = "\u{c}{{vu7218/63e3666663-666/3330e633/63e3666663666/3333 Expr<'a> { expr_prec_layer!(or, and, "||"); expr_prec_layer!(and, compare, "&&"); - expr_prec_layer!(compare, bor, alt(("==", "!=", ">=", ">", "<=", "<",))); + + fn compare(i: &mut &'a str, level: Level<'_>) -> ParseResult<'a, WithSpan<'a, Self>> { + let right = |i: &mut _| { + let op = alt(("==", "!=", ">=", ">", "<=", "<")); + (ws(op), |i: &mut _| Self::bor(i, level)).parse_next(i) + }; + + let start = *i; + let expr = Self::bor(i, level)?; + let Some((op, rhs)) = opt(right).parse_next(i)? else { + return Ok(expr); + }; + let expr = WithSpan::new(Self::BinOp(op, Box::new(expr), Box::new(rhs)), start); + + if let Some((op2, _)) = opt(right).parse_next(i)? { + return Err(winnow::error::ErrMode::Cut(ErrorContext::new( + format!( + "comparison operators cannot be chained; \ + consider using explicit parentheses, e.g. `(_ {op} _) {op2} _`" + ), + op, + ))); + } + + Ok(expr) + } + expr_prec_layer!(bor, bxor, "bitor".value("|")); expr_prec_layer!(bxor, band, token_xor); expr_prec_layer!(band, shifts, token_bitand); diff --git a/askama_parser/src/tests.rs b/askama_parser/src/tests.rs index c44eb154..fda99dfe 100644 --- a/askama_parser/src/tests.rs +++ b/askama_parser/src/tests.rs @@ -605,35 +605,6 @@ fn test_associativity() { )) )], ); - assert_eq!( - Ast::from_str("{{ a == b != c > d > e == f }}", None, &syntax) - .unwrap() - .nodes, - vec![Node::Expr( - Ws(None, None), - WithSpan::no_span(Expr::BinOp( - "==", - Box::new(WithSpan::no_span(Expr::BinOp( - ">", - Box::new(WithSpan::no_span(Expr::BinOp( - ">", - Box::new(WithSpan::no_span(Expr::BinOp( - "!=", - Box::new(WithSpan::no_span(Expr::BinOp( - "==", - Box::new(WithSpan::no_span(Expr::Var("a"))), - Box::new(WithSpan::no_span(Expr::Var("b"))) - ))), - Box::new(WithSpan::no_span(Expr::Var("c"))) - ))), - Box::new(WithSpan::no_span(Expr::Var("d"))) - ))), - Box::new(WithSpan::no_span(Expr::Var("e"))) - ))), - Box::new(WithSpan::no_span(Expr::Var("f"))) - )) - )], - ); } #[test] @@ -1423,3 +1394,26 @@ fn there_is_no_digit_two_in_a_binary_integer() { assert!(Ast::from_str("{{ 0o9 }}", None, &syntax).is_err()); assert!(Ast::from_str("{{ 0xg }}", None, &syntax).is_err()); } + +#[test] +fn comparison_operators_cannot_be_chained() { + const OPS: &[&str] = &["==", "!=", ">=", ">", "<=", "<"]; + + let syntax = Syntax::default(); + for op1 in OPS { + assert!(Ast::from_str(&format!("{{{{ a {op1} b }}}}"), None, &syntax).is_ok()); + for op2 in OPS { + assert!(Ast::from_str(&format!("{{{{ a {op1} b {op2} c }}}}"), None, &syntax).is_err()); + for op3 in OPS { + assert!( + Ast::from_str( + &format!("{{{{ a {op1} b {op2} c {op3} d }}}}"), + None, + &syntax, + ) + .is_err() + ); + } + } + } +} diff --git a/testing/tests/ui/comparator-chaining.rs b/testing/tests/ui/comparator-chaining.rs new file mode 100644 index 00000000..0edf4aac --- /dev/null +++ b/testing/tests/ui/comparator-chaining.rs @@ -0,0 +1,49 @@ +// Comparison operators cannot be chained, so our parser must reject chained comparisons. + +use askama::Template; + +#[derive(Template)] +#[template(ext = "txt", source = "{{ a == b != c }}")] +struct EqNe { + a: usize, + b: usize, + c: usize, +} + +#[derive(Template)] +#[template(ext = "txt", source = "{{ a <= b < c }}")] +struct Between { + a: usize, + b: usize, + c: usize, +} + +#[derive(Template)] +#[template(ext = "txt", source = "{{ ((a == b) == c) == d == e }}")] +struct ThreeTimesOk { + a: usize, + b: usize, + c: usize, + d: usize, + e: usize, +} + +#[derive(Template)] +#[template(ext = "txt", source = "{{ a == (b == (c == d == e)) }}")] +struct ThreeTimesOk2 { + a: usize, + b: usize, + c: usize, + d: usize, + e: usize, +} + +// Regression test for +#[derive(Template)] +#[template( + ext = "", + source = "\u{c}{{vu7218/63e3666663-666/3330e633/63e3666663666/3333 :1:5 + "== b != c }}" + --> tests/ui/comparator-chaining.rs:6:34 + | +6 | #[template(ext = "txt", source = "{{ a == b != c }}")] + | ^^^^^^^^^^^^^^^^^^^ + +error: comparison operators cannot be chained; consider using explicit parentheses, e.g. `(_ <= _) < _` + --> :1:5 + "<= b < c }}" + --> tests/ui/comparator-chaining.rs:14:34 + | +14 | #[template(ext = "txt", source = "{{ a <= b < c }}")] + | ^^^^^^^^^^^^^^^^^^ + +error: comparison operators cannot be chained; consider using explicit parentheses, e.g. `(_ == _) == _` + --> :1:19 + "== d == e }}" + --> tests/ui/comparator-chaining.rs:22:34 + | +22 | #[template(ext = "txt", source = "{{ ((a == b) == c) == d == e }}")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: comparison operators cannot be chained; consider using explicit parentheses, e.g. `(_ == _) == _` + --> :1:17 + "== d == e)) }}" + --> tests/ui/comparator-chaining.rs:32:34 + | +32 | #[template(ext = "txt", source = "{{ a == (b == (c == d == e)) }}")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: comparison operators cannot be chained; consider using explicit parentheses, e.g. `(_ < _) < _` + --> :1:52 + " tests/ui/comparator-chaining.rs:45:14 + | +45 | source = "\u{c}{{vu7218/63e3666663-666/3330e633/63e3666663666/3333 :1:41 - "crate<338 tests/ui/crate_identifier.rs:99:14 | -99 | source = "{{\u{c}KK3e331 :1:27