diff --git a/askama_derive/src/generator.rs b/askama_derive/src/generator.rs index 5d5cc7b04..f3f461370 100644 --- a/askama_derive/src/generator.rs +++ b/askama_derive/src/generator.rs @@ -756,6 +756,19 @@ enum DisplayWrap { Unwrapped, } +/// `Wrapped & Wrapped == Wrapped`, otherwise `Unwrapped` +impl std::ops::BitAnd for DisplayWrap { + type Output = Self; + + #[inline] + fn bitand(self, rhs: Self) -> Self::Output { + match self { + DisplayWrap::Wrapped => rhs, + DisplayWrap::Unwrapped => self, + } + } +} + #[derive(Default, Debug)] struct WritableBuffer<'a> { buf: Vec>, diff --git a/askama_derive/src/generator/expr.rs b/askama_derive/src/generator/expr.rs index a3a114a6e..501f25589 100644 --- a/askama_derive/src/generator/expr.rs +++ b/askama_derive/src/generator/expr.rs @@ -1,6 +1,7 @@ use std::borrow::Cow; use std::str::FromStr; +use parser::expr::Conditional; use parser::node::CondTest; use parser::{ AssociatedItem, CharLit, CharPrefix, Expr, PathComponent, Span, StrLit, StrPrefix, Target, @@ -14,6 +15,7 @@ use super::{ DisplayWrap, Generator, LocalMeta, Writable, binary_op, compile_time_escape, is_copyable, logic_op, range_op, unary_op, }; +use crate::generator::node::EvaluatedResult; use crate::heritage::Context; use crate::integration::Buffer; use crate::{CompileError, field_new, quote_into}; @@ -103,6 +105,7 @@ impl<'a> Generator<'a, '_> { Expr::Concat(ref exprs) => self.visit_concat(ctx, buf, exprs)?, Expr::LetCond(ref cond) => self.visit_let_cond(ctx, buf, cond)?, Expr::ArgumentPlaceholder => DisplayWrap::Unwrapped, + Expr::Conditional(ref cond) => self.visit_conditional_expr(ctx, buf, cond)?, }) } @@ -251,6 +254,37 @@ impl<'a> Generator<'a, '_> { } } + fn visit_conditional_expr( + &mut self, + ctx: &Context<'_>, + buf: &mut Buffer, + cond: &Conditional<'a>, + ) -> Result { + let result; + let expr = if cond.test.contains_bool_lit_or_is_defined() { + result = self.evaluate_condition(WithSpan::clone(&cond.test), &mut true); + match &result { + EvaluatedResult::AlwaysTrue => return self.visit_expr(ctx, buf, &cond.then), + EvaluatedResult::AlwaysFalse => return self.visit_expr(ctx, buf, &cond.otherwise), + EvaluatedResult::Unknown(expr) => expr, + } + } else { + &cond.test + }; + + let mut then_buf = Buffer::new(); + let then = self.visit_expr(ctx, &mut then_buf, &cond.then)?; + let mut otherwise_buf = Buffer::new(); + let otherwise = self.visit_expr(ctx, &mut otherwise_buf, &cond.otherwise)?; + + let span = ctx.span_for_node(expr.span()); + buf.write_token(Token![if], span); + self.visit_condition(ctx, buf, expr)?; + quote_into!(buf, span, { { #then_buf } else { #otherwise_buf } }); + + Ok(then & otherwise) + } + fn visit_let_cond( &mut self, ctx: &Context<'_>, diff --git a/askama_derive/src/generator/node.rs b/askama_derive/src/generator/node.rs index cf78ac1a0..bb7004a9a 100644 --- a/askama_derive/src/generator/node.rs +++ b/askama_derive/src/generator/node.rs @@ -187,7 +187,7 @@ impl<'a> Generator<'a, '_> { Ok(size_hint) } - fn evaluate_condition( + pub(super) fn evaluate_condition( &self, expr: WithSpan<'a, Box>>, only_contains_is_defined: &mut bool, @@ -310,6 +310,15 @@ impl<'a> Generator<'a, '_> { EvaluatedResult::AlwaysTrue } } + Expr::Conditional(cond) => { + // If we can tell the outcome of the condition, recurse. + let expr = match self.evaluate_condition(cond.test, only_contains_is_defined) { + EvaluatedResult::AlwaysTrue => cond.then, + EvaluatedResult::AlwaysFalse => cond.otherwise, + EvaluatedResult::Unknown(expr) => return EvaluatedResult::Unknown(expr), + }; + self.evaluate_condition(expr, only_contains_is_defined) + } } } @@ -1500,7 +1509,7 @@ struct Conds<'a> { } #[derive(Debug, Clone, PartialEq)] -enum EvaluatedResult<'a> { +pub(super) enum EvaluatedResult<'a> { AlwaysTrue, AlwaysFalse, Unknown(WithSpan<'a, Box>>), @@ -1660,6 +1669,9 @@ fn is_cacheable(expr: &WithSpan<'_, Box>>) -> bool { Expr::As(expr, _) => is_cacheable(expr), Expr::Try(expr) => is_cacheable(expr), Expr::Concat(args) => args.iter().all(is_cacheable), + Expr::Conditional(cond) => { + is_cacheable(&cond.test) && is_cacheable(&cond.then) && is_cacheable(&cond.otherwise) + } // Doesn't make sense in this context. Expr::LetCond(_) => false, // We have too little information to tell if the expression is pure: diff --git a/askama_derive/src/tests.rs b/askama_derive/src/tests.rs index 67618d728..c4c55e61f 100644 --- a/askama_derive/src/tests.rs +++ b/askama_derive/src/tests.rs @@ -1,7 +1,9 @@ //! Files containing tests for generated code. -use std::fmt; +use std::io::Write; use std::path::Path; +use std::process::exit; +use std::{fmt, io}; use console::style; use prettyplease::unparse; @@ -124,7 +126,8 @@ fn compare_ex( } } - panic!( + let _: io::Result<()> = writeln!( + io::stderr().lock(), "\n\ === Expected ===\n\ \n\ @@ -138,11 +141,15 @@ fn compare_ex( \n\ {diff}\n\ \n\ - === FAILURE ===", + === FAILURE ===\n\ + \n\ + {location}\n", expected = style(&expected).red(), generated = style(&generated).green(), diff = Diff(&expected, &generated), + location = std::panic::Location::caller(), ); + exit(1); } } @@ -1443,3 +1450,117 @@ fn check_expr_ungrouping() { 11, ); } + +#[test] +fn test_conditional_expr() { + compare( + r"{{ a if b else c }}", + r#" + match ( + &((&&askama::filters::AutoEscaper::new( + &(if askama::helpers::as_bool(&(self.b)) { self.a } else { self.c }), + askama::filters::Text, + )) + .askama_auto_escape()?), + ) { + (__askama_expr0,) => { + (&&&askama::filters::Writable(__askama_expr0)) + .askama_write(__askama_writer, __askama_values)?; + } + }"#, + &[("a", "i8"), ("b", "i8"), ("c", "i8")], + 3, + ); + + compare( + r"{{ a if b is defined else c }}", + r#" + match ( + &((&&askama::filters::AutoEscaper::new(&(self.a), askama::filters::Text)) + .askama_auto_escape()?), + ) { + (__askama_expr0,) => { + (&&&askama::filters::Writable(__askama_expr0)) + .askama_write(__askama_writer, __askama_values)?; + } + }"#, + &[("a", "i8"), ("b", "i8"), ("c", "i8")], + 3, + ); + + compare( + r"{{ a if b is not defined else c }}", + r#" + match ( + &((&&askama::filters::AutoEscaper::new(&(self.c), askama::filters::Text)) + .askama_auto_escape()?), + ) { + (__askama_expr0,) => { + (&&&askama::filters::Writable(__askama_expr0)) + .askama_write(__askama_writer, __askama_values)?; + } + }"#, + &[("a", "i8"), ("b", "i8"), ("c", "i8")], + 3, + ); + + compare( + r"{{ a if b if c else d else e }}", + r#" + match ( + &((&&askama::filters::AutoEscaper::new( + &(if askama::helpers::as_bool( + &(if askama::helpers::as_bool(&(self.c)) { self.b } else { self.d }) + ) { + self.a + } else { + self.e + }), + askama::filters::Text, + )) + .askama_auto_escape()?), + ) { + (__askama_expr0,) => { + (&&&askama::filters::Writable(__askama_expr0)) + .askama_write(__askama_writer, __askama_values)?; + } + }"#, + &[ + ("a", "i8"), + ("b", "i8"), + ("c", "i8"), + ("d", "i8"), + ("e", "i8"), + ], + 3, + ); + + compare( + r"{{ a if b else c if d else e }}", + r#" + match ( + &((&&askama::filters::AutoEscaper::new( + &(if askama::helpers::as_bool(&(self.b)) { + self.a + } else { + if askama::helpers::as_bool(&(self.d)) { self.c } else { self.e } + }), + askama::filters::Text, + )) + .askama_auto_escape()?), + ) { + (__askama_expr0,) => { + (&&&askama::filters::Writable(__askama_expr0)) + .askama_write(__askama_writer, __askama_values)?; + } + }"#, + &[ + ("a", "i8"), + ("b", "i8"), + ("c", "i8"), + ("d", "i8"), + ("e", "i8"), + ], + 3, + ); +} diff --git a/askama_parser/src/expr.rs b/askama_parser/src/expr.rs index 4cda09585..721a81b0e 100644 --- a/askama_parser/src/expr.rs +++ b/askama_parser/src/expr.rs @@ -139,6 +139,11 @@ fn check_expr<'a>(expr: &WithSpan<'a, Box>>, allowed: Allowed) -> Parse } Expr::LetCond(cond) => check_expr(&cond.expr, Allowed::default()), Expr::ArgumentPlaceholder => cut_error!("unreachable", expr.span), + Expr::Conditional(cond) => { + check_expr(&cond.then, Allowed::default())?; + check_expr(&cond.test, Allowed::default())?; + check_expr(&cond.otherwise, Allowed::default()) + } Expr::BoolLit(_) | Expr::NumLit(_, _) | Expr::StrLit(_) @@ -224,6 +229,14 @@ pub enum Expr<'a> { /// This variant should never be used directly. /// It is used for the handling of named arguments in the generator, esp. with filters. ArgumentPlaceholder, + Conditional(Conditional<'a>), +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Conditional<'a> { + pub then: WithSpan<'a, Box>>, + pub test: WithSpan<'a, Box>>, + pub otherwise: WithSpan<'a, Box>>, } #[derive(Clone, Debug, PartialEq)] @@ -315,6 +328,84 @@ impl<'a> Expr<'a> { allow_underscore: bool, ) -> ParseResult<'a, WithSpan<'a, Box>> { let _level_guard = level.nest(i)?; + Self::if_else(i, level, allow_underscore) + } + + /// Like [`Expr::parse()`], but does not parse conditional expressions, + /// i.e. the token `if` is not consumed. + pub(super) fn parse_no_if_else( + i: &mut &'a str, + level: Level<'_>, + allow_underscore: bool, + ) -> ParseResult<'a, WithSpan<'a, Box>> { + let _level_guard = level.nest(i)?; + Self::range(i, level, allow_underscore) + } + + fn if_else( + i: &mut &'a str, + level: Level<'_>, + allow_underscore: bool, + ) -> ParseResult<'a, WithSpan<'a, Box>> { + #[inline(never)] // very unlikely case + fn actually_cond<'a>( + i: &mut &'a str, + level: Level<'_>, + allow_underscore: bool, + then: WithSpan<'a, Box>>, + start: &'a str, + if_span: &'a str, + ) -> ParseResult<'a, WithSpan<'a, Box>>> { + let Some(test) = + opt(|i: &mut _| Expr::parse(i, level, allow_underscore)).parse_next(i)? + else { + return cut_error!( + "conditional expression (`.. if .. else ..`) expects an expression \ + after the keyword `if`", + if_span, + ); + }; + + let Some(else_span) = opt(ws(keyword("else").take())).parse_next(i)? else { + return cut_error!( + "in Askama, the else-case of a conditional expression (`.. if .. else ..`) \ + is not optional", + test.span(), + ); + }; + + let Some(otherwise) = + opt(|i: &mut _| Expr::if_else(i, level, allow_underscore)).parse_next(i)? + else { + return cut_error!( + "conditional expression (`.. if .. else ..`) expects an expression \ + after the keyword `else`", + else_span, + ); + }; + + let expr = Box::new(Expr::Conditional(Conditional { + test, + then, + otherwise, + })); + Ok(WithSpan::new(expr, start, i)) + } + + let start = *i; + let expr = Self::range(i, level, allow_underscore)?; + if let Some(if_span) = opt(ws(keyword("if").take())).parse_next(i)? { + actually_cond(i, level, allow_underscore, expr, start, if_span) + } else { + Ok(expr) + } + } + + fn range( + i: &mut &'a str, + level: Level<'_>, + allow_underscore: bool, + ) -> ParseResult<'a, WithSpan<'a, Box>> { let range_right = move |i: &mut _| { ( ws(alt(("..=", ".."))), @@ -636,6 +727,7 @@ impl<'a> Expr<'a> { Self::BinOp(v) if matches!(v.op, "&&" | "||") => { v.lhs.contains_bool_lit_or_is_defined() || v.rhs.contains_bool_lit_or_is_defined() } + Self::Conditional(cond) => cond.test.contains_bool_lit_or_is_defined(), Self::NumLit(_, _) | Self::StrLit(_) | Self::CharLit(_) diff --git a/askama_parser/src/node.rs b/askama_parser/src/node.rs index 5a304be3d..eb850124b 100644 --- a/askama_parser/src/node.rs +++ b/askama_parser/src/node.rs @@ -581,7 +581,7 @@ impl<'a> Loop<'a> { cut_node( Some("for"), ( - ws(|i: &mut _| Expr::parse(i, s.level, true)), + ws(|i: &mut _| Expr::parse_no_if_else(i, s.level, true)), opt(if_cond), opt(Whitespace::parse), |i: &mut _| s.tag_block_end(i), diff --git a/askama_parser/src/tests.rs b/askama_parser/src/tests.rs index e2a280369..7b28756d5 100644 --- a/askama_parser/src/tests.rs +++ b/askama_parser/src/tests.rs @@ -1,5 +1,6 @@ use winnow::Parser; +use crate::expr::Conditional; use crate::node::{Lit, Raw, Whitespace, Ws}; use crate::{ Ast, Expr, Filter, InnerSyntax, Node, Num, PathComponent, PathOrIdentifier, Span, StrLit, @@ -1647,3 +1648,77 @@ fn test_macro_call_illegal_raw_identifier() { ); } } + +#[test] +fn test_cond_expr() { + let syntax = Syntax::default(); + + assert_eq!( + Ast::from_str("{{ a if b else c }}", None, &syntax) + .unwrap() + .nodes, + [Box::new(Node::Expr( + Ws(None, None), + WithSpan::no_span(Box::new(Expr::Conditional(Conditional { + then: WithSpan::no_span(Box::new(Expr::Var("a"))), + test: WithSpan::no_span(Box::new(Expr::Var("b"))), + otherwise: WithSpan::no_span(Box::new(Expr::Var("c"))), + }))), + ))], + ); + assert_eq!( + Ast::from_str("{{ a if b if c else d else e }}", None, &syntax) + .unwrap() + .nodes, + [Box::new(Node::Expr( + Ws(None, None), + WithSpan::no_span(Box::new(Expr::Conditional(Conditional { + then: WithSpan::no_span(Box::new(Expr::Var("a"))), + test: WithSpan::no_span(Box::new(Expr::Conditional(Conditional { + then: WithSpan::no_span(Box::new(Expr::Var("b"))), + test: WithSpan::no_span(Box::new(Expr::Var("c"))), + otherwise: WithSpan::no_span(Box::new(Expr::Var("d"))), + }))), + otherwise: WithSpan::no_span(Box::new(Expr::Var("e"))), + }))), + ))], + ); + assert_eq!( + Ast::from_str("{{ a if b else c if d else e }}", None, &syntax) + .unwrap() + .nodes, + [Box::new(Node::Expr( + Ws(None, None), + WithSpan::no_span(Box::new(Expr::Conditional(Conditional { + then: WithSpan::no_span(Box::new(Expr::Var("a"))), + test: WithSpan::no_span(Box::new(Expr::Var("b"))), + otherwise: WithSpan::no_span(Box::new(Expr::Conditional(Conditional { + then: WithSpan::no_span(Box::new(Expr::Var("c"))), + test: WithSpan::no_span(Box::new(Expr::Var("d"))), + otherwise: WithSpan::no_span(Box::new(Expr::Var("e"))), + }))), + }))), + ))], + ); + assert_eq!( + Ast::from_str("{{ a if b if c else d else e if f else g }}", None, &syntax) + .unwrap() + .nodes, + [Box::new(Node::Expr( + Ws(None, None), + WithSpan::no_span(Box::new(Expr::Conditional(Conditional { + then: WithSpan::no_span(Box::new(Expr::Var("a"))), + test: WithSpan::no_span(Box::new(Expr::Conditional(Conditional { + then: WithSpan::no_span(Box::new(Expr::Var("b"))), + test: WithSpan::no_span(Box::new(Expr::Var("c"))), + otherwise: WithSpan::no_span(Box::new(Expr::Var("d"))), + }))), + otherwise: WithSpan::no_span(Box::new(Expr::Conditional(Conditional { + then: WithSpan::no_span(Box::new(Expr::Var("e"))), + test: WithSpan::no_span(Box::new(Expr::Var("f"))), + otherwise: WithSpan::no_span(Box::new(Expr::Var("g"))), + }))), + }))), + ))], + ); +} diff --git a/testing/tests/conditional-expr.rs b/testing/tests/conditional-expr.rs new file mode 100644 index 000000000..3f31e9c13 --- /dev/null +++ b/testing/tests/conditional-expr.rs @@ -0,0 +1,277 @@ +use askama::Template; + +#[test] +fn test_associativity() { + // Here we test the associativity of conditional expressions. + // `a if b else c if d else e` is the same as `a if b else (c if d else e)` in Python, + // so we want to parse it right-to-left, too. + + #[derive(Template)] + #[template( + source = r#" + {{- + "a" if a else + "b" if b else + "c" if c else + "d" if d else + "-" + -}} + "#, + ext = "html" + )] + struct Abcd<'a> { + a: bool, + b: &'a bool, + c: &'a &'a bool, + d: &'a &'a &'a bool, + } + + for a in [false, true] { + for b in [&false, &true] { + for c in [&&false, &&true] { + for d in [&&&false, &&&true] { + let expected = match (a, b, c, d) { + (true, _, _, _) => "a", + (_, true, _, _) => "b", + (_, _, true, _) => "c", + (_, _, _, true) => "d", + _ => "-", + }; + let actual = Abcd { a, b, c, d }.to_string(); + assert_eq!(actual, expected, "a={a} b={b} c={c} d={d}"); + } + } + } + } +} + +#[test] +fn test_associativity_reversed() { + // This test makes sure explicit grouping works. + + #[derive(Template)] + #[template( + source = r#" + {{- + ( + ( + ( + "-" if !d else "d" + ) if !c else "c" + ) if !b else "b" + ) if !a else "a" + -}} + "#, + ext = "html" + )] + struct Abcd<'a> { + a: bool, + b: &'a bool, + c: &'a &'a bool, + d: &'a &'a &'a bool, + } + + for a in [false, true] { + for b in [&false, &true] { + for c in [&&false, &&true] { + for d in [&&&false, &&&true] { + let expected = match (a, b, c, d) { + (true, _, _, _) => "a", + (_, true, _, _) => "b", + (_, _, true, _) => "c", + (_, _, _, true) => "d", + _ => "-", + }; + let actual = Abcd { a, b, c, d }.to_string(); + assert_eq!(actual, expected, "a={a} b={b} c={c} d={d}"); + } + } + } + } +} + +#[test] +fn test_associativity_infix() { + // Being a ternary operator, a conditional-expression can be the condition of a + // conditional-expression without the need for explicit grouping. + + #[derive(Template)] + #[template( + source = r#"{{- "a" if then if cond else otherwise else "b" -}}"#, + ext = "html" + )] + struct ThenOtherwise<'a> { + cond: bool, + then: &'a bool, + otherwise: &'a &'a bool, + } + + for cond in [false, true] { + for then in [&false, &true] { + for otherwise in [&&false, &&true] { + let expected = if if cond { *then } else { **otherwise } { + "a" + } else { + "b" + }; + let actual = ThenOtherwise { + cond, + then, + otherwise, + } + .to_string(); + assert_eq!( + actual, expected, + "cond={cond} then={then} otherwise={otherwise}" + ); + } + } + } +} + +// Conditional expressions are just expressions, so you should be able to use them in all +// places that expect an expression. + +#[test] +fn test_call_argument() { + #[derive(Template)] + #[template( + source = r#"{{- method(then if cond else otherwise) -}}"#, + ext = "html" + )] + struct AsArgument { + cond: bool, + then: i32, + otherwise: i32, + } + + impl AsArgument { + fn method(&self, value: &i32) -> i32 { + *value + } + } + + assert_eq!( + AsArgument { + cond: true, + then: 1, + otherwise: -1 + } + .to_string(), + "1" + ); + assert_eq!( + AsArgument { + cond: false, + then: 1, + otherwise: -1 + } + .to_string(), + "-1" + ); +} + +#[test] +fn test_filter_argument() { + #[derive(Template)] + #[template( + source = r#"{{- input | add(then if cond else otherwise) -}}"#, + ext = "html" + )] + struct AsFilterArgument { + input: i32, + cond: bool, + then: i32, + otherwise: i32, + } + + mod filters { + pub(crate) fn add(input: &i32, _: &dyn askama::Values, arg: &i32) -> askama::Result { + Ok(*input + *arg) + } + } + + assert_eq!( + AsFilterArgument { + input: 10, + cond: true, + then: 1, + otherwise: -1 + } + .to_string(), + "11" + ); + assert_eq!( + AsFilterArgument { + input: 10, + cond: false, + then: 1, + otherwise: -1 + } + .to_string(), + "9" + ); + + for input in -10..10 { + for cond in [false, true] { + for then in -10..10 { + for otherwise in -10..10 { + let expected = if cond { + input + then + } else { + input + otherwise + }; + let actual = AsFilterArgument { + input, + cond, + then, + otherwise, + }; + assert_eq!(expected.to_string(), actual.to_string()); + } + } + } + } +} + +#[test] +fn test_if_condition() { + #[derive(Template)] + #[template( + source = r#" + {%- if then if cond else otherwise -%} + a + {%- else -%} + b + {%- endif -%} + "#, + ext = "html" + )] + struct AsIfCondition { + cond: bool, + then: bool, + otherwise: bool, + } + + for cond in [false, true] { + for then in [false, true] { + for otherwise in [false, true] { + let expected = if if cond { then } else { otherwise } { + "a" + } else { + "b" + }; + let actual = AsIfCondition { + cond, + then, + otherwise, + } + .to_string(); + assert_eq!( + actual, expected, + "cond={cond} then={then} otherwise={otherwise}" + ); + } + } + } +} diff --git a/testing/tests/ui/conditional-expr.rs b/testing/tests/ui/conditional-expr.rs new file mode 100644 index 000000000..9240e55de --- /dev/null +++ b/testing/tests/ui/conditional-expr.rs @@ -0,0 +1,47 @@ +use askama::Template; + +// Truncated conditional expressions + +#[derive(Template)] +#[template(ext = "html", source = "{{ 1 + 2 if }}")] +struct StopAfterIf; + +#[derive(Template)] +#[template(ext = "html", source = "{{ 1 + 2 if 🦀 }}")] +struct OtherTokenAfterIf; + +#[derive(Template)] +#[template(ext = "html", source = "{{ 1 + 2 if 3 + }}")] +struct EndInBinaryStateInCondition; + +#[derive(Template)] +#[template(ext = "html", source = "{{ 1 + 2 if 3 + 4 }}")] +struct ElseIsNotOptional; // actually valid in Jinja, defaults to `else 0` + +#[derive(Template)] +#[template(ext = "html", source = "{{ 1 + 2 if 3 + 4 else }}")] +struct StopAfterElse; + +#[derive(Template)] +#[template(ext = "html", source = "{{ 1 + 2 if 3 + 4 else 🦀 }}")] +struct OtherTokenAfterElse; + +// Raw identifiers and strings are not keywords + +#[derive(Template)] +#[template(ext = "html", source = "{{ then r#if condition else otherwise }}")] +struct RawIf; + +#[derive(Template)] +#[template(ext = "html", source = "{{ then if condition r#else otherwise }}")] +struct RawElse; + +#[derive(Template)] +#[template(ext = "html", source = r#"{{ then "if" condition else otherwise }}"#)] +struct StringIf; + +#[derive(Template)] +#[template(ext = "html", source = r#"{{ then if condition "else" otherwise }}"#)] +struct StringElse; + +fn main() {} diff --git a/testing/tests/ui/conditional-expr.stderr b/testing/tests/ui/conditional-expr.stderr new file mode 100644 index 000000000..3d740f8c1 --- /dev/null +++ b/testing/tests/ui/conditional-expr.stderr @@ -0,0 +1,79 @@ +error: conditional expression (`.. if .. else ..`) expects an expression after the keyword `if` + --> :1:9 + "if }}" + --> tests/ui/conditional-expr.rs:6:35 + | +6 | #[template(ext = "html", source = "{{ 1 + 2 if }}")] + | ^^^^^^^^^^^^^^^^ + +error: conditional expression (`.. if .. else ..`) expects an expression after the keyword `if` + --> :1:9 + "if 🦀 }}" + --> tests/ui/conditional-expr.rs:10:35 + | +10 | #[template(ext = "html", source = "{{ 1 + 2 if 🦀 }}")] + | ^^^^^^^^^^^^^^^^^^^ + +error: in Askama, the else-case of a conditional expression (`.. if .. else ..`) is not optional + --> :1:12 + "3 + }}" + --> tests/ui/conditional-expr.rs:14:35 + | +14 | #[template(ext = "html", source = "{{ 1 + 2 if 3 + }}")] + | ^^^^^^^^^^^^^^^^^^^^ + +error: in Askama, the else-case of a conditional expression (`.. if .. else ..`) is not optional + --> :1:12 + "3 + 4 }}" + --> tests/ui/conditional-expr.rs:18:35 + | +18 | #[template(ext = "html", source = "{{ 1 + 2 if 3 + 4 }}")] + | ^^^^^^^^^^^^^^^^^^^^^^ + +error: conditional expression (`.. if .. else ..`) expects an expression after the keyword `else` + --> :1:18 + "else }}" + --> tests/ui/conditional-expr.rs:22:35 + | +22 | #[template(ext = "html", source = "{{ 1 + 2 if 3 + 4 else }}")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: conditional expression (`.. if .. else ..`) expects an expression after the keyword `else` + --> :1:18 + "else 🦀 }}" + --> tests/ui/conditional-expr.rs:26:35 + | +26 | #[template(ext = "html", source = "{{ 1 + 2 if 3 + 4 else 🦀 }}")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: unknown node `r` + --> :1:8 + "r#if condition else otherwise }}" + --> tests/ui/conditional-expr.rs:32:35 + | +32 | #[template(ext = "html", source = "{{ then r#if condition else otherwise }}")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: in Askama, the else-case of a conditional expression (`.. if .. else ..`) is not optional + --> :1:11 + "condition r#else otherwise }}" + --> tests/ui/conditional-expr.rs:36:35 + | +36 | #[template(ext = "html", source = "{{ then if condition r#else otherwise }}")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: failed to parse template source + --> :1:8 + "\"if\" condition else otherwise }}" + --> tests/ui/conditional-expr.rs:40:35 + | +40 | #[template(ext = "html", source = r#"{{ then "if" condition else otherwise }}"#)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: in Askama, the else-case of a conditional expression (`.. if .. else ..`) is not optional + --> :1:11 + "condition \"else\" otherwise }}" + --> tests/ui/conditional-expr.rs:44:35 + | +44 | #[template(ext = "html", source = r#"{{ then if condition "else" otherwise }}"#)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^