From 5a368426d864453099c318bf36d74385944ad96a Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 29 Jan 2025 18:32:49 +0100 Subject: [PATCH] Add SemiStrict and improve Jinja2 compatibility (#687) --- CHANGELOG.md | 7 +++ minijinja/src/compiler/codegen.rs | 7 ++- minijinja/src/environment.rs | 14 +++--- minijinja/src/filters.rs | 12 ++++-- minijinja/src/functions.rs | 2 +- minijinja/src/syntax.rs | 9 ++++ minijinja/src/utils.rs | 48 +++++++++++++-------- minijinja/src/value/argtypes.rs | 4 +- minijinja/src/value/deserialize.rs | 10 +++-- minijinja/src/value/mod.rs | 50 +++++++++++++++------- minijinja/src/value/ops.rs | 4 +- minijinja/tests/test_undefined.rs | 69 ++++++++++++++++++++++++++++++ 12 files changed, 188 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c682b6ec..6ac18fbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to MiniJinja are documented here. +## 2.8.0 + +- Added `SemiStrict` undefined mode that is like strict but allows + to be checked for truthiness. Additionally an if expression without + an else block will always produce a silent undefined object that + never errors for compatibility with Jinja2. #687 + ## 2.7.0 - Removed string interning. #675 diff --git a/minijinja/src/compiler/codegen.rs b/minijinja/src/compiler/codegen.rs index db718be3..ba8364f8 100644 --- a/minijinja/src/compiler/codegen.rs +++ b/minijinja/src/compiler/codegen.rs @@ -7,7 +7,7 @@ use crate::compiler::instructions::{ use crate::compiler::tokens::Span; use crate::output::CaptureMode; use crate::value::ops::neg; -use crate::value::{Kwargs, Value, ValueMap}; +use crate::value::{Kwargs, Value, ValueMap, ValueRepr}; #[cfg(test)] use similar_asserts::assert_eq; @@ -660,7 +660,10 @@ impl<'source> CodeGenerator<'source> { if let Some(ref false_expr) = i.false_expr { self.compile_expr(false_expr); } else { - self.add(Instruction::LoadConst(Value::UNDEFINED)); + // special behavior: missing false block have a silent undefined + // to permit special casing. This is for compatibility also with + // what Jinja2 does. + self.add(Instruction::LoadConst(ValueRepr::SilentUndefined.into())); } self.end_if(); } diff --git a/minijinja/src/environment.rs b/minijinja/src/environment.rs index 8fe1b2d5..25f3e75b 100644 --- a/minijinja/src/environment.rs +++ b/minijinja/src/environment.rs @@ -13,7 +13,7 @@ use crate::expression::Expression; use crate::output::Output; use crate::template::{CompiledTemplate, CompiledTemplateRef, Template, TemplateConfig}; use crate::utils::{AutoEscape, BTreeMapKeysDebug, UndefinedBehavior}; -use crate::value::{FunctionArgs, FunctionResult, Value}; +use crate::value::{FunctionArgs, FunctionResult, Value, ValueRepr}; use crate::vm::State; use crate::{defaults, filters, functions, tests}; @@ -796,10 +796,14 @@ impl<'source> Environment<'source> { state: &State, out: &mut Output, ) -> Result<(), Error> { - if value.is_undefined() && matches!(self.undefined_behavior, UndefinedBehavior::Strict) { - Err(Error::from(ErrorKind::UndefinedError)) - } else { - (self.formatter)(out, state, value) + match (self.undefined_behavior, &value.0) { + // this intentionally does not check for SilentUndefined. SilentUndefined is + // exclusively used in the missing else condition of an if expression to match + // Jinja2 behavior. Those go straight to the formatter. + (UndefinedBehavior::Strict | UndefinedBehavior::SemiStrict, &ValueRepr::Undefined) => { + Err(Error::from(ErrorKind::UndefinedError)) + } + _ => (self.formatter)(out, state, value), } } diff --git a/minijinja/src/filters.rs b/minijinja/src/filters.rs index 143e7cdd..c4e36668 100644 --- a/minijinja/src/filters.rs +++ b/minijinja/src/filters.rs @@ -640,7 +640,9 @@ mod builtins { #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))] pub fn int(value: &Value) -> Result { match &value.0 { - ValueRepr::Undefined | ValueRepr::None => Ok(Value::from(0)), + ValueRepr::Undefined | ValueRepr::SilentUndefined | ValueRepr::None => { + Ok(Value::from(0)) + } ValueRepr::Bool(x) => Ok(Value::from(*x as u64)), ValueRepr::U64(_) | ValueRepr::I64(_) | ValueRepr::U128(_) | ValueRepr::I128(_) => { Ok(value.clone()) @@ -673,7 +675,9 @@ mod builtins { #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))] pub fn float(value: &Value) -> Result { match &value.0 { - ValueRepr::Undefined | ValueRepr::None => Ok(Value::from(0.0)), + ValueRepr::Undefined | ValueRepr::SilentUndefined | ValueRepr::None => { + Ok(Value::from(0.0)) + } ValueRepr::Bool(x) => Ok(Value::from(*x as u64 as f64)), ValueRepr::String(..) | ValueRepr::SmallStr(_) => value .as_str() @@ -1190,7 +1194,9 @@ mod builtins { Ok(rv) } else { match &value.0 { - ValueRepr::None | ValueRepr::Undefined => Ok("".into()), + ValueRepr::None | ValueRepr::Undefined | ValueRepr::SilentUndefined => { + Ok("".into()) + } ValueRepr::Bytes(b) => Ok(percent_encoding::percent_encode(b, SET).to_string()), ValueRepr::String(..) | ValueRepr::SmallStr(_) => Ok( percent_encoding::utf8_percent_encode(value.as_str().unwrap(), SET).to_string(), diff --git a/minijinja/src/functions.rs b/minijinja/src/functions.rs index fd6bb743..08cdff93 100644 --- a/minijinja/src/functions.rs +++ b/minijinja/src/functions.rs @@ -336,7 +336,7 @@ mod builtins { let mut rv = match value { None => ValueMap::default(), Some(value) => match value.0 { - ValueRepr::Undefined => ValueMap::default(), + ValueRepr::Undefined | ValueRepr::SilentUndefined => ValueMap::default(), ValueRepr::Object(obj) if obj.repr() == ObjectRepr::Map => { obj.try_iter_pairs().into_iter().flatten().collect() } diff --git a/minijinja/src/syntax.rs b/minijinja/src/syntax.rs index 7415ca46..1bd6d2a9 100644 --- a/minijinja/src/syntax.rs +++ b/minijinja/src/syntax.rs @@ -180,6 +180,15 @@ //! {{ title|upper if title }} //! ``` //! +//! Note that for compatibility with Jinja2, when the `else` block is missing the undefined +//! value will be marked as "silent". This means even if strict undefined behavior is +//! requested, this undefined value will print to an empty string. This means +//! that this is always valid: +//! +//! ```jinja +//! {{ value if false }} -> prints an empty string (silent undefined returned from else) +//! ``` +//! //! # Tags //! //! Tags control logic in templates. The following tags exist: diff --git a/minijinja/src/utils.rs b/minijinja/src/utils.rs index 007dca80..b74da090 100644 --- a/minijinja/src/utils.rs +++ b/minijinja/src/utils.rs @@ -106,8 +106,9 @@ pub enum AutoEscape { /// Defines the behavior of undefined values in the engine. /// -/// At present there are three types of behaviors available which mirror the behaviors -/// that Jinja2 provides out of the box. +/// At present there are three types of behaviors available which mirror the +/// behaviors that Jinja2 provides out of the box and an extra option called +/// `SemiStrict` which is a slightly less strict undefined. #[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] #[non_exhaustive] pub enum UndefinedBehavior { @@ -116,6 +117,7 @@ pub enum UndefinedBehavior { /// * **printing:** allowed (returns empty string) /// * **iteration:** allowed (returns empty array) /// * **attribute access of undefined values:** fails + /// * **if true:** allowed (is considered false) #[default] Lenient, /// Like `Lenient`, but also allows chaining of undefined lookups. @@ -123,12 +125,21 @@ pub enum UndefinedBehavior { /// * **printing:** allowed (returns empty string) /// * **iteration:** allowed (returns empty array) /// * **attribute access of undefined values:** allowed (returns [`undefined`](Value::UNDEFINED)) + /// * **if true:** allowed (is considered false) Chainable, + /// Like strict, but does not error when the undefined is checked for truthyness. + /// + /// * **printing:** fails + /// * **iteration:** fails + /// * **attribute access of undefined values:** fails + /// * **if true:** allowed (is considered false) + SemiStrict, /// Complains very quickly about undefined values. /// /// * **printing:** fails /// * **iteration:** fails /// * **attribute access of undefined values:** fails + /// * **if true:** fails Strict, } @@ -136,18 +147,17 @@ impl UndefinedBehavior { /// Utility method used in the engine to determine what to do when an undefined is /// encountered. /// - /// The flag indicates if this is the first or second level of undefined value. If - /// `parent_was_undefined` is set to `true`, the undefined was created by looking up - /// a missing attribute on an undefined value. If `false` the undefined was created by - /// looking up a missing attribute on a defined value. + /// The flag indicates if this is the first or second level of undefined value. The + /// parent value is passed too. pub(crate) fn handle_undefined(self, parent_was_undefined: bool) -> Result { match (self, parent_was_undefined) { (UndefinedBehavior::Lenient, false) | (UndefinedBehavior::Strict, false) + | (UndefinedBehavior::SemiStrict, false) | (UndefinedBehavior::Chainable, _) => Ok(Value::UNDEFINED), - (UndefinedBehavior::Lenient, true) | (UndefinedBehavior::Strict, true) => { - Err(Error::from(ErrorKind::UndefinedError)) - } + (UndefinedBehavior::Lenient, true) + | (UndefinedBehavior::Strict, true) + | (UndefinedBehavior::SemiStrict, true) => Err(Error::from(ErrorKind::UndefinedError)), } } @@ -156,10 +166,12 @@ impl UndefinedBehavior { /// This fails only for strict undefined values. #[inline] pub(crate) fn is_true(self, value: &Value) -> Result { - if matches!(self, UndefinedBehavior::Strict) && value.is_undefined() { - Err(Error::from(ErrorKind::UndefinedError)) - } else { - Ok(value.is_true()) + match (self, &value.0) { + // silent undefined doesn't error, even in strict mode + (UndefinedBehavior::Strict, &ValueRepr::Undefined) => { + Err(Error::from(ErrorKind::UndefinedError)) + } + _ => Ok(value.is_true()), } } @@ -176,10 +188,12 @@ impl UndefinedBehavior { /// Are we strict on iteration? #[inline] pub(crate) fn assert_iterable(self, value: &Value) -> Result<(), Error> { - if matches!(self, UndefinedBehavior::Strict) && value.is_undefined() { - Err(Error::from(ErrorKind::UndefinedError)) - } else { - Ok(()) + match (self, &value.0) { + // silent undefined doesn't error, even in strict mode + (UndefinedBehavior::Strict | UndefinedBehavior::SemiStrict, &ValueRepr::Undefined) => { + Err(Error::from(ErrorKind::UndefinedError)) + } + _ => Ok(()), } } } diff --git a/minijinja/src/value/argtypes.rs b/minijinja/src/value/argtypes.rs index 630b3542..701d6c7b 100644 --- a/minijinja/src/value/argtypes.rs +++ b/minijinja/src/value/argtypes.rs @@ -937,7 +937,9 @@ impl TryFrom for Kwargs { fn try_from(value: Value) -> Result { match value.0 { - ValueRepr::Undefined => Ok(Kwargs::new(Default::default())), + ValueRepr::Undefined | ValueRepr::SilentUndefined => { + Ok(Kwargs::new(Default::default())) + } ValueRepr::Object(_) => { Kwargs::extract(&value).ok_or_else(|| Error::from(ErrorKind::InvalidOperation)) } diff --git a/minijinja/src/value/deserialize.rs b/minijinja/src/value/deserialize.rs index 4ebcdbb8..941bf0a1 100644 --- a/minijinja/src/value/deserialize.rs +++ b/minijinja/src/value/deserialize.rs @@ -181,7 +181,9 @@ impl<'de> Deserializer<'de> for Value { ValueRepr::F64(v) => visitor.visit_f64(v), ValueRepr::String(ref v, _) => visitor.visit_str(v), ValueRepr::SmallStr(v) => visitor.visit_str(v.as_str()), - ValueRepr::Undefined | ValueRepr::None => visitor.visit_unit(), + ValueRepr::Undefined | ValueRepr::SilentUndefined | ValueRepr::None => { + visitor.visit_unit() + } ValueRepr::Bytes(ref v) => visitor.visit_bytes(v), ValueRepr::Object(o) => match o.repr() { ObjectRepr::Plain => Err(de::Error::custom("cannot deserialize plain objects")), @@ -197,7 +199,9 @@ impl<'de> Deserializer<'de> for Value { fn deserialize_option>(self, visitor: V) -> Result { match self.0 { - ValueRepr::None | ValueRepr::Undefined => visitor.visit_unit(), + ValueRepr::None | ValueRepr::Undefined | ValueRepr::SilentUndefined => { + visitor.visit_unit() + } _ => visitor.visit_some(self), } } @@ -405,7 +409,7 @@ impl de::Error for Error { fn value_to_unexpected(value: &Value) -> Unexpected { match value.0 { - ValueRepr::Undefined | ValueRepr::None => Unexpected::Unit, + ValueRepr::Undefined | ValueRepr::SilentUndefined | ValueRepr::None => Unexpected::Unit, ValueRepr::Bool(val) => Unexpected::Bool(val), ValueRepr::U64(val) => Unexpected::Unsigned(val), ValueRepr::I64(val) => Unexpected::Signed(val), diff --git a/minijinja/src/value/mod.rs b/minijinja/src/value/mod.rs index dc41a7c5..2f5166b0 100644 --- a/minijinja/src/value/mod.rs +++ b/minijinja/src/value/mod.rs @@ -410,7 +410,11 @@ impl SmallStr { #[derive(Clone)] pub(crate) enum ValueRepr { + /// The regular undefined type produced as part of template evaluation Undefined, + /// A special undefined marker that indicates an always-quiet undefined. + /// This is emitted for ternary expressions with missing else blocks. + SilentUndefined, Bool(bool), U64(u64), I64(i64), @@ -428,7 +432,7 @@ pub(crate) enum ValueRepr { impl fmt::Debug for ValueRepr { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { - ValueRepr::Undefined => f.write_str("undefined"), + ValueRepr::Undefined | ValueRepr::SilentUndefined => f.write_str("undefined"), ValueRepr::Bool(ref val) => fmt::Debug::fmt(val, f), ValueRepr::U64(ref val) => fmt::Debug::fmt(val, f), ValueRepr::I64(ref val) => fmt::Debug::fmt(val, f), @@ -458,7 +462,7 @@ impl fmt::Debug for ValueRepr { impl Hash for Value { fn hash(&self, state: &mut H) { match self.0 { - ValueRepr::None | ValueRepr::Undefined => 0u8.hash(state), + ValueRepr::None | ValueRepr::Undefined | ValueRepr::SilentUndefined => 0u8.hash(state), ValueRepr::String(ref s, _) => s.hash(state), ValueRepr::SmallStr(ref s) => s.as_str().hash(state), ValueRepr::Bool(b) => b.hash(state), @@ -488,7 +492,10 @@ impl PartialEq for Value { fn eq(&self, other: &Self) -> bool { match (&self.0, &other.0) { (&ValueRepr::None, &ValueRepr::None) => true, - (&ValueRepr::Undefined, &ValueRepr::Undefined) => true, + ( + &ValueRepr::Undefined | &ValueRepr::SilentUndefined, + &ValueRepr::Undefined | &ValueRepr::SilentUndefined, + ) => true, (&ValueRepr::String(ref a, _), &ValueRepr::String(ref b, _)) => a == b, (&ValueRepr::SmallStr(ref a), &ValueRepr::SmallStr(ref b)) => a.as_str() == b.as_str(), (&ValueRepr::Bytes(ref a), &ValueRepr::Bytes(ref b)) => a == b, @@ -578,7 +585,10 @@ impl Ord for Value { } match (&self.0, &other.0) { (&ValueRepr::None, &ValueRepr::None) => Ordering::Equal, - (&ValueRepr::Undefined, &ValueRepr::Undefined) => Ordering::Equal, + ( + &ValueRepr::Undefined | &ValueRepr::SilentUndefined, + &ValueRepr::Undefined | &ValueRepr::SilentUndefined, + ) => Ordering::Equal, (&ValueRepr::String(ref a, _), &ValueRepr::String(ref b, _)) => a.cmp(b), (&ValueRepr::SmallStr(ref a), &ValueRepr::SmallStr(ref b)) => { a.as_str().cmp(b.as_str()) @@ -632,7 +642,7 @@ impl fmt::Debug for Value { impl fmt::Display for Value { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self.0 { - ValueRepr::Undefined => Ok(()), + ValueRepr::Undefined | ValueRepr::SilentUndefined => Ok(()), ValueRepr::Bool(val) => val.fmt(f), ValueRepr::U64(val) => val.fmt(f), ValueRepr::I64(val) => val.fmt(f), @@ -1043,7 +1053,7 @@ impl Value { /// perform operations on it. pub fn kind(&self) -> ValueKind { match self.0 { - ValueRepr::Undefined => ValueKind::Undefined, + ValueRepr::Undefined | ValueRepr::SilentUndefined => ValueKind::Undefined, ValueRepr::Bool(_) => ValueKind::Bool, ValueRepr::U64(_) | ValueRepr::I64(_) | ValueRepr::F64(_) => ValueKind::Number, ValueRepr::None => ValueKind::None, @@ -1107,7 +1117,10 @@ impl Value { ValueRepr::String(ref x, _) => !x.is_empty(), ValueRepr::SmallStr(ref x) => !x.is_empty(), ValueRepr::Bytes(ref x) => !x.is_empty(), - ValueRepr::None | ValueRepr::Undefined | ValueRepr::Invalid(_) => false, + ValueRepr::None + | ValueRepr::Undefined + | ValueRepr::SilentUndefined + | ValueRepr::Invalid(_) => false, ValueRepr::Object(ref x) => x.is_true(), } } @@ -1119,7 +1132,7 @@ impl Value { /// Returns `true` if this value is undefined. pub fn is_undefined(&self) -> bool { - matches!(&self.0, ValueRepr::Undefined) + matches!(&self.0, ValueRepr::Undefined | ValueRepr::SilentUndefined) } /// Returns `true` if this value is none. @@ -1220,7 +1233,9 @@ impl Value { /// ``` pub fn get_attr(&self, key: &str) -> Result { let value = match self.0 { - ValueRepr::Undefined => return Err(Error::from(ErrorKind::UndefinedError)), + ValueRepr::Undefined | ValueRepr::SilentUndefined => { + return Err(Error::from(ErrorKind::UndefinedError)) + } ValueRepr::Object(ref dy) => dy.get_value(&Value::from(key)), _ => None, }; @@ -1271,7 +1286,7 @@ impl Value { /// assert_eq!(value.to_string(), "Foo"); /// ``` pub fn get_item(&self, key: &Value) -> Result { - if let ValueRepr::Undefined = self.0 { + if let ValueRepr::Undefined | ValueRepr::SilentUndefined = self.0 { Err(Error::from(ErrorKind::UndefinedError)) } else { Ok(self.get_item_opt(key).unwrap_or(Value::UNDEFINED)) @@ -1305,7 +1320,9 @@ impl Value { /// ``` pub fn try_iter(&self) -> Result { match self.0 { - ValueRepr::None | ValueRepr::Undefined => Some(ValueIterImpl::Empty), + ValueRepr::None | ValueRepr::Undefined | ValueRepr::SilentUndefined => { + Some(ValueIterImpl::Empty) + } ValueRepr::String(ref s, _) => { Some(ValueIterImpl::Chars(0, s.chars().count(), Arc::clone(s))) } @@ -1336,7 +1353,9 @@ impl Value { /// reversible itself, it consumes it and then reverses it. pub fn reverse(&self) -> Result { match self.0 { - ValueRepr::Undefined | ValueRepr::None => Some(self.clone()), + ValueRepr::Undefined | ValueRepr::SilentUndefined | ValueRepr::None => { + Some(self.clone()) + } ValueRepr::String(ref s, _) => Some(Value::from(s.chars().rev().collect::())), ValueRepr::SmallStr(ref s) => { // TODO: add small str optimization here @@ -1627,9 +1646,10 @@ impl Serialize for Value { ValueRepr::U64(u) => serializer.serialize_u64(u), ValueRepr::I64(i) => serializer.serialize_i64(i), ValueRepr::F64(f) => serializer.serialize_f64(f), - ValueRepr::None | ValueRepr::Undefined | ValueRepr::Invalid(_) => { - serializer.serialize_unit() - } + ValueRepr::None + | ValueRepr::Undefined + | ValueRepr::SilentUndefined + | ValueRepr::Invalid(_) => serializer.serialize_unit(), ValueRepr::U128(u) => serializer.serialize_u128(u.0), ValueRepr::I128(i) => serializer.serialize_i128(i.0), ValueRepr::String(ref s, _) => serializer.serialize_str(s), diff --git a/minijinja/src/value/ops.rs b/minijinja/src/value/ops.rs index 082a4d32..531b35d3 100644 --- a/minijinja/src/value/ops.rs +++ b/minijinja/src/value/ops.rs @@ -134,7 +134,9 @@ pub fn slice(value: Value, start: Value, stop: Value, step: Value) -> Result Ok(Value::from(Vec::::new())), + ValueRepr::Undefined | ValueRepr::SilentUndefined | ValueRepr::None => { + Ok(Value::from(Vec::::new())) + } ValueRepr::Object(obj) if matches!(obj.repr(), ObjectRepr::Seq | ObjectRepr::Iterable) => { Ok(Value::make_object_iterable(obj, move |obj| { let len = obj.enumerator_len().unwrap_or_default(); diff --git a/minijinja/tests/test_undefined.rs b/minijinja/tests/test_undefined.rs index c231dfeb..0131fda6 100644 --- a/minijinja/tests/test_undefined.rs +++ b/minijinja/tests/test_undefined.rs @@ -38,6 +38,68 @@ fn test_lenient_undefined() { assert_eq!(render!(in env, "{{ 42 in undefined }}"), "false"); } +#[test] +fn test_semi_strict_undefined() { + let mut env = Environment::new(); + env.set_undefined_behavior(UndefinedBehavior::SemiStrict); + + assert_eq!( + env.render_str("{{ true.missing_attribute }}", ()) + .unwrap_err() + .kind(), + ErrorKind::UndefinedError + ); + assert_eq!( + env.render_str("{{ undefined.missing_attribute }}", ()) + .unwrap_err() + .kind(), + ErrorKind::UndefinedError + ); + assert_eq!( + env.render_str("<{% for x in undefined %}...{% endfor %}>", ()) + .unwrap_err() + .kind(), + ErrorKind::UndefinedError + ); + assert_eq!( + env.render_str("{{ 'foo' is in(undefined) }}", ()) + .unwrap_err() + .kind(), + ErrorKind::UndefinedError + ); + assert_eq!(render!(in env, "<{% if undefined %}42{% endif %}>"), "<>"); + assert_eq!( + env.render_str("<{{ undefined }}>", ()).unwrap_err().kind(), + ErrorKind::UndefinedError + ); + assert_eq!(render!(in env, "{{ undefined is undefined }}"), "true"); + assert_eq!(render!(in env, "<{{ 42 if false }}>"), "<>"); + assert_eq!( + render!(in env, "{{ x.foo is undefined }}", x => HashMap::::new()), + "true" + ); + assert_eq!( + env.render_str( + "<{% if x.foo %}...{% endif %}>", + context! { x => HashMap::::new() } + ) + .unwrap(), + "<>" + ); + assert_eq!( + env.render_str("{{ undefined|list }}", ()) + .unwrap_err() + .kind(), + ErrorKind::InvalidOperation + ); + assert_eq!( + env.render_str("{{ 42 in undefined }}", ()) + .unwrap_err() + .kind(), + ErrorKind::UndefinedError + ); +} + #[test] fn test_strict_undefined() { let mut env = Environment::new(); @@ -67,11 +129,18 @@ fn test_strict_undefined() { .kind(), ErrorKind::UndefinedError ); + assert_eq!( + env.render_str("<{% if undefined %}42{% endif %}>", ()) + .unwrap_err() + .kind(), + ErrorKind::UndefinedError + ); assert_eq!( env.render_str("<{{ undefined }}>", ()).unwrap_err().kind(), ErrorKind::UndefinedError ); assert_eq!(render!(in env, "{{ undefined is undefined }}"), "true"); + assert_eq!(env.render_str("<{{ 42 if false }}>", ()).unwrap(), "<>"); assert_eq!( render!(in env, "{{ x.foo is undefined }}", x => HashMap::::new()), "true"